AngularJS migration — transitioning to Component architecture

Transforming Directives and standalone Controllers into Smart and Dumb Components

Pascal Maniraho
Simple
7 min readSep 25, 2017

--

Composing components is like composing music — Photo by Dayne Topkin on Unsplash

Intro

This post introduces daily refactoring as an effective way to transition to Angular from large-scale Angular 1.x apps. The term Angular will refer to the new framework with versions 2, 4.x, 5.x, etc.

It is stuffed with resources to boost your familiarity with Components, and how to introduce them to your existing application without breaking eggs.

We’ve got you covered If you want to “Transition to React Component architecture” instead.

Composability

Components enhance composability of front-end applications. It worth to mentions that Directives are Components too. In that spirit, this article will highlight some changes on directive definition to take into account while transitioning to new Component architecture.

Standalone controllers look more of a directive when paired with a template. This makes them a good candidate to transfer to Component.

Forms are another good example of entities that can be broken down into smaller composable entities.

To keep your mojo going 💪, there is a curated reading list at the end of this article, but it is better to read along to understand the context in which those links may help you even more.

You are reading the 4th article in a series of blog posts about soft code migration. The third article is at this link. I moved Hoo.gya platform that helps you to rent various stuff to/from your friends, neighbours and coworkersfrom Angular 1.x to Angular 4.x.

Put to rest

Angular sunset following features: Directive Definition Object(DDO), Standalone Controllers(making any need of ControllerAs and bindToController obsolete), $scope(and dependents: $on, $emit, $broadcast, $watch, $destroy and $apply). For data binding, digest cycle was replaced by change detection. jqLite has been deprecated and removed. The new framework favors uni-directional(more reactive: Rx/Redux, change events) instead of bi-directional data binding via digest cycles. You can find the more on this source.

…They decided (the single worst strategic mistake that any software company can make) to rewrite the code from scratch ~ Joel On Software

Big-bang vs Incremental migration

There are two mainstream ways to upgrade to Angular 2: a full rewrite(big-bang) or Progressively. A full rewrite may not be the best path if you are limited in terms of time, team and money. Keep in mind support of applications in production, fixing bugs and adding features along the way. Progressively by using ngUpgraders and hack everything to make things work. Progressively, identifying similarities in both frameworks, and modify small parts till the whole thing is moved to a new framework. The latter yields efforts to build inside-out directives/controllers into Components, later on, re-write templates and add proper annotations(you get the picture).

Your recommendation (👏 👏 clapping) is my motivation to followup with a new article. Feel free to leave your questions in comments below, I will be glad to help you however I can!

Where to start

Up to this point, we started by testing 100% of a hypothetical existing legacy app, to have more confidence as we add more code.

We adopted skinny controllers(ControllerAs), and moved most of the business logic into Services. Directives have been restyled to make them look more like Angular+Components. Later in future, the plan is to reduce — up-to-eliminate — dependency on the$scopeobject.

Update Angular+ libraries, Update templates and bootstrap the application on a new architecture, will follow. To adopt Component architecture, the least risky approach is to use the bottom-up approach.

The bottom-up stipulates that we start by transforming smaller directives into components. After this operation is complete, larger (Solo Controller+their templates) directives follow suit. Finally, the last part to migrate is the router. We rinse and repeat this strategy until all routes are completed.

The bottom-up approach reduces collateral damage, allows to fix existing and new bugs — of course if the said bugs are on directives — or adding missing features.

Directives

Link function achieved seamless integration with third-party JavaScript libraries, for instance, jQuery plugins. It helped contain DOM manipulation into a small space. Keeping in mind that Directives are Components, as you migrate your Large scale Angular 1.x app to component first architecture, it will make more sense to leave Directives having Link function as such, and promote others to Components.

# Directive with Link function should remain as such.
.directive('date', function($scope, $element, $attrs, $ctrl){
return{
link: function(){
//leveraging caller's controller function
$element.datetimepicker({onChangeDateTime: $ctrl.onDateChange});
}
};
});

Components

Components are eating Directives for lunch. Components are specialized Directives. It is relatively easy to transform Directives’ DDO to a component object than it is for standalone Controllers. Since Components have a Controller and a Template, standalone Controllers became obsolete. Angular 2 ditched standalone Controllers for good. What is a Component? component is a loose term for a DOM element that has its own view, model, and behaviors”.

# Directive: Bold indicates things that have to go
.directive('editor', function(){
return{
scope:{}, restrict: 'EAC', controllerAs: 'vm',
bindToController: true,
controller: function(){},
template: `<div/>`
};
});
# Becoming Component: Bold indicates new stuff
.component('editor', {
bindings:{}, controllerAs: 'vm',
controller: function(){},
template: `<div/>`
});

Reading list

Forms as Components

Forms are a good example of re-usable components, therefore most likely to cover most scenarios you will run into while doing your migration. Think of a form to edit “awesomeness”. It is easy to name it “awesome”. Later on, you may be attempted to use “awesome” as a model(on $scope) in your templates.

# template
<form name="awesome" ng-init="init()">
<input ng-model="awesome.level">
<button ng-click="save(awesome)"></button>
</form>
# directive
...directive('editAwesome',[function(){
return{
controller: function($scope){
#model initialization
$scope.init = function(){...};
$scope.save = function(form){
if(form.$valid) #send request
};
#on change action
$scope.$watch('awesome', function(){...});
},
templateUrl: 'path/to/edit-awesome.html'
}
};
}]);

ControllerAs

For a smooth transition, introducing ControllerAs notation(available from Angular 1.3) in both Directive Controllers and Standalone Controllers gives an opportunity to component-ize legacy code/app. The same strategy can be applied to Forms, Standalone Controllers, and Controller on Directives in the same fashion.

# DDO
...directive('editAwesome', [{
controller: function EditAwesomeController(){},
controllerAs: 'vm'
}]);
#in template
<div ng-controller="EditAwesomeController as vm"
ng-init="vm.init()">
</div>

Template

As you move with your unit tests though, renaming the form to “vm.awesome”, introduces naming collision with your model “vm.awesome.level”, which makes it difficult to test! Especially if the parent(caller) directive used bi-directional binding on the model, such as in:

# Old way to initialization 
<div awesome="awesome" edit-awesome></div>
# new template form initialization
<edit-awesome awesome="awesome"></edit-awesome>

Bindings

The good thing is to remove the coupling of actual JavaScript from Framework specific constructs. This maximizes future iterations, such as adding Annotations. These modifications improve the performance of your current app, therefore improving the user experience.

# Detach Controller
function EditAwesomeController($scope){
var vm = this;
vm.init = function(){/**request and initialize models*/};
vm.save = function(form){
if(form.$valid)/**send data to server*/
};
# Detach $watch constructs
vm.telephoneChange = function(){/**do UI animation*/};
$scope.$watch('vm.model.telephone', vm.telephoneChange);
# Inject $scope dependency to the controller.
EditAwesomeController.$inject = ['$scope'];
# DDO: Directive Declaration Object
function EditAwesome(){
return{
controllerAs: 'vm',
bindToController:true,
controller
: EditAwesomeController,
template:
`<form name="vm.awesome" ng-init="vm.init()">
<input type="telephone" ng-model="vm.model.telephone"/>
<button ng-click="vm.save(vm.awesome)">Save</button>
</form>`
}
}
}
# Attach EditAwesome Directive to Angular
...directive('editAwesome',EditAwesome);

Dumb versus Smart Components

A smooth component architecture transition, in a manageable way, starts from bottom-up. From directives to controller actions, to routes. Actions become Dumb Component, with bindings to Parent Controller. Later on, Controller becomes a Smart Component, to do the heavy lifting. Smart to Dump Component communication is done via “bindings”(bidirectional, or unidirectional with change detection in Smart Component), or by “require-ing” Smart Component inside Dumb Components.

Change Detection.

# parent template
<ui-view on-action=$ctrl.action()></ui-view>
# smart-dumb components
.component('smart',{ })
.component('smart.dumb',{bindings: {onChange:'&'}},
controller: function(){
this.$onInit() = function(){}
this.save = function(){this.$onChange();}
});

Require

Alternatively, using “require” deeply binds dumb and smart components. This limitation fosters a 1-on-1 relationship between components, hence a limited re-usability. Good news is this approach offloads bindings form $state specifications.

.component('smart',{bindings: {..} })
.component('dumb',{require: '^smart'},
controller: function(){
this.$onInit() = function(){ this.smart.init();}
this.save = function(){this.smart.save();}
});

Router

UI-Router became mainstream in Angular community. UI-Router provides enhanced state management capabilities. Its named views made it easy to nest views at nth level.

# Bootstrapping router the old way:
...run([$routeState,function('$rootScope'){
$rootScope.$on('$stateChangeStart',
function(evt, toState, toParams, fromState, fromParams){})
}]);
# Bootstrapping router the new way:
...run(function($transition$){
$transition$.onStart({to:'*'},
function(transition){
var srvc = transition.injector().get('Service');
var authorize = $transition$.params('data').authorize;
});
});

UI-Router Component support

The new UI-Router adds a twist to support Component architecture. Old Controllers, abstract and non-abstract, can easily be translated into component(smart or dumb). It worth mentioning that $stateParams was replaced by $transition$ service in UI-Router 3.x.

# Component declaration on a router looks like:
$stateProvider
.state('dumb',{component: 'bumb'})
.state('smart',{component: 'smart'})

Abstract States

Abstract states brought Controller inheritance to the next level. Transferring data between states with $stateParams object, made it easy to add session secured private workspace. Initialization requests hook server state(resolved response) to client state.

# old way
$stateProvider
.state('smart',{url, abstract, controller, templateUrl, ctrlAs})
.state('smart.dumb',{url, templateUrl, data:{authorize}})
# new way
$stateProvider
.state(‘smart’,{component:smart, resolve:{}, data:{}})
.state(‘smart.dumb’,{parent:smart},component:’dumb’})

Summary

Angular was built on the success of Angular 1.x, in hope increase flexibility, simplicity, and performance. I hope this post helps you take advantage of Components to move your legacy codebase 1 step forward. Next article will be about: Using Redux, to break $scope dependency, improve performance and increase readiness for Angular 2/4.x/5.x code migration.

Additional readings

Outro

Gosh, You rock If you read all above goodness-es! Thank You! Like last time, YOUR recommendation(👏 👏 👏) motivates me to follow up this post with techniques I use while upgrading Hoo.gy — a platform that makes it possible for you to rent stuff from your friends and neighbors. It is green sharing economy, to curb consumerism and save you thousands in credit card debts, and our planet ;-)

--

--

Pascal Maniraho
Simple
Editor for

Web lover, code crafter, beer drinker, created http://hoo.gy, Montrealer, and training to run a half-marathon :-)