Todd Motto

Todd Motto

Owner, Ultimate Angular

Walkthrough to upgrade an Angular 1.x component to Angular 2
Oct 27, 2015
10 mins read
Edit post

In this article we’re going to look at upgrading your first AngularJS (1.x) component, a simple todo app, across to Angular (v2+) code. We’ll compare the API differences, templating syntaxes and hopefully it’ll shed some light on upgrading to Angular, and well as making it appear less daunting.

AngularJS Todo App

We’ll be rewriting this small component in Angular, so let’s look at the existing functionality:

  • Add items to todo list
  • Ability to delete items
  • Ability to mark items as complete
  • Display count of incomplete and total todos

Let’s look at the source code to understand exactly how it’s built and what’s going on.

The HTML is extremely simple, a <todo> element.

<todo></todo>

And JavaScript Directive:

function todo() {
return {
scope: {},
controller: function () {
// set an empty Model for the <input>
this.label = '';
// have some dummy data for the todo list
// complete property with Boolean values to display
// finished todos
this.todos = [{
label: 'Learn Angular',
complete: false
},{
label: 'Deploy to S3',
complete: true
},{
label: 'Rewrite Todo Component',
complete: true
}];
// method to iterate the todo items and return
// a filtered Array of incomplete items
// we then capture the length to display 1 of 3
// for example
this.updateIncomplete = function () {
return this.todos.filter(function (item) {
return !item.complete;
}).length;
};
// each todo item contains a ( X ) button to delete it
// we simply splice it from the Array using the $index
this.deleteItem = function (index) {
this.todos.splice(index, 1);
};
// the submit event for the <form> allows us to type and
// press enter instead of ng-click on the <button> element
// we capture $event and prevent default to prevent form submission
// and if the label has a length, we'll unshift it into the this.todos
// Array which will then add the new todo item into the list
// we'll then set this.label back to an empty String
this.onSubmit = function (event) {
if (this.label.length) {
this.todos.unshift({
label: this.label,
complete: false
});
this.label = '';
}
event.preventDefault();
};
},
// instantiate the Controller as "vm" to namespace the
// Class-like Object
controllerAs: 'vm',
// our HTML template
templateUrl: '../partials/todo.html'
};
}

angular
.module('Todo', [])
.directive('todo', todo);

// manually bootstrap the application when DOMContentLoaded fires
document.addEventListener('DOMContentLoaded', function () {
angular.bootstrap(document, ['Todo']);
});

The todo.html contents, a simple template that holds the UI logic to repeat our todo items, manage all submit/deleting functionality. This should all look pretty familiar.

<div class="todo">
<form ng-submit="vm.onSubmit($event);">
<h3>Todo List: ({{ vm.updateIncomplete() }} of {{ vm.todos.length }})</h3>
<div class="todo__fields">
<input ng-model="vm.label" class="todo__input">
<button type="submit" class="todo__submit">
Add <i class="fa fa-check-circle"></i>
</button>
</div>
</form>
<ul class="todo__list">
<li ng-repeat="item in vm.todos" ng-class="{
'todo__list--complete': item.complete
}"
>
<input type="checkbox" ng-model="item.complete">
<p>{{ item.label }}</p>
<span ng-click="vm.deleteItem($index);">
<i class="fa fa-times-circle"></i>
</span>
</li>
</ul>
</div>

The app is complete below:

Migration preparation

One of the design patterns I highly recommend is using the controllerAs syntax (see my article here on it) inside the Directive definition, this allows our Controllers to be free of injecting $scope and adopt a more “Class-like” way of writing Controllers. We use the this keyword to create public methods which then gets bound to the $scope automatically by Angular at runtime.

Using controllerAs, IMO, is a crucial step to preparing AngularJS components for migration to Angular, as the way we write components in Angular uses the this keyword on an Object definition for our public methods.

Project setup/bootstrapping

Files to include, and boostrapping the application.

Angular 1.x

We’re going to walk through every single part of the setup of AngularJS versus Angular, from bootstrapping the application to creating the component, so follow closely.

We have the basic HTML page, including version 1.4.7 of AngularJS, and manually bootstrapping the application using angular.bootstrap.

<!doctype html>
<html>
<head>
<script src="//code.angularjs.org/1.4.7/angular.min.js"></script>
</head>
<body>
<todo></todo>
<script>
document.addEventListener('DOMContentLoaded', function () {
angular.bootstrap(document, ['Todo']);
});
</script>
</body>
</html>
Angular

We’re going to actually create the Angular application component in ES5, there will be no ES6 and TypeScript because this will let you write Angular in the browser with ease, and also the final working example is using ES5 running in JSFiddle.

There will, however, be the TypeScript/ES6 example at the end to demonstrate the full migration from 1.x to ES5, then the final ES6 + TypeScript solution.

First we need to include Angular, I’m not going to npm install or mess about installing dependencies, how-to steps are on the angular.io website. Let’s get up and running and learn the framework basics and migrate our AngularJS app.

First, we need to include Angular in the <head>; you’ll notice I’m using angular2.sfx.dev.js from version 2.0.0-alpha.44. This .sfx. means it’s the self-executing bundled version, targeted at ES5 use without System loader polyfills, so we don’t need to add System.js to our project.

<!doctype html>
<html>
<head>
<script src="//code.angularjs.org/2.0.0-alpha.44/angular2.sfx.dev.js"></script>
</head>
<body>
<todo></todo>
<script>
document.addEventListener('DOMContentLoaded', function () {
ng.bootstrap(Todo);
});
</script>
</body>
</html>

So far everything is super simple, instead of window.angular we have window.ng as the global namespace.

Component definition

Upgrading the Directive to an Angular component.

AngularJS

Stripping out all the JavaScript Controller logic from the Directive leaves us with something like this:

function todo() {
return {
scope: {},
controller: function () {},
controllerAs: 'vm',
templateUrl: '../partials/todo.html'
};
}

angular
.module('Todo', [])
.directive('todo', todo);
Angular

In Angular, we create a Todo variable, which assigns the result of ng to it with corresponding chained definitions (Component, Class) - these are all new in Angular.

Inside .Component(), we tell Angular to use the selector: 'todo', which is exactly the same as .directive('todo', todo); in AngularJS. We also tell Angular where to find our template, just like in AngularJS we use the templateUrl property.

Finally, the .Class() method is what holds the logic for our component, we kick things off with a constructor property that acts as the “constructor” class. So far so good!

var Todo = ng
.Component({
selector: 'todo',
templateUrl: '../partials/todo.html'
})
.Class({
constructor: function () {}
});

document.addEventListener('DOMContentLoaded', function () {
ng.bootstrap(Todo);
});

Component logic

Next, it makes sense to move our Controller logic from AngularJS across to Angular’s .Class() method. If you’ve used ReactJS, this will look familiar. This is also why I suggest using controllerAs syntax because this process will be extremely simple to do.

AngularJS

Let’s look what we have in our todo component already. Public methods use this to bind to the $scope Object automatically for us, and we’re using controllerAs: 'vm' to namespace the instance of the Controller for use in the DOM.

controller: function () {
this.label = '';
this.todos = [{
label: 'Learn Angular',
complete: false
},{
label: 'Deploy to S3',
complete: true
},{
label: 'Rewrite Todo Component',
complete: true
}];
this.updateIncomplete = function () {
return this.todos.filter(function (item) {
return !item.complete;
}).length;
};
this.deleteItem = function (index) {
this.todos.splice(index, 1);
};
this.onSubmit = function (event) {
if (this.label.length) {
this.todos.unshift({
label: this.label,
complete: false
});
this.label = '';
}
event.preventDefault();
};
},
controllerAs: 'vm',
Angular

Now, let’s kill the Controller entirely, and move these public methods into the .Class() definition inside Angular:

.Class({
constructor: function () {
this.label = '';
this.todos = [{
label: 'Learn Angular',
complete: false
},{
label: 'Deploy to S3',
complete: true
},{
label: 'Rewrite Todo Component',
complete: true
}];
},
updateIncomplete: function () {
return this.todos.filter(function (item) {
return !item.complete;
}).length;
},
deleteItem: function (index) {
this.todos.splice(index, 1);
},
onSubmit: function (event) {
if (this.label.length) {
this.todos.unshift({
label: this.label,
complete: false
});
this.label = '';
}
event.preventDefault();
}
});

Learnings here: “public” methods become properties of the Object passed into the .Class() method, and we don’t need to refactor any of the code because in AngularJS we were using the controllerAs syntax alongside the this keyword - seamless and easy.

At this stage, the component will work, however the template we have is completely based off AngularJS Directives, so we need to update this.

Template migration

Here’s the entire template that we need to migrate to the new syntax:

<div class="todo">
<form ng-submit="vm.onSubmit($event);">
<h3>Todo List: ({{ vm.updateIncomplete() }} of {{ vm.todos.length }})</h3>
<div class="todo__fields">
<input ng-model="vm.label" class="todo__input">
<button type="submit" class="todo__submit">
Add <i class="fa fa-check-circle"></i>
</button>
</div>
</form>
<ul class="todo__list">
<li ng-repeat="item in vm.todos" ng-class="{
'todo__list--complete': item.complete
}"
>
<input type="checkbox" ng-model="item.complete">
<p>{{ item.label }}</p>
<span ng-click="vm.deleteItem($index);">
<i class="fa fa-times-circle"></i>
</span>
</li>
</ul>
</div>

Let’s be smart and attack this in chunks though, keeping only the functional pieces we need. Starting with the <form>:

<!-- AngularJS -->
<form ng-submit="vm.onSubmit($event);">

</form>

<!-- Angular -->
<form (submit)="onSubmit($event);">

</form>

Key changes here are the new (submit) syntax, this indicates that an event is to be bound, where we pass in $event as usual. Secondly, we are no longer needing a Controller, which means controllerAs is dead - note how the vm. prefix is dropped - this is fantastic.

Next up is the two-way binding on the <input>:

<!-- AngularJS -->
<input ng-model="vm.label" class="todo__input">

<!-- Angular -->
<input [(ng-model)]="label" class="todo__input">

This sets up two-way binding on ng-model, also dropping the vm. prefix. This fully refactored section of code will look like so:

<form (submit)="onSubmit($event);">
<h3>Todo List: ({{ updateIncomplete() }} of {{ todos.length }})</h3>
<div class="todo__fields">
<input [(ng-model)]="label" class="todo__input">
<button type="submit" class="todo__submit">
Add <i class="fa fa-check-circle"></i>
</button>
</div>
</form>

Moving onto the list of todo items. There’s quite a lot going on here, the ng-repeat over the todo items, a conditional ng-class to show items completed (crossed out), a checkbox to mark things as complete, and finally the ng-click binding to delete that specific todo item from the list.

<!-- AngularJS -->
<ul class="todo__list">
<li ng-repeat="item in vm.todos" ng-class="{
'todo__list--complete': item.complete
}"
>
<input type="checkbox" ng-model="item.complete">
<p>{{ item.label }}</p>
<span ng-click="vm.deleteItem($index);">
<i class="fa fa-times-circle"></i>
</span>
</li>
</ul>

<!-- Angular -->
<ul class="todo__list">
<li *ng-for="#item of todos; #i = index" [ng-class]="{
'todo__list--complete': item.complete
}">
<input type="checkbox" [(ng-model)]="item.complete">
<p>{{ item.label }}</p>
<span (click)="deleteItem(i);">
<i class="fa fa-times-circle"></i>
</span>
</li>
</ul>

The differences here are mainly in the ng-repeat syntax and moving across to ng-for, which uses #item of Array syntax. Interestingly enough, $index isn’t given to us “for free” anymore, we have to request it and assign it to a variable to gain access to it (#i = $index) which then allows us to pass that specific Array index into the deleteItem method.

Altogether we have our finished Angular component markup migration:

<div class="todo">
<form (submit)="onSubmit($event);">
<h3>Todo List: ({{ updateIncomplete() }} of {{ todos.length }})</h3>
<div class="todo__fields">
<input [(ng-model)]="label" class="todo__input">
<button type="submit" class="todo__submit">
Add <i class="fa fa-check-circle"></i>
</button>
</div>
</form>
<ul class="todo__list">
<li *ng-for="#item of todos; #i = index" [ng-class]="{
'todo__list--complete': item.complete
}">
<input type="checkbox" [(ng-model)]="item.complete">
<p>{{ item.label }}</p>
<span (click)="deleteItem(i);">
<i class="fa fa-times-circle"></i>
</span>
</li>
</ul>
</div>

Altogether our Angular component will look like so:

var Todo = ng
.Component({
selector: 'todo',
template: [
'<div class="todo">',
'<form (submit)="onSubmit($event);">',
'<h3>Todo List: ({{ updateIncomplete() }} of {{ todos.length }})</h3>',
'<div class="todo__fields">',
'<input [(ng-model)]="label" class="todo__input">',
'<button type="submit" class="todo__submit">',
'Add <i class="fa fa-check-circle"></i>',
'</button>',
'</div>',
'</form>',
'<ul class="todo__list">',
'<li *ng-for="#item of todos; #i = index" [ng-class]="{',
'\'todo__list--complete\': item.complete',
'}">',
'<input type="checkbox" [(ng-model)]="item.complete">',
'<p>{{ item.label }}</p>',
'<span (click)="deleteItem(i);">',
'<i class="fa fa-times-circle"></i>',
'</span>',
'</li>',
'</ul>',
'</div>'
].join(''),
directives: [
ng.CORE_DIRECTIVES,
ng.FORM_DIRECTIVES
]
})
.Class({
constructor: function () {
this.label = '';
this.todos = [{
label: 'Learn Angular',
complete: false
},{
label: 'Deploy to S3',
complete: true
},{
label: 'Rewrite Todo Component',
complete: true
}];
},
updateIncomplete: function () {
return this.todos.filter(function (item) {
return !item.complete;
}).length;
},
deleteItem: function (index) {
this.todos.splice(index, 1);
},
onSubmit: function (event) {
if (this.label.length) {
this.todos.unshift({
label: this.label,
complete: false
});
this.label = '';
}
event.preventDefault();
}
});

It’s important to note an additional directives: [] property inside the .Component() method, this tells the component what Directives to include for us to use. We have used ng-for and ng-model which are from the CORE and FORM Directive modules, so we need to explicitly define them inside the Array as dependencies:

directives: [
ng.CORE_DIRECTIVES,
ng.FORM_DIRECTIVES
]

And that’s it! The working solution:

Check out the Angular cheatsheet, this is extremely handy when refactoring your templates from AngularJS to Angular.

ES6 + TypeScript version

import {
Component,
CORE_DIRECTIVES,
FORM_DIRECTIVES
} from 'angular2/angular2';

@Component({
selector: 'todo'
templateUrl: '../partials/todo.html',
directives: [
CORE_DIRECTIVES,
FORM_DIRECTIVES
]
})

export class Todo {

constructor() {
this.label = '';
this.todos = [{
label: 'Learn Angular',
complete: false
},{
label: 'Deploy to S3',
complete: true
},{
label: 'Rewrite Todo Component',
complete: true
}];
}

updateIncomplete() {
return this.todos.filter(item => !item.complete).length;
}

deleteItem(index) {
this.todos.splice(index, 1);
}

onSubmit(event) {
if (this.label.length) {
this.todos.unshift({
label: this.label,
complete: false
});
this.label = '';
}
event.preventDefault();
}

}

Note how we’re using ES6 import, with TypeScript @ decorators (@Component), as well as the ES6 class syntax to define a new Class to be exported.

We’re also not using any browser globals (window.ng) which is fantastic, all dependencies we need get imported from 'angular2/angular2', even our directives: [] dependency Array.

Visit angular.io for everything else.

Steps to take now to prepare for Angular

Oct 24, 2015

Superfast Angular: use ngModelOptions to limit $digest cycles

The $digest cycle is the critical entity for keeping our Angular applications fast: the faster...

Nov 13, 2015

Exploring the Angular 1.5 .component() method

AngularJS 1.5 introduced the .component() helper method, which is much simpler than the .directive() definition...