Component pattern for Angular 1.3
Prepare yourself for the future!
This is a re-post from my personal blog. It was a lazy evening, I was watching some video from ng-conf when suddenly Miško Hevery dropped one line which captured my attention and pushed my understanding of what the future holds for us. Despite all the hate and anger, Angular 2.0 is here to stay. Well at least for some time… If you want your code to be worthy of fabled Angular 2.0 you have to use the best available ingredients. You will need ES6 and most probably also the newest incarnation of TypeScript too… Actually need is maybe a bit too strong word for the occasion because Angular 2.0 can be developed also by using good old ES5. But use them, at least in case you want to be considered anywhere near effective while developing YetAnotherGreatApp™. But there is something else, something that is quite key and easily achievable today.
The component paradigm
Let’s start with something what can be considered best practice of today. We are using ui-router to manage state in our applications and we are wiring controllers to templates in our state configuration using controllerAs syntax.
// state/stateModule.js /* global angular */ (function () { "use strict"; angular .module('app.module', [ 'ui.router' ]) .config(config); // @ngInject function config($stateProvider) { $stateProvider .state('app.state', { url: '/state', controller: StateController controllerAs: 'ctrl' templateUrl: 'app/state/state.tpl.html', resolve: { // ... } }); } })();
Controllers benefit from controllerAs syntax which enables them to live and do their job without being depended on soon to be deprecated $scope.
// state/stateController.js /* global angular */ (function () { "use strict"; angular .module('app.module') .controller('StateController', StateController); // @ngInject function StateController() { var ctrl = this; ctrl.doStuff = doStuff; function doStuff() { // ... } } })();
In my experience, in vast majority of circumstances we will wire one particular controller to exactly one particular template, thus we’re dealing with something like a component. The main theme of component concept is reusability, reausability that has such a strong presence in backend development with all the libraries and frameworks, while still being quite elusive (or chaotic if you will) in the frontend environment. Yes we have Angular and Ember but the reality of most applications is that we implement everything time and time again because it’s never completely the same. Component approach is a step into bright future where we don’t rewrite those login forms for the millionth time for all our applications.
Future is now
Let’s start with creating component as a directive. We will use directive definition object where we declare controller, controllerAs, templateUrl, and bindToController properties. New (Angular 1.3.X) bindToController property will help as in situations where we want to pass something to the directive’s isolated scope from parent component. When used, properties from isolated scope will be automatically bound to controller’s this.
// state/stateComponent.js /* global angular */ (function () { "use strict"; angular .module('app.module') .directive('stateComponent', stateComponent); function stateComponent() { return { restrict: 'A', scope: { // isolated scope, use to pass data from parent // eg: data: '=' }, controller: State, controllerAs: 'ctrl', bindToController: true, templateUrl: 'state/state.tpl.html' }; } // @ngInject function State() { var ctrl = this; ctrl.doStuff = doStuff; function doStuff() { // ... } } })();
Now adjust the ui-router state configuration. We will use inline template instead of templateUrl and remove controller related properties. In the inline template we will declare component directive.
// state/stateModule.js /* global angular */ (function () { "use strict"; angular .module('app.module', [ 'ui.router' ]) .config(config); // @ngInject function config($stateProvider) { $stateProvider .state('app.state', { url: '/state', template: '
', resolve: { // ... }, }); } })();You may be asking about the resolve. How will we now inject resolved data to the controller which is not there? While it is possible to inject resolved dependency also to the directive’s controller this can (and should) be solved by using models.
Yes, there will be ES6 too…
One more step, adjust the controller to use ES6 class syntax, which we can transpile as of today using babel (former 6to5) transpiler.
// state/stateComponent.js /* global angular */ (function () { "use strict"; angular .module('app.module') .directive('stateComponent', stateComponent); function stateComponent() { return { restrict: 'A', scope: { // isolated scope, use to pass data from parent // eg: data: '=' }, controller: State, controllerAs: 'ctrl', bindToController: true, templateUrl: 'state/state.tpl.html' }; } class State { constructor() { } doStuff() { // ... } } }());
You may have noticed that we still sport nasty directive definition object, that’s true but now the whole Angular 1.3.X syntax is isolated in just couple of lines of code in the beginning of file. When the time will finally come, it should be trivial to migrate. We just swap directive definition object with new annotation syntax. Following syntax is taken from Angular 2.0 Hello World example and may be subject to change as angular’s API is still in development.
@Component({
// eg: componentServices: [MyService]
})
@Template({
inline:
...
// eg: directives: [RedDec]
})
class State {
constructor() {
}
doStuff() {
// ...
}
}
To wrap it up, Angular still enjoys great popularity despite turbulent nature of JavaScript frameworks landscape and steep concurrence from Facebook’s React. Using approach described in this post should provide you with future proof code that will be easy to migrate when the time is right.