Developing a zippy component in Angular
We are following the development of Angular 2.0.0 since the beginning on and are also contributing to the project. Just recently we’ve built a simple zippy component in Angular and in this article we want to show you how.
Getting started with Angular 2.0.0
There are several options today to get started with Angular. For instance, we can go to angular.io and use the quickstart guide. Or, we can install the Angular CLI, which takes care of scaffolding, building and serving Angular applications. In this article we will use Pawel Kozlowski’s ng2-play repository the Angular CLI, but again, you can use whatever suits you.
We start by installing Angular CLI as a global command on our local machine using npm.
$ npm install -g angular-cli
Once that is done, we can scaffold a new Angular project by running ng new <PROJECT_NAME>
. Note that the project is scaffolded in the directory where we’re in at this moment.
$ ng new zippy-app
Next, we navigate into the project and run ng serve
, which will essentially build and serve a hello world app on http://localhost:4200
.
$ cd zippy-app
$ ng serve
We open a browser tab on localhost://4200
and what we see is the text “zippy-app works!“. Cool, we’re all set up to build a zippy component in Angular!
Building the zippy component
Before we start building the zippy component with Angular, we need to clarify what we’re talking about when using the term “zippy”. It turns out that a lot of people think they don’t know what a zippy is, even if they do, just because of the naming.
Also known as “accordion”. You can click the summary text and the actual content toggles accordingly. If you take a look at this particular plunk, you’ll see that we actually don’t need to do any special implementation to get this working. We have the <details>
element that does the job for us. But how can we implement such a thing in Angular?
We start off by adding a new file src/app/my-zippy.component.ts
and creating a class in ES2015 that we export, so it can be imported by other consumers of this class, by using the ES2015 module system. If you’re not familiar with modules in ES2015 you might want to read our article on using ES2015 with Angular today.
Special Tip: We would normaly use Angular CLI to generate a component for us, instead of creating the files manually, but this articles focuses on understanding the building blocks of creating a custom component.
export class ZippyComponent {
}
The next thing we want to do, is to make our ZippyComponent
class an actual component and give it a template so that we can see that it is ready to be used. In order to tell Angular that this particular class is a component, we use something called “Decorators”.
Decorators are a way to add metadata to our existing code. Those decorators are actually not supported by ES2015 but have been developed as language extension of the TypeScript transpiler, which is used in this project. We’re not required to use decorators though. As mentioned, those are just transpiled to ES5 and then simply used by the framework. However, for simplicity sake we’ll use them in this article.
Angular provides us with a couple of decorators so we can express our code in a much more elegant way. In order to build a component, we need the @Component()
decorator. Decorators can be imported just like classes or other symbols, by using ES2015 module syntax. If you heard about annotations in traceur before and wonder how they relate to decorators, you might want to read our article on the difference between annotations and decorators.
import { Component } from '@angular/core';
export class ZippyComponent {
}
The Component
decorator adds information about what our component’s element name will be, what input properties it has and more. We can also add information about the component’s view and template.
We want our zippy component to be usable as <my-zippy>
element. So all we need to do, is to add a @Component()
decorator with that particular information. To specify the element name, or rather CSS selector, we need to add a selector
property that matches a CSS selector.
import { Component } from '@angular/core';
@Component({
selector: 'my-zippy'
})
export class ZippyComponent {
}
Next, our component needs a template. We add information about the component’s view. templateUrl
tells Angular where to load the component template from. To make templateUrl
work with relative paths, we add another property moduleId
with a value module.id
. To get more information on moduleId
, make sure to check out our article on Component-Relative Paths in Angular
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'my-zippy',
templateUrl: 'my-zippy.component.html'
})
export class ZippyComponent {
}
Later at runtime, when Angular compiles this component, it’ll fetch my-zippy.component.html
asynchronously. Let’s create a file src/app/my-zippy.component.html
with the following contents:
<div class="zippy">
<div class="zippy__title">
▾ Details
</div>
<div class="zippy__content">
This is some content.
</div>
</div>
CSS classes can be ignored for now. They just give us some semantics throughout our template.
Alright, believe it or not, that’s basically all we need to do to create a component. Let’s use our zippy component inside the application. In order to do that, we need to do things:
- Add our new component to the application module
- Use
ZippyComponent
inZippyAppComponent
’s template
Angular comes with a module system that allows us to register directives, components, service and many other things in a single place, so we can use them throughout our application. If we take a look at the src/app/app.module.ts
file, we see that Angular CLI already created a module for us. To register ZippyComponent
on AppModule
, we import it and add it to the list AppModule
’s declarations:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ZippyAppComponent } from './zippy-app.component';
import { ZippyComponent } from './my-zippy.component';
@NgModule({
imports: [BrowserModule],
declarations: [ZippyAppComponent, ZippyComponent], // we're adding ZippyComponent here
bootstrap: [ZippyAppComponent]
})
export class AppModule {}
We don’t worry too much about the imports
for now, but we acknowledge that Angular needs BrowserModule
to make our app run in the browser. The declarations
property defines all directives and pipes that are used in this module and bootstrap
tells Angular, which component should be bootstrapped to run the application. ZippyAppComponent
is our root component and has been generated by Angular CLI as well, ZippyComponent
is our own custom component that we’ve just created.
Now, to actually render our zippy component in our application, we need to use it in ZippyAppComponent
’s template. Let’s do that right away:
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'zippy-app',
template: '<my-zippy></my-zippy>'
})
export class ZippyAppComponent {
}
Nice! Running this in the browser gives us at least something that looks like a zippy component. The next step is to bring our component to life.
Bringing the component to life
In order to bring this component to life, let’s recap quickly what we need:
- Clicking on the zippy title should toggle the content
- The title of the should be configurable from the outside world, currently hard-coded in the template
- DOM that is used inside the
<my-zippy>
element should be projected in the zippy content
Let’s start with the first one: when clicking on the zippy title, the content should toggle. How do we implement that in Angular?
We know, in Angular 1.x, we’d probably add an ngClick
directive to the title and set a scope property to true
or false
and toggle the zippy content respectively by using either ngHide
or ngShow
. We can do pretty much the same in Angular >= 2.x as well, just that we have a bit different semantics.
Instead of adding an ngClick
directive (which we don’t have in Angular 2.x), to call for instance a method toggle()
, we bind to the click
event directly using the following template syntax.
...
<div class="zippy__title" (click)="toggle()">
▾ Details
</div>
...
If you’re not familiar with this syntax I recommend you either reading this article on integrating Web Components with Angular, or this article about Angular’s template syntax demystified. Misko’s keynote from this year’s ng-conf is also a nice resource.
Now we’re basically listening on a click
event and execute a statement. But where does toggle()
come from? We can access component methods directly in our template. There’s no $scope
service or controller that provides those methods. Which means, toggle()
is just a method defined in ZippyComponent
.
Here’s what the implementation of this method could look like:
export class ZippyComponent {
toggle() {
this.visible = !this.visible;
}
}
We simply invert the value of the component’s visible
property. In order to get a decent default state, we set visible
to true
when the component is loaded.
export class ZippyComponent {
visible = true;
toggle() {
this.visible = !this.visible;
}
}
Now that we have a property that represents the visibility state of the content, we can use it in our template accordingly. Instead of ngHide
or ngShow
(which we also don’t have in Angular >= 2.x), we can simply bind the value of our visible
property to our zippy content’s hidden
property, which every DOM element has by default.
...
<div class="zippy__content" [hidden]="!visible">
This is some content.
</div>
...
Again, what we see here is part of the new template syntax in Angular. Angular >= 2.x binds to properties rather than attributes in order to work with Web Components, and this is how you do it. We can now click on the zippy title and the content toggles!
Oh! The little arrow in the title still points down, even if the zippy is closed. We can fix that easily with Angular’s interpolation like this:
...
<div class="zippy__title" (click)="toggle()">
{{ visible ? '▾' : '▸' }} Details
</div>
...
Okay, we’re almost there. Let’s make the zippy title configurable. We want that consumers of our component can define how they pass a title to it. Here’s what our consumer will be able to do:
<zippy title="Details"></zippy>
<zippy [title]="'Details'"></zippy>
<zippy [title]="evaluatesToTitle"></zippy>
In Angular >= 2.x, we don’t need to specify how scope properties are bound in our component, the consumer does. That means, this gets a lot easier in Angular too, because all we need to do is to import the @Input()
decorator and teach our component about an input property, like this:
import { Component, Input } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'my-zippy',
templateUrl: 'my-zippy.component.html'
})
export class ZippyComponent {
@Input() title;
...
}
Basically what we’re doing here, is telling Angular that the value of the title
attribute is projected to the title
property. Input data that flows into the component. If we want to map the title
property to a different attribute name, we can do so by passing the attribute name to @Input()
:
@Component({
moduleId: module.id,
selector: 'my-zippy',
templateUrl: 'my-zippy.component.html'
})
export class ZippyComponent {
@Input('zippyTitle') title;
...
}
But for simplicity’s sake, we stick with the shorthand syntax. There’s nothing more to do to make the title configurable, let’s update the template for ZippyAppComponent
app.
@Component({
moduleId: module.id,
selector: 'zippy-app',
template: '<my-zippy title="Details"></zippy>',
})
...
Now we need to change the template of zippy to make title to appear at correct place, let’s udpate the template for zippy title.
...
<div class="zippy__title" (click)="toggle()">
{{ visible ? '▾' : '▸' }} {{title}}
</div>
...
Insertion Points instead of Transclusion
Our component’s title is configurable. But what we really want to enable, is that a consumer can decide what goes into the component and what not, right?
We could for example use our component like this:
<my-zippy title="Details">
<p>Here's some detailed content.</p>
</my-zippy>
In order to make this work, we’ve used transclusion in Angular 1. We don’t need transclusion anymore, since Angular 2.x makes use of Shadow DOM (Emulation) which is part of the Web Components specification. Shadow DOM comes with something called “Content Insertion Points” or “Content Projection”, which lets us specify, where DOM from the outside world is projected in the Shadow DOM or view of the component.
I know, it’s hard to believe, but all we need to do is adding a <ng-content>
tag to our component template.
...
<div class="zippy__content" [hidden]="!visible">
<ng-content></ng-content>
</div>
...
Angular uses Shadow DOM (Emulation) since 2.x by default, so we can just take advantage of that technology. It turns out that insertion points in Shadow DOM are even more powerful than transclusion in Angular. Angular 1.5 introduces multiple transclusion slots, so we can explicitly “pick” which DOM is going to be projected into our directive’s template. The <ng-content>
tag lets us define which DOM elements are projected too. If you want to learn more about Shadow DOM, I recommend the articles on html5rocks.com or watch this talk from ng-europe.
Putting it all together
Yay, this is how we build a zippy component in Angular. Just to make sure we’re on the same page, here’s the complete zippy component code we’ve written throughout this article:
import { Component, Input } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'my-zippy',
templateUrl: 'my-zippy.component.html'
})
export class ZippyComponent {
@Input() title;
visible = true;
toggle() {
this.visible = !this.visible;
}
}
And here’s the template:
<div class="zippy">
<div (click)="toggle()" class="zippy__title">
{{ visible ? '▾' : '▸' }} {{title}}
</div>
<div [hidden]="!visible" class="zippy__content">
<ng-content></ng-content>
</div>
</div>
I’ve set up a repository so you can play with the code here. In fact, I’ve also added this component to the Angular project. The pull request is pending merged here and likely to be merged the next few days. At this point I’d like to say thank you to Victor and Misko for helping me out on getting this implemented.
You might notice that it also comes with e2e tests. The component itself even emits it’s own events using EventEmitter
, which we haven’t covered in this article. Check out the demos to see event emitters in action!