Angular Tips

Join us in our way to learning Angular.js

Migrating Directives to Angular 2

| Comments

Article updated on Feb 5th for Angular 2 beta 3.

Angular 2 it is just around the corner and people are still afraid because Angular 2 changes too much and it will hard to migrate. That is not true at all, in fact you will see how easy is Angular 2 by highlighting the semantic shift between those two.

Let’s migrate a directive from Angular 1 to Angular 2. I love accordions, everybody love accordions! Let’s migrate ui-bootstrap accordion to Angular 2. I can’t assume that you’re familiar with it, so we are code both at the same time, explaining the differences along the way. I highly recommend you to, at least, replicate yourself the Angular 2 one.

Use this plunker to code the Angular 1 version.

Use this plunker to code the Angular 2 version.

Designing our accordion

Our accordion will be composed of 2 directives. The first one will be an element called <accordion> which will host one or many <accordion-group>. Each <accordion-group> will contain a heading which is the clickable area that will allow us to toggle each group. The content that we put inside the <accordion-group>, will be its content. So without messing too much with the syntax now, the idea is to have something like:

1
2
3
4
5
6
7
8
<accordion>
  <accordion-group heading="First one">
    Lot of content in here.
  </accordion-group>
  <accordion-group heading="Another group">
    More interesting stuff.
  </accordion-group>
</accordion>

The <accordion> will contain an option to force other groups to close (to just leave one open) and each <accordion-group> can be toggle by code. Sounds like a plan.

Coding the accordion directive in Angular 1

First we need to code the <accordion> directive that will wrap all our groups. Let’s code the basic skeleton:

accordion.js
1
2
3
4
5
6
7
angular.module('app')

  .directive('accordion', function() {
    return {
      templateUrl: 'accordion.html'
    };
  });

The objective of the <accordion> directive is to be a “wrapper” of groups, so for now we just need a template to put those groups. What does bootstrap say about this template? It needs to be a <div> with the panel-group class. Something like:

1
<div class="panel-group"></div>

The problem is that we need to grab all those <accordion-group> elements and move them inside our accordion’s template. How do we do that? Transclusion. That means that we need to set a tranclusion point in that template:

accordion.html
1
<div class="panel-group" ng-transclude></div>

Good, since we are using transclusion, we need to activate it on the directive:

accordion.js
1
2
3
4
5
6
7
8
angular.module('app')

  .directive('accordion', function() {
    return {
      transclude: true,
      templateUrl: 'accordion.html'
    };
  });

Perfect! Let’s use it:

index.html
1
2
3
4
5
<body ng-controller="MainCtrl">
  <accordion>

  </accordion>
</body>

If we execute it now, we just get an empty <div>.

Coding the accordion directive in Angular 2

We are used to code directives for everything. In Angular a directive is something we add to our HTML, it doesn’t matter if it is an element that generates some content (like an accordion or an alert box) or if it is an attribute to add / modify some behavior (like a validation directive, ng-model, etc).

In Angular 2 we have several types of directives, the most common one is the Component directive which is type of directive that has a template. Here we don’t have a .directive function like in Angular 1, instead we have simple classes that gets annotated to give them a certain behavior. Let’s import the annotations we need for a Component:

accordion.ts
1
import {Component} from 'angular2/core';

For a Component, we just need the Component annotation. Then we create our Component class:

accordion.ts
1
export class Accordion {}

Nothing fancy, we create (and export) a class named Accordion. As I said before, this is not a component yet, so we need to annotate it with:

accordion.ts
1
2
3
4
@Component({

})
export class Accordion {}

Now our Accordion class is a Component. We need to customize the annotation using properties. One of them is selector which will define how can we use the Component in our HTML. Some options are:

  • foo: that will restrict for an element.
  • [foo]: that will restrict for an attribute.
  • .class: that will restrict for a class.
  • input[type=text]: that will apply this directive only in a <input type="text">.

This serves the same purpose as the restrict option in Angular 1, but here we have more flexibility. We can not only restrict it by element or attributes like we used to, but also restrict it to certain types of elements or give a different name depending in where we use it. There are more options apart from those 4, but that is outside the scope of the article.

Ok, so we want to restrict it to elements:

accordion.ts
1
2
3
4
@Component({
  selector: 'accordion'
})
export class Accordion {}

And now we define its component:

accordion.ts
1
2
3
4
5
6
7
import {Component} from 'angular2/core';

@Component({
  selector: 'accordion',
  templateUrl: 'src/accordion.html'
})
export class Accordion {}

We can both have our template inline (with template) or in a external file (with templateUrl).

All we need now is to code write our template to be able to transclude our stuff. Wait, transclude?

“Transclusion” in Angular 2

First thing first, go to all your dictionaries and delete the “Transclusion” entry that you added long time ago as there is no more transclusion in Angular 2. Web Components have a feature called Shadow DOM (you can learn more about that here). One of the thing we can do with the it is be able to “project” (transclude) the content we need from our Component element into its template with <content>. That means that when in the past we had to use ng-transclude, we use <content> now:

1
2
3
<div class="panel-group">
  <content></content>
</div>

Now if we do something like:

1
<accordion>Foo</accordion>

The <content> element in the template will be replaced with Foo. The directives in Angular 2 are not Web Components, they just work like Web Components. So for our Components we have a our own version of <content> called <ng-content>.

The good thing about <content>/<ng-content> is that in contrast to ng-transclude (this has been fixed in angular recently), you can have several of them in one template. For example:

1
2
<ng-content select=".foo"></ng-content>
<ng-content select="[foo]"></ng-content>

There it will project all the elements with the foo class into the first ng-content and the elements with the foo directive. That gives much much flexibility versus ng-transclude. No more dummy directives to create extra transclusion points!

Coding the accordion template

Now that we have more knowledge about <ng-content>, we can write our template:

accordion.html
1
2
3
<div class="panel-group">
  <ng-content></ng-content>
</div>

Thanks to this, our groups will be projected into that <ng-content>.

Something important in here is that we cannot add attributes like classes to the <ng-content> and that is because it will be replaced. No point to add stuff to something that will disappear, right? Because of that, we had to create a div wrapper because we really need that panel-group class.

The problem with this is that we are going to end with:

1
2
3
4
5
<accordion class="ng-binding">
  <div class="panel-group">
    ...
  </div>
</accordion>

Would be nice if we could apply that class directly to the host element (the accordion one). The thing is, we can! The Component annotation allows us to modify that host element, so we can tell it to add a panel-group class to it like:

accordion.ts
1
2
3
4
5
6
7
@Component({
  selector: 'accordion',
  templateUrl: 'src/accordion.html',
  host: {
    'class': 'panel-group'
  }
})

Then we can remove the div wrapper from the template:

accordion.html
1
<ng-content></ng-content>

And now we get:

1
2
3
<accordion class="ng-binding panel-group">
  ...
</accordion>

Fantastic!

To try it, we modify our application html to use the accordion:

app.html
1
2
3
<accordion>

</accordion>

It won’t work yet because our application is not aware of the accordion directive yet. To fix that, we need first to import it:

app.ts
1
import {Accordion} from './accordion';

That alone won’t do the job, because now in Angular 2, we need to specify which directives are we using in our component. We can do that thanks to the directives property of the Component annotation:

app.ts
1
2
3
4
5
@Component({
  selector: 'my-app',
  templateUrl: 'src/app.html',
  directives: [Accordion]
})

It is more manual than Angular 1, but this gives us much more flexibility and we won’t have more directives’ name conflicts.

Coding the accordion-group directive in Angular 1

Now we need the directive that we are going to transclude into that parent accordion. Let’s create it:

accordion.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.directive('accordionGroup', function() {
  return {
    transclude: true,
    templateUrl: 'accordion-group.html',
    scope: {
      heading: '@'
    },
    link: function(scope, element, attrs) {
      scope.toggleOpen = function() {
        scope.isOpen = !scope.isOpen;
      }
    }
  };
});

Our groups needs a heading string and also we need to transclude the group content into the template. Also, we need a function that will toggle our isOpen variable. For the template:

accordion-group.html
1
2
3
4
5
6
7
8
9
10
<div class="panel panel-default">
  <div class="panel-heading" ng-click="toggleOpen()">
    <h4 class="panel-title">
      <a href tabindex="0" class="accordion-toggle"><span>{{heading}}</span></a>
    </h4>
  </div>
  <div class="panel-collapse" ng-show="isOpen">
    <div class="panel-body" ng-transclude></div>
  </div>
</div>

The template is pretty simple. It follows bootstrap conventions for the accordion. The ui-bootstrap version uses the collapse directive to hide the content, but a simple ng-show works for us here.

If we try it now with:

index.html
1
2
3
4
5
6
7
8
<accordion>
  <accordion-group heading="First one">
    Lot of content in here.
  </accordion-group>
  <accordion-group heading="Another group">
    More interesting stuff.
  </accordion-group>
</accordion>

Yikes, we have a fully operational accordion!

One thing before we move forward. Imagine that we don’t write proper documentation and we see that heading attribute. What does it receive? A string? a scope property? I have no idea. Perhaps I want to send the content of $scope.foo so I try:

1
<accordion-group heading="foo"></accordion-group>

Just to realize that this failed miserably. So I try:

1
<accordion-group heading="{{foo}}"></accordion-group>

And now I see it working. Not really clear.

Coding the accordion-group directive in Angular 2

We won’t have much problem migrating this to Angular 2. Let’s see it:

accordion.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {Component, Input} from 'angular2/core';

...

@Component({
  selector: 'accordion-group',
  templateUrl: 'src/accordion-group.html'
})
export class AccordionGroup {
  isOpen = false;

  @Input() heading: string;

  toggleOpen(event) {
    event.preventDefault();
    this.isOpen = !this.isOpen;
  }
}

In this case, to pass the heading input we had in the Angular 1 version, we have a new annotation called Input. Thanks to it, we can annotate instance variables to contain the value of those attributes.

Back in the Angular 1 version, we had to create a link function to add the toggleOpen function. In Angular 2 we just need to create a method in our class.

The template is pretty much the same:

accordion-group.html
1
2
3
4
5
6
7
8
9
10
11
12
<div class="panel panel-default" [ngClass]="{'panel-open': isOpen}">
  <div class="panel-heading" (click)="toggleOpen($event)">
    <h4 class="panel-title">
      <a href tabindex="0"><span>{{heading}}</span></a>
    </h4>
  </div>
  <div class="panel-collapse" [hidden]="!isOpen">
    <div class="panel-body">
      <ng-content></ng-content>
    </div>
  </div>
</div>

Here we have a ngClass (more on the syntax in a bit), a click event for the toggle, a hidden property (which substitutes the ng-show we had) and again, an <ng-content> to project our group’s content.

We can now use it in our app. First, we need to import the new component:

app.ts
1
import {Accordion, AccordionGroup} from './accordion';

And tell our View annotation that we are going to use it:

app.ts
1
2
3
4
@View({
  templateUrl: 'src/app.html',
  directives: [Accordion, AccordionGroup]
})

Finally, we just need to use it:

app.html
1
2
3
4
5
6
7
8
<accordion>
  <accordion-group heading="First one">
    Lot of content in here.
  </accordion-group>
  <accordion-group heading="Another group">
    More interesting stuff.
  </accordion-group>
</accordion>

If we run it, we get the exact same behavior.

But now, we don’t need to ask ourselves what does that heading receive. If we use it like in the previous snippet, it will receive a string for sure. If we want to pass some variable from our application, we can simply do:

1
<accordion-group [heading]="foo"></accordion-group>

We get both behavior now without any extra code. That means, no more asking ourselves how to use the directive anymore! To learn more about the [] syntax used here and before with ngClass and hidden, please read the “Properties” section in my previous article.

Opening groups dynamically in Angular 1

We want an is-open attribute for the groups so we can open or close them via code. First, we need to ask ourselves: What is the most common way of using an attribute like that? Uhm, we would like to be able to pass a boolean to it and also a variable from the scope. That means that we need to use = because @ would make our ng-show to be open with any string we pass to it.

With those insights, we just need to add a new property to the scope:

accordion.js
1
2
3
4
scope: {
  heading: '@',
  isOpen: '='
}

Just that. We can now do:

index.html
1
2
3
4
5
6
7
8
<accordion>
  <accordion-group heading="First one" is-open="isOpen">
    Lot of content in here.
  </accordion-group>
  <accordion-group heading="Another group">
    More interesting stuff.
  </accordion-group>
</accordion>

Now if we set a isScope variable in our controller:

app.js
1
2
3
.controller('MainCtrl', function($scope) {
  $scope.isOpen = true;
});

That will make our first group to open at startup.

The issue with this again is the syntax is that we need to decide beforehand how do we want that attribute to be used so we can code it. The problem is that end users could have different ideas and use it wrong. Not their fault tho.

Opening groups dynamically in Angular 2

In Angular 2, we don’t need to make any question. You want a isOpen attribute? Decorate our isOpen with Input:

accordion.ts
1
2
3
4
5
6
7
8
9
export class AccordionGroup {
  @Input() isOpen = false;
  @Input() heading: string;

  toggleOpen(event) {
    event.preventDefault();
    this.isOpen = !this.isOpen;
  }
}

Now the value of isOpen can be set with an attribute but will be initialized to false by default.

Now we can do:

app.html
1
2
3
4
5
6
7
8
<accordion>
  <accordion-group heading="First one" isOpen="true">
    Lot of content in here.
  </accordion-group>
  <accordion-group heading="Another group">
    More interesting stuff.
  </accordion-group>
</accordion>

You say you would like to send a property from your component? Sure:

1
<accordion-group heading="First one" [isOpen]="isOpen"></accordion-group>

Now it will look for a isOpen attribute on the MyApp component:

app.ts
1
2
3
export class MyApp {
  isOpen = true;
}

Much easier, isn’t it?

Worth mentioning that even when this Input annotation seems to be analogous on how we do two way binding in angular, changing isScope by toggling, won’t change the parent’s model, in other words, MyApp’s isOpen won’t change.

Closing other groups in Angular 1

The most common behavior on an accordion is to close the other groups when we open one. Ok, how can we code that? A group doesn’t see another group, but the accordion itself has all children within. Can we use that? Sure.

This is a two parts idea. First we need to be able to somehow register all our groups within the accordion. In Angular, we can require a parent directive. By doing that, we are actually getting access to that directive’s controller. Alright, let’s create a controller for the accordion that is able to register groups:

accordion.js
1
2
3
4
5
6
7
8
9
10
11
12
.directive('accordion', function() {
  return {
    ...
    controller: function() {
      var groups = [];

      this.addGroup = function(groupScope) {
        groups.push(groupScope);
      }
    }
  }
}

Ideally this controller would be “external” but this works for the demo. We just need a groups array and a method to add groups. We just need a reference to their scope to make this work.

Now the groups needs to call this method on startup:

accordion.js
1
2
3
4
5
6
7
8
9
10
11
12
13
.directive('accordionGroup', function() {
  return {
    ...
    require: '^accordion',
    link: function(scope, element, attrs, ctrl) {
      ctrl.addGroup(scope);

      scope.toggleOpen = function() {
        scope.isOpen = !scope.isOpen;
      }
    }
  };
});

Noticed that we had to require our accordion directive in order to be able to access its controller. Now, we need a method on that controller that will close all groups except the one being opened:

accordion.js
1
2
3
4
5
6
7
8
9
10
11
controller: function() {
  ...

  this.closeOthers = function(openGroup) {
    angular.forEach(groups, function(group) {
      if (group !== openGroup) {
        group.isOpen = false;
      }
    });
  }
}

Simple loop that will set the isOpen attribute on the groups to false. Now, we simply need to call that method when we click on a header or when isOpen changes by other means. That is fixed with a $watch in the group’s link function:

accordion.js
1
2
3
4
5
scope.$watch('isOpen', function(value) {
  if (value) {
    ctrl.closeOthers(scope);
  }
});

Now that function will be called when isOpen changes and it will close the other groups successfully.

Closing other groups in Angular 2

In Angular this is done a bit simpler. We need to be able to register groups as well, but in this case we don’t have controllers, so the code goes inside the Accordion class:

accordion.ts
1
2
3
4
5
6
7
export class Accordion {
  groups: Array<AccordionGroup> = [];

  addGroup(group: AccordionGroup): void {
    this.groups.push(group);
  }
}

First we create our groups array and thanks to TypeScript typing, we get real intellisense on our editors (not plunker tho). Nothing fancy in here.

Then we need to register our groups in it. How? We used to require the accordion to give us access to its controller. Now what? You can inject the parent accordion now:

accordion.ts
1
2
3
4
5
6
7
8
9
export class AccordionGroup {
  ...

  constructor(private accordion: Accordion) {
    this.accordion.addGroup(this);
  }

  ...
}

By injecting it, we get access to it so we just need to call its addGroup method.

For the close others, another method on the Accordion class:

accordion.ts
1
2
3
4
5
6
7
8
9
10
11
export class Accordion {
  ...

  closeOthers(openGroup: AccordionGroup): void {
    this.groups.forEach((group: AccordionGroup) => {
      if (group !== openGroup) {
        group.isOpen = false;
      }
    });
  }
}

This time we don’t have $watch anymore, so what we need to do is to rely on standard getters and setters (yay!):

accordion.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export class AccordionGroup {
  private _isOpen: = false;

  @Input() heading: string;

  @Input()
  set isOpen(value: boolean) {
    this._isOpen = value;
    if (value) {
      this.accordion.closeOthers(this);
    }
  }

  get isOpen() {
    return this._isOpen;
  }

  constructor(private accordion: Accordion) {
    this.accordion.addGroup(this);
  }

  toggleOpen(event: MouseEvent): void {
    event.preventDefault();
    this.isOpen = !this.isOpen;
  }
}

So here we changed our isOpen input to be a setter. That way we can run some code every time our input changes (like a $watch). So when we toggle isOpen it will call closeOthers. As a good convention, every time you add a setter, add a getter as well (unless the value is meant to be readonly). Also since the setter / getter is the public interface, we can mark _isOpen as private.

Works like a charm. The good part in here is that we don’t have anymore those link vs controller wars. When to use link, when to use controller. We just have our class and nothing else.

Removing groups in Angular 1

Our last feature (or I will need to turn this monster into a book). Being able to remove groups. Why do we need that? Imagine we have an array of groups and a function to delete groups:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$scope.groups = [
  {
    heading: 'Dynamic 1',
    content: 'This is dynamic'
  },
  {
    heading: 'Dynamic 2',
    content: 'This is also dynamic'
  }
];

$scope.closeDynamic = function() {
  $scope.groups.pop();
};

Now we decide we want to create dynamic groups like:

index.html
1
2
3
4
5
6
7
8
9
10
11
<accordion>
  <accordion-group heading="First one" is-open="isOpen">
    Lot of content in here.
  </accordion-group>
  <accordion-group heading="{{group.heading}}" ng-repeat="group in groups">
    {{group.content}}
  </accordion-group>
  <accordion-group heading="Another group">
    More interesting stuff.
  </accordion-group>
</accordion>

If we start calling that closeDynamic function, we will see the group disappearing as expected, but our accordion will still have a reference to a dead scope. That could cause some leaks.

To fix that, we just need to listen to the $destroy event that every scope fires when it gets killed and with that, we just remove that group from the list:

accordion.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
controller: function() {
  ...

  this.addGroup = function(groupScope) {
    groups.push(groupScope);

    groupScope.$on('$destroy', function() {
      removeGroup(groupScope);
    })
  }

  var removeGroup = function(group) {
    var index = groups.indexOf(group);
    if (index !== -1) {
      groups.splice(index, 1);
    }
  }
}

That is easily done. We listen to it, we remove it.

Removing groups in Angular 2

In Angular 2, we need to take a different approach. Angular 2 directives have lifecycle hooks, so we can do stuff OnInit, OnDestroy, etc.

First, let’s code the removeGroup method on the Accordion class:

accordion.ts
1
2
3
4
5
6
7
8
9
10
export class Accordion {
  ...

  removeGroup(group: AccordionGroup): void {
    const index = this.groups.indexOf(group);
    if (index !== -1) {
      this.groups.splice(index, 1);
    }
  }
}

Now we need to call that OnDestroy. To do that, first we import the OnDestroy interface:

accordion.ts
1
import {Component, Input, OnDestroy} from 'angular2/core';

And then we implement the ngOnDestroy method:

accordion.ts
1
2
3
ngOnDestroy() {
  this.accordion.removeGroup(this);
}

To see this in action, let’s modify our MyApp component to have a list of dynamic groups:

app.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {Component} from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';

import {Accordion, AccordionGroup} from './accordion';

@Component({
  selector: 'my-app',
  templateUrl: 'src/app.html',
  directives: [Accordion, AccordionGroup]
})
export class MyApp {
  isOpen = false;

  groups: Array<any> = [
    {
      heading: 'Dynamic 1',
      content: 'I am dynamic!'
    },
    {
      heading: 'Dynamic 2',
      content: 'Dynamic as well'
    }
  ];

  removeDynamic() {
    this.groups.pop();
  }
}

bootstrap(MyApp);

And its html:

app.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<p>
  <button type="button" class="btn btn-default" (click)="removeDynamic()">
    Remove last dynamic
  </button>
</p>

<accordion>
  <accordion-group heading="This is the header" isOpen="true">
    This is the content
  </accordion-group>
  <accordion-group [heading]="group.heading" *ngFor="#group of groups">
    {{group.content}}
  </accordion-group>
  <accordion-group heading="Another group" [isOpen]="isOpen">
    More content
  </accordion-group>
</accordion>

Notice that nice ngFor in there to generate multiple groups. The syntax is a bit different from what we used in here, but that is a topic for another article ;)

Conclusions

We have seen that migrating an Angular directive to Angular 2 is not that problematic. We just need to learn a couple of new things, but at the end, we can see how Angular 2 simplifies lot of stuff:

  • No more Controllers or Link functions.
  • Even when the new html syntax is weird at first, makes our directive really straightforward to use.
  • No more =, @ and &.
  • Using a parent directive is as easy as injecting it.
  • Lot of lifecycle hooks to customize easily our directives.
  • Much more variety of selectors types.
  • No need to worry on whether to create a directive with isolated scope or not.
  • “Transclusion” makes much much sense now.

And we only covered the tip of the iceberg. There are lots and lots of new cool things, like dynamic loading a directive.

On the other hand, TypeScript, even when it adds a bit of verbosity to our code, it really shines when we use it with a nice editor.

Check the end result of the ng1 version.

Check the end result of the ng2 version.

I also wrote an ng2 version with ES5 (using angular 2 alpha). You can see what a nice job the team did to make the syntax as close as possible to what we can achieve with ES6 or TypeScript.

Comments