Software in context

Tags


Migrating to AngularJS 2.0

6th November 2014

AngularJS Latte

It's November 2014, and the Angular 2.0 buzz is getting louder.

The team has announced that its next major version will be a complete re-write with no backwards compatibility. We can expect a release to happen at least a year from now, and it's said that support for 1.3 will continue for 18-24 months from that point.

Our goal with Angular 2 is to make the best possible set of tools for building web apps not constrained by maintaining backwards compatibility with existing APIs. Angular blog

As expected, reactions are a mixed bag:

But in general, the community is pretty skeptical. Aside from renewing various neuroses about polluted HTML, "Cult of Google", and Angular looking like a Java framework, the 2.0 news is forcing out a primary fear: I'll eventually have to re-write my 1.3 app if I want support!

Yep.

The reaction is not surprising, because the Internet is usually a safe place to vent untempered fears and opinions. But at the same time, it feels like an overreaction. As software engineers, we should already be in the habit of periodically re-writing codebases to adapt to the latest technologies and evolutions in our systems. And especially as web developers, we should know that the web evolves fast.

It is unfortunate that Angular 1.x wasn't perfectly aligned with the 10 year roadmap of the front-end web. But should we have expected it to be? On the ThoughtWorks blog, Mircea Moise argues that this is an inherent problem with development fraweworks:

Frameworks are wonderful in the sense that they can get you fast to a Minimum Viable Product but you have to pay the price of maintainability and evolution on the long run. AngularJS: The Bad Bits

I completely agree. By definition, a framework locks you into a certain way of doing things. But rather than using it to ditch frameworks altogether, I offer this maxim as evidence that the Angular team is doing the right thing by having a radically different 2.0. The "best possible set of tools" isn't always a superset of the current one. We're on the cusp of a new era of web application development, and it's because we have frameworks that push the technological envelope.

Plus, it's not all that bad!

We have until early 2016 to prepare, and an additional 18-24 months of support brings us up to 2018! That's a long time off in web years. The Angular blog also notes:

Once we have an initial version of Angular 2, we'll start to work on a migration path for Angular 1 apps.

How to Prepare for a Migration

There isn't an initial version of Angular 2 yet, so I'm not going to propose an actual migration path. What I offer below are some preemptive steps you can take now with your 1.3 codebase that should greatly ease an eventual transition. These suggestions emerge directly from the proposed changes. Hopefully they also offer some explanation as to why 2.0 is so different from its predecessor.

What's going to change?

Because it's still in flux, we won't reproduce all aspects of the proposal. Some high-level discussion from ng-learn.org can be found here. Also there are some juicy details hiding behind the hokeyness of this recent (10/28/14) ng-europe presentation. The general themes:

  1. Simplify the template syntax. There is still debate about the details.
  2. Move towards apps built of "components" with cleaner APIs and implicit scopes.
  3. Leverage features coming with ECMAScript 6 and AtScript to reduce the Angular layer and improve IDE support.
  4. Lots of internal cleanup, e.g. using native object observation instead of a digest cycle.

In my mind, numbers (2) and (3) have the greatest present-day implications. We'll get into details as needed within the following suggestions:

1. Make Directives, not Controllers

Web Components

We'll work with an example. First let's create a simplistic 1.0 controller to orient ourselves:

index.html

<div ng-controller="HelloWorldController">  
  <strong>{{message}}</strong>
</div>  

helloWorldController.js

demoApp.controller('HelloWorldController', function($scope) {  
  $scope.message = 'Hello World';  
});

This will output Hello World.

One of the major changes coming in 2.0 is the death of the controller, and a new emphasis on components. In this context, component is a loose term for a DOM element that has its own view, model, and behaviors. With Angular 1, we can reformulate Hello World as a directive to achieve more of a component-based architecture:

index.html

<hello-world>  

helloWorldTemplate.html

<strong>{{message}}</strong>  

helloWorldDirective.js

demoApp.directive('helloWorld', function() {  
  return {
    restrict: 'E',
    templateUrl: 'helloWorldTemplate.html',
    link: function(scope, element, attrs) {
      scope.message = 'Hello World';
    }
  };
});

Some advantages to this approach:

  1. We've housed our markup in it's own template with minimal code; no ng-include required.
  2. We can stamp Hello World anywhere simply by writing <hello-world>.
  3. The markup and its personal scope always travel together.
  4. We can start to cleanly add customizations with HTML attributes like <hello-world language="spanish">.
  5. We get all of this without losing any of the functionality of a standalone controller and $scope, it's here implicitly.

If you've been writing Angular apps for a while, you've maybe started to favor this approach because it feels cleaner to have your app built out of well-defined components, as opposed to a mesh of naked controllers. Angular 2.0 completely buys into this:

In Angular 1, we started with templates and controllers, and the component model was actually an afterthought. In Angular 2, we're embracing the component model, and we're making components the basic building block for building applications. Tobias Bosch

The controller is getting killed off, and components are coming to town. Of all the proposed changes, this is the one that seems to be the most consequential for the design of our applications, and the most natural direction for the framework to go—the idea being that the browser is also headed this way with Web Components.

We'll get into actual 2.0 component definitions shortly. For now, I claim that the single most effective practice you can currently adopt to create migrate-able 1.3 apps is to make directives, not controllers. Many of the other changes will end up being rote translations if the visual building blocks of your app are directive-backed components.

2. Clean up your $scopes

Another big advantage of moving towards component-based apps is that it's easier to define their interfaces; plus, HTML elements already have an easily mappable interface in events, attributes, and properties. Component APIs are evident in this look at a possible 2.0 component definition (transcribed from these ng-europe slides):

@ComponentDirective
class SantaTodoApp {  
  constructor() {
    this.newTodoTitle = '';
  }  
  addTodo: function() { ... }
  removeTodo: function(todo) { ... }
  todosOf: function(filter) { ... }  
}

New syntax aside, there are couple of things to note:

  1. There's no scope object! Because components are the only direct owners of views, we can use their function scopes as the execution context for their respective templates.
  2. The behavior of the component is clearly exposed. This will make for more testable code and empowered IDEs.

A smooth API surface implies inter-component communication. How will components get references to each other? Another ng-europe slide from the SantaTodoApp:

class TabPane {  
  constructor(
    tabContainer:TabContainer,
    element:HTMLElement
  ) { ... }
  ...
}

class TabContainer {  
  constructor(tabs:Query<TabPane>) { ... }
  ...
}

This one demonstrates how DI will be able to inject dependent components, and a new Query mechanism will find and maintain references to them at runtime. Overall we're looking at a much cleaner object-oriented approach to coding interactions between elements of an app, instead of running everything through a network of $scopes.

History Manager Example

So how do we align a scope-based 1.3 codebase with 2.0 ideals? To discover this one we'll have to get our hands dirty with a more realistic example. Let's say we have a directive-backed container that offers generic browser-like back and forward navigation:

index.html

<history-container>  

historyContainerDirective.js

historyApp.directive('historyContainer', function(PageService) {  
  return {
    restrict: 'E',
    templateUrl: 'historyContainerTemplate.html',
    link: function(scope, element, attrs) {

      scope.history = [];
      scope.selectedIndex = 0;

      scope.canGoBack = function() { ... };
      scope.canGoForward = function() { ... };
      scope.navigateBack = function() { ... };
      scope.navigateForward = function() { ... };

      // initialize history
      PageService.getInitialPageIds()
        .then(function(pageIds) {
          angular.forEach(pageIds, function(pageId) {
            scope.history.push(pageId);
          };
          scope.selectedIndex = scope.history.length - 1;
        });

    }
  };
});

A matching template would render navigation buttons and the page at the current index:

historyContainerTemplate.html

<button ng-show="canGoBack()" ng-click="goBack()">Go Back</button>  
<button ng-show="canGoForward()" ng-click="goForward()">Go Forward</button>  
<div ng-repeat="page in history" ng-show="$index === selectedIndex">  
  This is page {{pageId}}.
</div>

So we start with an initial set of pages from a fictional PageService, and we can go back and forth between them. For example's sake the page content is just its ID.

This example wants to demonstrate inter-component communication. We already have one component, let's extract pages into their own directive:

pageDirective.js

historyApp.directive('page', function() {  
  return {
    restrict: 'E',
    templateUrl: 'pageTemplate.html',
    link: function(scope, element, attrs) {
      scope.pageId = attrs.pageId;
    }
  };
});

If this were a real application, this is where we could implement a lookup of the page's content from its ID.

pageTemplate.html

This is page {{pageId}}.  

Finally, update the history template to use <page>:

historyContainerTemplate.html

<button ng-show="canGoBack()" ng-click="goBack()">Go Back</button>  
<button ng-show="canGoForward()" ng-click="goForward()">Go Forward</button>  
<page ng-repeat="pageId in history" ng-show="$index === selectedIndex" page-id="pageId">  

Just like in a real web browser, a page should be able to link to another page, triggering the addition of a state to the history. We can add a scope function to the history container to prepare for this:

historyContainerDirective.js

  ...
  // add a page of history after the current page and navigate to it
  scope.addPage = function(pageId) {    
    scope.history = scope.history.slice(0, scope.selectedIndex + 1);
    scope.history.push(pageId);
    scope.navigateForward();
  };
  ...

Now we're at the point where we can think about how <page> will talk to <history-container>, and we'll try to center on the most future-proof solution.

2.1 Avoid scope.$parent

A easy option is to use $parent from within the page template:

pageTemplate.html

This is page {{pageId}}.  
<button ng-click="$parent.addPage(somePageId)">Shiny new page!</button>  

Since <page> is nested just within <history-container>, this will work fine, but it's brittle—it'll break as soon as this hierarchy isn't the case. Of course we could always change it to $parent.$parent… but we deserve better.

2.2 Avoid scope.$on (if possible)

What about an event-based solution? This seems to be the current popular approach to inter-directive communication. In our case it might look like:

pageTemplate.html

This is page {{pageId}}.  
<button ng-click="doAddPage(somePageId)">Shiny new page!</button>  

Add a function to the page scope to fire the event:

pageDirective.js

  ...
  scope.doAddPage = function(pageId) {
    scope.$emit('ADD_PAGE', pageId);
  };
  ...

We'll need a matching listener on the container scope:

historyContainerDirective.js

  ...
  scope.$on('ADD_PAGE', function(event, pageId) {
    scope.addPage(pageId);
  });
  ...

This is nice because we've solved the $parent brittleness problem; it will work as long as <page> is anywhere within <history-container>. Additionaly an event-based design looks more like a directive API because the code for sending and listening can at least reside within the directives they represent. But we've introduced some new problems:

  1. It's now on us to maintain consistency about the name of the event.
  2. Events are a one way phenomena. If historyContainer's $on ever goes away for some reason, page's $emit won't care, it will keep on firing silently. Not good for maintainability.
  3. If we want communication in the downward direction, from parent to child scope, we'll have to use $broadcast instead of $emit.

Outside of writing a more sophisticated, injectable service that finds and manages references to directives (essentially the proposed query mechanism for 2.0), I haven't found ways to avoid resorting to $broadcast or $emit to pass information around. I'm open to suggestions.

2.3 Avoid scope message passing

Another solution is to have <page> set a property that <history-container> can $watch, for example on the $rootScope:

pageDirective.js

  ...
  scope.doAddPage = function(pageId) {
    $rootScope.pageId = pageId;
  };
  ...

And on the receiving end:

historyContainerDirective.js

  ...
  $rootScope.$watch('newPageId', function(newPageId) {
    scope.addPage(pageId);
  });
  ...

This approach improves on event-based communication in that it works regardless of the components' relative positions in the scope hierarchy, but again has its own baggage:

  1. The $rootScope has to be injected all the time, although we can avoid this if we retool to leverage scope inheritance.
  2. All inter-component dialog must be funneled through a single object, which is awkward.
  3. Like $ons, $watches can silently outlive code that sets their property.
  4. If a $watched item gets updated outside of the digest cycle, we must remember to call scope.$apply() or nothing will happen until the next cycle.

This doesn't get us much further, because just like with the event system, object watching is going to get a complete overhaul in Angular 2.

2.4 Prefer scope inheritance with namespaces

The solution I've settled on for inter-directive communication that looks to be the easiest to migrate is based on scope inheritance. I didn't tell you, but back in example 2.1, we could have avoided using $parent altogether:

pageTemplate.html

This is page {{pageId}}.  
<button ng-click="addPage(somePageId)">Shiny new page!</button>  

Because scopes inherit their parent scopes' properties via the prototype chain, addPage resolves to the function in <history-container> we originally wanted to call.

The problem here is that if we ever write scope.addPage = somethingElse; from the page directive, we'll create an addPage in the page scope and "mask" the one in historContainer. This nice discussion of Angular scopes thoroughly explains this phenomenon.

A way to avoid this is to add a namespace to the historyContainer scope. This looks like a slightly altered addPage definition:

historyContainerDirective.js

  ...
  scope.historyContainer = {};
  scope.historyContainer.addPage = function(pageId) {    
    scope.history = scope.history.slice(0, scope.selectedIndex + 1);
    scope.history.push(pageId);
    scope.navigateForward();
  };
  ...

Now code like scope.historyContainer.addPage = somethingElse; will have to resolve historyContainer in the prototype chain before the assignment occurs. Lucky for us, this has the added benefit of being way more readable:

pageTemplate.html

This is page {{pageId}}.  
<button ng-click="historyContainer.addPage(somePageId)">Shiny new page!</button>  

And if we stuff all the historyContainer scope things into scope.historyContainer, we have something that resembles a well-defined API, and should be much easier to translate directly into an ECMAScript 6 class like the SantaTodoApp when the time comes.

This is the convention I've adopted for my Angular apps. For a given directive sampleDirective, all scope properties live at scope.sample.*. The only downside is that it only works for talking to parent scopes. A system like the proposed Query mechanism is necessary to get direct references to child components. When this is necessary I fall back to $broadcast.

It's worth noting that a similar feel can be achieved with the controller as sytax, but this clashes with our suggestion to move off of naked controllers.

3. Try TypeScript

Another big influencer of the changes coming in Angular 2 is the eventual arrival of an ECMAScript 6 super-implementation called AtScript. This evolution in ECMAScript—also called Harmony—addresses a lot of the shortcomings that prevent JavaScript from being a bone fide software engineering language, primarily by adding classes and modules.

Remember that ECMAScript is a specification (of which the various flavors of JavaScript are only partial implementations). Here's a slide that's been making the rounds lately:

Script Intersections

TypeScript is an existing ES6 implementation from Microsoft that adds types, and AtScript is a proposal from Google for an implementation that adds annotations like the @ComponentDirective above.

By embracing AtScript, Angular 2 can remove the need for a custom module system, and excise all that funky service, provider, and factory stuff. Different components of an app can just be defined as native classes, modularized for DI, and annotated to have special meaning within an Angular context. This should result in a much thinner Angular… and maybe fewer "Angular way" complaints?

AtScript isn't around yet, but it only builds on the already-established TypeScript. Thus my final suggestion for creating a future-proof Angular app is to try TypeScript. There's a TypeScript → JavaScript compiler for Node that should be handy for writing pieces of an app as ES6 classes. And since future apps will have to maintain backwards compatibility for ES5 probably forever, it'll be worth setting up the tooling for it in your project.


By no means did we cover all the ways to prepare for a migration to AngularJS 2.0 from a 1.3 codebase. Please share if you have anything to add! The details of the proposed changes are definitely not set in stone, as echoed daily by the Angular team. But I think the spirit of the proposed changes are certain, namely (1) a move toward components, and (2) a move toward ES6. If we prepare our code for these, we'll be ahead of the game, and a full migration will be mostly syntactical conversion, and result in a refreshingly simplified codebase.

Related posts:

Caleb Sotelo
AUTHOR

Caleb Sotelo

I'm a Software Engineer and Director of OpenX Labs. I try to write about software in a way that helps people truly understand it. Follow me @calebds.

View Comments