Skip to content

Instantly share code, notes, and snippets.

@bholloway
Last active January 10, 2018 17:43
Show Gist options
  • Save bholloway/20bd8f2776f61eba1595 to your computer and use it in GitHub Desktop.
Save bholloway/20bd8f2776f61eba1595 to your computer and use it in GitHub Desktop.
Angular ES6 Cheatsheet

Composition

It is important not to hide composition. It should be done centrally, at the root of the application.

In angular there are 2 different resources we must compose in order to build an application

  • Using a composition root we map directives, controllers, and services into the injector.

  • Using routing we compose views from HTML partials and controllers for each different application state.

composition root

In dependency injection we maintain a library of components at our disposal. We then assemble these into and application, by composition, using a composition root.

It is important for composition that each component (directives, controllers, services) is small and performs a well defined function. Complex components should be split to separate concerns.

These components fit together all in one place - the composition root. The composition root may be organised into multiple files so long as this aids clarity. Your composition root(s) should be in your library and included by your application main.js. That way if you have a development-only application, its main.js may include the same composition root(s) plus anything additional as an override.

We therefore need define only one module in the entire application. However that module must be dependent on the templates module. Meaning:

  angular.module('myApp', [ 'templates' ])

Where myApp should be customised to your application. This templates module is automatically generated from the HTML partials. The build system converts the HTML to a javascript routine that populates the angular template cache. This makes the partials immediately available to angular in a transparent manner.

routing

In a single page application it is critical that we encode application state in the route. This enables bookmarking and deep linking to reconstruct the page given only the URL.

For every change in view we need to encode state in the route. Where we have hierachical state, as with the angular UI-router, each layer of the state has its own HTML partial and recursively contains other views.

If you find your lowest-level partial is large it is likely you do not have sufficiently detailed state.

Ideally controllers are specified only in the routing. The controller specified here may be explicit, as an actual Class. In this case, there is no need to map any controller in the composition root.

/* globals angular */
import myAppRoutes from 'myAppRoutes';
import myDirective from 'myDirective';
import MyController from 'MyController';
import MyService from 'MyService';
angular.module('myApp', [ 'templates' ])
.config(myAppRoutes)
.directive('myDirective', myDirective)
.service('MyService', MyService)
import MyController from 'MyController';
/**
* <p>TODO description of this file</p>
*/
export default function todoRoutes($stateProvider, $urlRouterProvider) {
'use strict';
$urlRouterProvider.otherwise('/');
$stateProvider
.state('myState', {
url: '/:myParameter',
abstract: false,
templateUrl: 'partials/myPartial.html',
controller: MyController
});
}

Directives

Use a directive to augment a DOM node:

  • To transform its contents in a regular manner.
  • To add a behaviour that is general and reusable.

Do not use a directive:

  • To augment or manipulate shared scope (use a controller instead).
  • To allow a partial to include another partial specific to your application (this is not composable, use nested states instead).

Directives are invoked using hypenated-lowercase but are mapped as camelCase. Their factory is a method so is also camelCase. Meaning:

angular.module(...).directive('myDirective', myDirective);

And in HTML:

<div my-directive></div>

dependency injection

Injection is available at the top of the directive only in its function definition. This must be annotated with the @ngInject doctag.

There is no dependency injection in link(). Consequently you cannot change the order of parameters in the link() method.

restrict

Use any combination of the restrict characters; Attribute name, Element name, CSS Class name.

replace, template, templateUrl

Use either template or templateUrl but not both. The template must not be specific to your use case.

If you need specific content then use transculusion, meaning:

 replace:    true,
 transclude: true,
 template:   '<div ng-transclude></div>',

The replace option relates to the HTML node in which you apply your directive. Generally you will want this node to only comprise your template (any any transcluded content). However when using replace: false your template will appear first, before the existing content.

controller, require, scope

Your directive should have a well defined dependency on your document. Start with scope: { } to isolate your scope.

Do not use a controller() function, use a link() function instead.

Pass parameters explicitly through attributes. Attributes are a parameter of the link() function, or may be coppied to the isolated scope; @ implies string initialisation, = implies 2-way string binding, & implies a function that evaluates the attribute as a statement.

If you need to expose an API, and only in this case, use a Class for the controller property. Meaning controller: MyController (as the definition not as a string). This will be instantiated. Other directives may require your directive and will receive this instance as an additional parameter in its link() method. See the angular docs for full details.

link

The driver of complexity in the directive is the link() function. Where your link function is simple then write it in-line. However if it is complex then make it an external Class.

/**
* <p>TODO description of this directive</p>
* @ngInject
*/
export default function mySmallDirective(/* TODO inject */) {
return {
restrict: 'ACE',
replace: true,
transclude: false,
template: undefined,
templateUrl: undefined,
controller: undefined,
require: undefined,
scope: { },
link: function (scope, element, attributes) {
// TODO only a handful of lines
// assign 2 scope variables or less
// define 2 functions or less
}
};
}
import MyDirectiveLink from 'MyDirectiveLink';
/**
* <p>TODO description of this directive</p>
* @ngInject
*/
export default function myLargeDirective($inject) {
return {
restrict: 'ACE',
replace: true,
transclude: false,
template: undefined,
templateUrl: undefined,
controller: undefined,
require: undefined,
scope: { },
link: function (scope, element, attributes) {
$injector.invoke(MyDirectiveLink, { $scope:scope, element:element, attributes:attributes });
}
};
}

Controllers

Use a controller to augment shared scope.

  • To decorate it with value that HTML partials may bind to.
  • To provide behaviour that HTML partials may invoke.

Do not use a controller:

  • To store application state (describe in the route instead).
  • To store persistent data (controllers and scope are dropped on every route change, use service instead).
  • To implement behaviour and business logic (defer to a service).

In angular, controllers are implemented as a Class. Angular will resolve the dependency as a class and then new that class. Therefore the mapping should be PascalCase as well as the classname itself. For example:

angular.module(...).controller('MyController', MyController);

In our implementation you can effectively assume that scope instanceof controller. This is because in our constructor we will extend() the scope with the members of the instance.

Extend is a method that will copy members of one object to another. It was popularised by jQuery.extend() but we require a different implementation that will copy accessors (get/set) correctly.

We will therefore need to bind() these members (accessors and functions) to ensure that their this reference is not lost.

In javascript instance methods can easily loose their this reference. We can leverage Function.bind() method can to bind all functions and accessors of a given object.

The controller is somewhat similar to the directive link() method. An external link implementation should look exactly like a controller but may inject element and attributes.

Behaviour should follow the flyweight pattern. Meaning that functions should gather operands from the scope and delegate to a method on a service. It should not implement the operation directly.

constructor

Injection is available in the constructor only. This must be annotated with the @ngInject doctag.

Firstly bind the instance and extend scope.

Initialise any private properties.

Add watch statements. Handlers may be organised as members of the class, or may be anonymous.

functions, accessors, properties

Public properties (i.e. variables) are not permitted. Use accessors, meaning getter and/or setter methods.

The function keyword is not needed.

The 'use strict' statement is not needed.

private properties

Private properties should have the same name as any public property that it shadows. A trailing underscore is required per google javascript style.

import bind from 'bind';
import extend from 'extend';
/**
* <p>TODO description of this class</p>
*/
export default class MyController {
/**
* @constructor
* @ngInject
*/
constructor($scope) {
// ensure this is not lost, copy public members to the scope
extend($scope, bind(this));
// private members
this.something_ = 'something';
}
get something() {
return this.something_;
}
set something(value) {
this.something_ = value;
}
method() {
// TODO
}
}

Services

Use service for any Class that is to be mapped into the dependency injector.

Do not use a service for a method (use a Class instead).

Services are implemented as a Class and should be PascalCase. Angular will instantiate once only and inject the same instance in all cases. The dependency is therefore camelCase since it is an instance and should not be new'd. For example:

angular.module(...).service('myService', MyService);

It is prudent to bind() public members (accessors and functions) to ensure that their this reference is not lost.

In javascript instance methods can easily loose their this reference. We can leverage Function.bind() method can to bind all functions and accessors of a given object.

constructor

Injection is available in the constructor only. This must be annotated with the @ngInject doctag.

Firstly bind the instance.

Initialise any private properties.

functions, accessors, properties

Public properties (i.e. variables) are not permitted. Use accessors, meaning getter and/or setter methods.

The function keyword is not needed.

The 'use strict' statement is not needed.

private properties

Private properties should have the same name as any public property that it shadows. A trailing underscore is required per google javascript style.

export default class MyService() {
/**
* @constructor
* @ngInject
*/
constructor(/* TODO inject */) {
// ensure this is not lost
bind(this);
// private members
this.something_ = 'something';
}
get something() {
return this.something_;
}
set something(value) {
this.something_ = value;
}
method() {
// TODO
}
}
Copy link

ghost commented Apr 23, 2017

helpful content

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment