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.
Table of contents
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
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 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">
<h3>Todo List: ({{ vm.updateIncomplete() }} of {{ vm.todos.length }})</h3>
<div class="todo__fields">
<button type="submit" class="todo__submit">
Add <i class="fa fa-check-circle"></i>
</button>
</div>
<ul class="todo__list">
<li>
<p>{{ item.label }}</p>
<span>
<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.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
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 ``; 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">
<h3>Todo List: ({{ vm.updateIncomplete() }} of {{ vm.todos.length }})</h3>
<div class="todo__fields">
<button type="submit" class="todo__submit">
Add <i class="fa fa-check-circle"></i>
</button>
</div>
<ul class="todo__list">
<li>
<p>{{ item.label }}</p>
<span>
<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 ``:
<!-- 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 ``:
<!-- 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
- Convert your application to ES6 + TypeScript
- Refactor any Directives using a decoupled component approach
- Refactor any Controllers to use controllerAs syntax
- Angular migration guide - ngMigrate