Updated 08/13/2016
AngularJS was originally created back in 2009. It was built initially with the hope of helping web designers sprinkle a little magic into their html. What happened instead was the revolution of the single page application. While AngularJS is still very powerful it has its warts, after all it was built with design first in mind. Given new libraries and architectures like Flux with React coming out, with speed and developer savvy in mind, AngularJS needed a revamp.
Now that the revamp of AngularJS is here, in the form of Angular 2. Currently production ready in rc phase 4. After all we are talking Google and everything in production is beta. I think it is now time to investigate the Keys to the Kingdom of Angular 2.
While following along the process of Angular 2’s creation, I didn’t initially see any easy way to upgrade your apps from Angular 1 to 2. However, with NG-CONF and the announcement of AngularJS 1.5, it looks like there is now a nice path forward, but that is for another post.
In this blog post I am going to start you on your way to picking up Angular 2 based on what you know from Angular 1 (pre 1.5). The true power of Angular 1 was in something called a Directive, and this is where I will start. I will be explaining the Angular 1 approach as I go along in the case you are new to Angular 1 as well.
I will be using Typescript for Angular 2 throughout this post as I want to show Angular 2 in all its glory. As we walk through the differences in how to create a Directive between Angular 1 and 2 we will be building Task and Subtask directives. If you wish to follow along by writing the code yourself you can get a start here:
This will be the start of many differences you see between the Angular 1 and 2. In this section we aren’t going to add any
special logic. We will be focusing strictly on setting up the directive files only.
In Angular 1 we create a directive by creating a .directive
function.
// tasks.js
angular.module('TaskApp');
app.directive('tasks',function() {
return {};
});
In Angular 2 you have three things you have to do:
@Component
or @Directive
annotation// app/tasks.ts
import {Component, View} from 'angular2/core';
@Component({
selector: 'tasks',
})
@View({
template: '<p>Hi</p>'
})
export class Tasks { }
After seeing the differences in setting up the the directive files between Angular 1 and 2 you probably have some questions.
@Component
instead of @Directive
? To start out, in Angular 2 everything is a Directive. There is no more
deciding on whether I should use a controller or a directive. The annotation @Component
is a directive that requires a view. This will be
what you use to build out all the different parts of your Angular app’s view, in essence it is Angular 2’s foundation. The
annotation @Directive
is a directive that acts on a behavior. You would use this to add behavior to your different HTML elements.@View
or can we put it somewhere external? @View
also has the templateUrl option you
can add to the metadata properties. With templateUrl you can point to a file location for your template.@View
with @Component
? You will always need to have a view with a @Component
. However, you do not
necessarily need to use the @View
annotation. @Component
actually contains the option of template and templateUrl as well. The power
of using @View
though is it allows you to specify devices to apply the display on, for example you can have one for mobile devices and one for the desktop.import
and export
? In Angular2 everything is modular, and therefore better by default, including the core Angular library.
So if you need something external from your Component you must import
it into your new component hence the import {Component, View} from 'angular2/core';
.
To make your class/component available to the rest of your Angular app you will need to export
it hence the export class Task {}
.
If you have been doing AngularJS for very long you probably made your Angular 1 app modular by using something like RequireJS or Webpack.
If you have never used RequireJS or Webpack I would advise you to do so. This will make the upgrade path
even easier. Here is what it would look like if you were using RequireJS or Webpack: // tasks.directive.js
(function() {
'use strict';
module.exports = Tasks;
Tasks.$inject = [];
function Tasks() {
return {
};
}
})();
/* tasks.module.js */
angular.module('TaskApp')
.directive('Tasks', require('./tasks.directive'));
Now we have setup the directives in both Angular 1 and 2, and understand how the setups differ. It is now time to add some logic to these Directives. We will be adding the ability to Add Tasks to a list by entering the Task name and clicking a Submit button.
We will start in Angular 1. We need to start out by adding <task>
tags to the index.html. It is between these tags
that Angular does all its work. For this post we are not concerned with ng-controller="MainCtrl"
, but this is required in
Angular 1 as everything starts with a controller. If you are familiar with Angular 1 you can just view the code snippets and move
on to the Angular 2 section.
<!-- index.html -->
<body ng-controller="MainCtrl">
<tasks></tasks>
</body>
Now lets build the tasks template in tasks.html.
<!-- tasks.html -->
<input type="text" ng-model="newTask" placeholder="Enter Tasks"/>
<button type="button" class="btn btn-primary" ng-click="addTask()">Submit</button>
<ol>
<li ng-repeat="task in tasks track by $index">
<h3>{ { task }}</h3>
</li>
</ol>
The only things to notice in the template are:
ng-model="newTask"
- In Angular 1, ng-model is a way to associate and/or use a variable in the Tasks directive’s scope in the template. It is
also a way to pass data from one directive to the next.ng-click="addTask()"
- Here we are creating an on-click event and having it call the Tasks directive’s addTask()
function.ng-repeat="task in tasks track by $index"
- This is Angular 1’s template version of a for loop. We are just looping through the
tasks on the Tasks directive’s scope and tracking each task by it’s index. There will be a <li></li>
tag created for each task.Now that we have the tags in the index.html and template for the Tasks directive lets actually build it out.
// tasks.js
angular.module('TaskApp')
.directive('tasks', function() {
return {
restrict: 'E',
templateUrl: 'tasks.html',
link: function(scope, element, attrs){
scope.tasks = [];
scope.newTask = '';
scope.addTask = function(){
if(scope.newTask !== ''){
scope.tasks.push(scope.newTask);
scope.newTask = '';
}
}
}
};
});
The things to point out about this Tasks directive are:
restrict: 'E'
- Restrict is the way we define how we declare the Tasks directive in the DOM. It accepts 3 different
values and all the possible combinations of the 3 values.
<div tasks></div>
<tasks></tasks>
<div class="tasks"></div>
link: function()
- This is often a debate in Angular 1, should we use link or controller in our directive. I typically
follow the following rules.
It is now time to build the Angular 2 Tasks directive or should I say component.
<!-- app/app.html -->
<tasks></tasks>
The only thing to ask here is why are we putting the <tasks>
tags in app/app.html instead of index.html. The reason is in Angular 2
there is always root component and in this example I made the root component a component called App. There is no particular reason I
did this. I could have just as easily made Tasks the root component.
<!-- app/tasks.html -->
<input type="text" [(ngModel)]="newTask" placeholder="Enter Tasks" />
<button type="button" class="btn btn-primary" (click)="add()">Submit</button>
<ol>
<li *ngFor="let task of tasks; let index = index;">
<h3>{ { task }}</h3>
</li>
</ol>
For the most part the Angular 2 Tasks template probably doesn’t look much different than the Angular 1 version. So nothing to say here. Time to get to the real reason we are here. Update here is that since rc you now will use let instead of a template variable to help show the scope fo the task or index variables.
// app/task.ts
import {Component} from 'angular2/core';
import {CORE_DIRECTIVES} from 'angular2/common';
@Component({
selector: 'tasks',
templateUrl: 'app/tasks.html',
directives: [CORE_DIRECTIVES]
})
export class Tasks {
private _tasks:Array<string> = [];
private _newTask:string = '';
get tasks():Array<string> {
return this._tasks;
}
get newTask():string {
return this._newTask;
}
set newTask(newTask):void {
this._newTask = newTask;
}
add():void {
if(this.newTask.length > 0){
this._tasks.push(this.newTask);
this.newTask = '';
}
}
}
So what you ask is going on here.
@View
annotation and added the templateUrl to the @Component
metadata.directives: [CORE_DIRECTIVES]
? Every component that is needed in the view is now required to be added to the
directives
property array. This would be available in the @View
metadata as well if you went that route for your display._tasks
and _newTasks
variables from other classes using them outright. In Angular 2 the template
is considered part of the class and therefore has access to them.Now that we have the ability to add tasks it would be nice if we could add subtasks to each task to allow more detail to be added. Lets also add a checkbox to each subtask so we can mark when it is complete (really since we aren’t hitting a server this is more for visual appeal).
Lets add the following in Angular 1.
<!-- tasks.html -->
<div class="page-header" ng-transclude>
</div>
<input type="text" ng-model="newTask" placeholder="Enter Tasks"/>
<button type="button" class="btn btn-primary" ng-click="addTask()">Submit</button>
<ol>
<li ng-repeat="task in tasks track by $index" >
<h3>{ { task.name }}</h3>
<sub-tasks ></sub-tasks>
</li>
</ol>
<!-- sub-tasks.html -->
<input type="text" ng-model="newSubTask" placeholder="Enter Sub Tasks"/>
<button type="button" class="btn btn-primary" ng-click="addSubTask()">Submit</button>
<ul>
<li ng-repeat="subTask in subTasks track by $index">
<input type="checkbox">
{ { subTask }}
</li>
</ul>
//sub-tasks.js
angular.module('TaskApp')
.directive('subTasks', function() {
return {
restrict: 'E',
templateUrl: 'sub-tasks.html',
link: function(scope, element, attrs) {
scope.subTasks = [];
scope.newSubTask = '';
scope.addSubTask = function(){
if(scope.newSubTask !== ''){
scope.subTasks.push(scope.newSubTask);
scope.newSubTask = '';
}
}
}
};
});
There is really nothing of note here. We just created the Subtasks directive, template, and added <sub-tasks>
tags
to the tasks.html. Everything else should make sense.
In Angular 2 there is slightly more work to do.
<!-- app/tasks.html -->
<div class="page-header">
<ng-content></ng-content>
</div>
<input type="text" [(ngModel)]="newTask" placeholder="Enter Tasks" />
<button type="button" class="btn btn-primary" (click)="add()">Submit</button>
<ol>
<li *ngFor="let task of tasks; let index = index;">
<h3>{ { task.name }}</h3>
<sub-tasks ></sub-tasks>
</li>
</ol>
// app/tasks.ts
import {Component} from 'angular2/core';
import {CORE_DIRECTIVES} from 'angular2/common';
import {SubTasks} from './sub-tasks'
@Component({
selector: 'tasks',
templateUrl: 'app/tasks.html',
directives: [CORE_DIRECTIVES, SubTasks]
})
export class Tasks {
private _tasks:Array<string> = [];
private _newTask:string = '';
get tasks():Array<string> {
return this._tasks;
}
get newTask():string {
return this._newTask;
}
set newTask(newTask):void {
this._newTask = newTask;
}
add():void {
if(this._newTask.length > 0){
this._tasks.push(this.newTask);
this.newTask = '';
}
}
}
We have to import the SubTasks component into app/tasks.ts so that Tasks component knows about it. Along with that we had to add SubTasks as a directive that will be used in the components view.
// app/sub-tasks.ts
import {Component} from 'angular2/core';
import {CORE_DIRECTIVES} from 'angular2/common';
@Component({
selector: 'sub-tasks',
templateUrl: 'app/sub-tasks.html',
directives: [CORE_DIRECTIVES],
})
export class SubTasks {
private subTasks:Array<string> = [];
add(newSubTask):void {
if(newSubTask.length > 0){
this.subTasks.push(newSubTask);
}
}
}
This probably looks pretty similar to the Tasks component. The only difference is we are passing in the newSubTask to the add method instead of using a variable already defined on SubTasks. Lets see why.
<!-- app/sub-tasks.html -->
<input type="text" #newSubTask placeholder="Enter Sub Tasks"/>
<button type="button" class="btn btn-primary" (click)="add(newSubTask.value); newSubTask.value=''">Submit</button>
<ul>
<li *ngFor="let subTask of subTasks; let index = index;">
<input type="checkbox">
{ { subTask.name }}
</li>
</ul>
The reasons is because in Angular 2 we can define template variables. You do this by adding a #variablename attribute to the element. As you can see we created a newSubTask template variable that holds the input value for the input field. We then pass this to the add function and reset it on the click event. We no longer have the need to create class variables just for passing data from the template to the Directive/Component.
Now that we can create tasks and subtasks lets take it one more step. Lets remove the subtasks that are complete and if a tasks no longer has any active subtasks lets remove them as well.
In Angular 1 lets start by setting up the Tasks directive:
<!-- tasks.html -->
<div class="page-header" ng-transclude>
</div>
<input type="text" ng-model="newTask" placeholder="Enter Tasks"/>
<button type="button" class="btn btn-primary" ng-click="addTask()">Submit</button>
<ol>
<li ng-repeat="task in tasks track by $index" ng-show="task.show">
<h3>{ { task.name }}</h3>
<sub-tasks task-index="$index"></sub-tasks>
</li>
</ol>
First we are going to have to send the Subtasks directive the index of the task that they belong to. This is done by passing the index as we loop to the attribute task-index.
//tasks.js
angular.module('TaskApp')
.directive('tasks', function() {
return {
restrict: 'E',
transclude: true,
templateUrl: 'tasks.html',
controller: function($scope){
this.closeOut = function(index){
if(index !== -1){
$scope.tasks[index].show = false;
}
}
},
link: function(scope, element, attrs, ctrl){
scope.tasks = [];
scope.newTask = '';
scope.addTask = function(){
if(scope.newTask !== ''){
scope.tasks.push({name: scope.newTask, show: true});
scope.newTask = '';
}
}
}
};
});
Second we are going to have to add a controller to the Tasks directive. We need this as mentioned earlier because another directive will need to talk to it, that directive being the Subtasks directive. In the controller we created a closeOut function that will be called by the Subtasks directive with the index of the finished task.
Now lets look at Subtasks.
<script src="https://code.angularjs.org/2.0.0-beta.7/router.js"></script>
All we did here was add a ng-click event to call removeSubTasks with the index of the Subtask that needs to be removed.
//sub-tasks.js
angular.module('TaskApp')
.directive('subTasks', function() {
return {
restrict: 'E',
templateUrl: 'sub-tasks.html',
scope: {
taskIndex: '='
},
require: '^tasks',
link: function(scope, element, attrs, ctrl) {
scope.subTasks = [];
scope.newSubTask = '';
scope.addSubTask = function(){
if(scope.newSubTask !== ''){
scope.subTasks.push(scope.newSubTask);
scope.newSubTask = '';
}
}
scope.removeSubTask = function(index){
if(index !== -1){
scope.subTasks.splice(index, 1);
}
if(scope.subTasks.length === 0){
ctrl.closeOut(scope.taskIndex);
}
}
}
};
});
What to notice here are:
=
- This is a two way binding between both the parent and child directive. So when the value changes in one directive
it changes in both. A ng-modle most always be passed in these cases.&
- This is used to bind a parent directive’s method to the child directive.@
- This is a one way binding between the parent directive and child directive. When the parent directive changes
it changes the child directive but not visa versa. An expression must be passed in these cases.require: '^tasks'
property to the directive. This forces the directive to be a child of the Tasks directive. It
also gives access in the link function to the Tasks controller.We now have a working Angular 1 Task and Subtask tracker.
So in Angular 2 and the power of Typescript lets build a new class called Task. In this class we want to hold the Task name and whether it is active or not.
// app/task.ts
export class Task {
private _active:boolean;
constructor(private _name:string){
this._active = true;
}
get name():string{
return this._name;
}
get active():string{
return this._active;
}
deactive():void{
this._active = false;
}
}
You might ask why we have an _active:boolean
defined but not one for _name
. Well in Typescript we can create public
and private class variables in the constructor which is what we did with _name
. You also might notice we have export
in front of class, this is so other components or classes can use this Task class.
Now lets update the Tasks component.
// app/tasks.ts
import {Component} from 'angular2/core';
import {CORE_DIRECTIVES} from 'angular2/common';
import {Task} from './task';
import {SubTasks} from './sub-tasks'
@Component({
selector: 'tasks',
templateUrl: 'app/tasks.html',
directives: [CORE_DIRECTIVES, SubTasks]
})
export class Tasks {
private _tasks:Array<Task> = [];
private _newTask:string = '';
get tasks():Array<Task> {
return this._tasks;
}
get newTask():string {
return this._newTask;
}
set newTask(newTask):void {
this._newTask = newTask;
}
add():void {
if(this._newTask.length > 0){
this._tasks.push(new Task(this.newTask));
this.newTask = '';
}
}
remove(index):void {
this._tasks[index].deactive();
}
}
Here we have imported the new Task class. We have also updated all the _tasks to use the new Task class. We have an
Array of Task now and we create new Task(this.newTask)
objects when we add a task. The other thing we added was
the remove
method. This method calls the deactive()
method on the Task class to set the active attribute to false
as shown above for the tasks index that was passed into the remove
method.
<!-- app/tasks.html -->
<div class="page-header">
<ng-content></ng-content>
</div>
<input type="text" [(ngModel)]="newTask" placeholder="Enter Tasks" />
<button type="button" class="btn btn-primary" (click)="add()">Submit</button>
<ol>
<li *ngFor="let task of tasks; let index = index;" [hidden]="!task.active">
<h3>{ { task.name }}</h3>
<sub-tasks [taskIndex]="index"></sub-tasks>
</li>
</ol>
In the tasks.html we have added a [hidden] attribute that is part of the CORE_DIRECTIVES which allows us to define wether or not to show a element. It replaces Angular 1’s ng-show and ng-hide. The other thing we did was define a new attribute on sub-tasks called [taskIndex] in which we pass the index of the task the subtask is associated with.
Now lets see how the subtask directive changed.
<!-- app/sub-tasks.html -->
<input type="text" #newSubTask placeholder="Enter Sub Tasks"/>
<button type="button" class="btn btn-primary" (click)="add(newSubTask.value); newSubTask.value=''">Submit</button>
<ul>
<li *ngFor="let subTask of subTasks; let index = index;">
<input type="checkbox" (click)="remove(index)">
{ { subTask.name }}
</li>
</ul>
Here all we did was add a (click) event to call the subtask directive’s remove
function with the index of the subtask
to remove.
// app/sub-tasks.ts
import {Component, Inject, forwardRef} from 'angular2/core';
import {CORE_DIRECTIVES} from 'angular2/common';
import {Tasks} from './tasks';
import {Task} from './task';
@Component({
selector: 'sub-tasks',
templateUrl: 'app/sub-tasks.html',
directives: [CORE_DIRECTIVES],
inputs: ['taskIndex']
})
export class SubTasks {
private subTasks:Array<Task> = [];
private _tasks:Tasks;
constructor(@Inject(forwardRef(() => Tasks)) tasks) {
this._tasks = tasks;
}
add(newSubTask):void {
if(newSubTask.length > 0){
this.subTasks.push(new Task(newSubTask));
}
}
remove(index:number):void{
if(index !== -1){
this.subTasks.splice(index, 1);
}
if(this.subTasks.length === 0){
this._tasks.remove(this.taskIndex);
}
}
}
Yes we changed quite a decent amount here.
_tasks
to use the new Task class.require
to say you want to use another directive. Instead you can do what makes sense. You can inject
the component or directive you need straight into the constructor. You might ask why I had to do @Inject(forwardRef(() => Tasks)) tasks)
?
Well this particular injection is special. That is because the Tasks component also imports the SubTasks component.
So since there is a circular dependency we use the @Inject
and forwardRef
component to allow us to force
SubTasks not to be created until there is a Tasks component instantiated.There are several ways you can still approve apon the given code using just Typescript. You can extend the Tasks component on the Subtasks component class since a Subtask really is a Task. There are several other things you could do as well. However for this post I just wanted to cover the differences between Angular 1 and 2 in regards to the Directive.
I hope this post has helped you learn. As you can see just from the perspective of writing a Directive, a ton has changed from Angular 1 to 2. If you wish to look at or play around with the final product of the TaskApp in Angular 1 and Angular2 you can view the plunkers below: The only thing that is different in the Angular 2 Plunker from what you see here is it is using beta and not using let in the *ngFor let scoping