@Component({
selector: 'joke',
template: `
<div class="card card-block">
<h4 class="card-title">{{ joke.setup }}</h4>
<p class="card-text"
[hidden]="joke.hide">{{ joke.punchline }}</p>
<a (click)="joke.toggle()"
class="btn btn-warning">Tell Me
</a>
</div>
`
})
class JokeComponent {
joke: Joke;
}
Nesting Components & Inputs
Goal
An application in Angular 2 is a set of custom components, tags, glued together in HTML.
So far we’ve only built applications with a single component, our goal now is to start building applications that are composed of multiple components working together.
Breaking up an application into multiple logical components makes it easier to:
-
Architect an application as it grows in complexity.
-
Re-use common components in multiple places.
The goal of this lecture is to break up our small application into 3 components and start binding them together.
Learning Outcomes
-
How to create and configure multiple components in one application.
-
How to enable Input Property Binding on a custom component so components can communicate with each other.
Create & Configure Multiple Components
If you think of a typical webpage we can normally break it down into a set of logical components each with its own view, for example most webpages can be broken up into a header, footer and perhaps a sidebar, like so:
We are going to break up our application into a top level AppComponent, this component won’t have any functionality and will just contain other Components. This will hold our JokeListComponent and our JokeListComponent will hold a list of JokeComponents.
Note
Our components will therefore nest something like the below image:
Tip
JokeComponent
This looks similar to our previous JokeListComponent
we just removed the NgFor
since this component will now render a single joke.
JokeListComponent
We’ve broken out a joke into its own JokeComponent
so now we change the JokeListComponent
template to contain multiple JokeComponent
components instead.
@Component({
selector: 'joke-list',
template: `
<joke *ngFor="let j of jokes"></joke>
`
})
class JokeListComponent {
jokes: Joke[];
constructor() {
this.jokes = [
new Joke("What did the cheese say when it looked in the mirror?", "Hello-me (Halloumi)"),
new Joke("What kind of cheese do you use to disguise a small horse?", "Mask-a-pony (Mascarpone)"),
new Joke("A kid threw a lump of cheddar at me", "I thought ‘That’s not very mature’"),
];
}
}
AppComponent
Our final component is our new top level AppComponent
, this just holds an instance of the JokeListComponent
.
@Component({
selector: 'app',
template: `
<joke-list></joke-list>
`
})
class AppComponent {
}
Configuring multiple Components
In order to use our new components we need to add them to the declarations on our NgModule
.
And since we’ve changed our top level component we need to set that in the bootstrap property as well as change our index.html
to use the <app></app>
root component instead.
@NgModule({
imports: [BrowserModule],
declarations: [
AppComponent,
JokeComponent,
JokeListComponent
],
bootstrap: [AppComponent]
})
export class AppModule {
}
<body class="container m-t-1">
<app></app>
</body>
In Angular 2 we need to be explicit regarding what components & directives are going to use in our Angular Module by either adding them to the imports
or declarations
property.
In Angular 1 each directive when added via a script tag was globally available, which made it convenient for smaller projects but a problem for larger ones. Issues like name clashes came up often as different third party libraries often used the same names.
With Angular 2 two third party libraries can export the same name for components but only the version we explicitly include into our Angular Module will be used.
Tip
NgFor
are all defined in CommonModule
which is again included in BrowserModule
which we have already added to our NgModule
imports.
Input Property Binding on a Custom Component
If we ran the application now we would see just some empty boxes with some errors in the console, like so:
The errors should read something like:
class JokeComponent - inline template:2:25 caused by: Cannot read property 'setup' of undefined
The above should give you a hint about what’s going on
-
It’s something to do with the
JokeComponent
-
It’s something to do with the template.
-
It’s something to do with the
setup
property.
So if we look at the offending part of our JokeComponent
template:
<h4 class="card-title">{{ joke.setup }}</h4>
Essentially Cannot read property 'setup' of undefined
in the context of joke.setup
means that joke
is undefined, it’s blank
If you remember from our JokeComponent
class we do have a property called joke
:
class JokeComponent {
joke: Joke;
}
And we are looping and creating JokeComponents in our JokeListComponent, like so:
<joke *ngFor="let j of jokes"></joke>
But we are not setting the property joke
of our JokeComponent
to anything, which is why it’s undefined
.
Ideally we want to write something like this:
<joke *ngFor="let j of jokes" [joke]="j"></joke>
In just the same way as we bind to the hidden
property of the p
tag in the element above we want to bind to the joke
property of our JokeComponent
.
Even though our JokeComponent
has a joke
property we can’t bind to it using the []
syntax, we need to explicitly mark it as an Input property on our JokeComponent
.
We do this by prepending the joke
property in the Component with a new annotation called @Input
, like so:
import { Input } from '@angular/core';
.
.
.
class JokeComponent {
@Input() joke: Joke;
}
This tells Angular that the joke
property is an input property and therefore in HTML we can bind to it using the []
input property binding syntax.
This @Input
now becomes part of the public interface of our component.
Lets say at some future point we decided to change the joke
property of our JokeComponent to perhaps just data
, like so:
class JokeComponent {
@Input() data: Joke;
}
Because this input is part of the public interface for our component we would also need to change all the input property bindings every where our component is used, like so:
<joke *ngFor="let j of jokes" [data]="j"></joke>
Not a great thing to ask the consumers of your component to have to do.
This is a common problem so to avoid expensive refactors the @Input
annotation takes a parameter which is the name of the input property to the outside world, so if we changed our component like so:
class JokeComponent {
@Input('joke') data: Joke;
}
To the outside world the input property name is still joke and we could keep the JokeListComponent
template the same as before:
<joke *ngFor="let j of jokes" [joke]="j"></joke>
Summary
An Angular application should be broken down into small logical Components which are glued together in HTML.
Its normal to have one root component called AppComponent which acts as the root node in the Component tree.
We need to explicitly declare all Components in the applications root NgModule.
We can make properties of our custom components input bindable with the []
syntax by prepending them with the @Input
annotation.
Listing
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {NgModule, Input} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {Component} from '@angular/core';
class Joke {
public setup: string;
public punchline: string;
public hide: boolean;
constructor(setup: string, punchline: string) {
this.setup = setup;
this.punchline = punchline;
this.hide = true;
}
toggle() {
this.hide = !this.hide;
}
}
@Component({
selector: 'joke',
template: `
<div class="card card-block">
<h4 class="card-title">{{ data.setup }}</h4>
<p class="card-text"
[hidden]="data.hide">{{ data.punchline }}</p>
<a (click)="data.toggle()"
class="btn btn-warning">Tell Me
</a>
</div>
`
})
class JokeComponent {
@Input('joke') data: Joke;
}
@Component({
selector: 'joke-list',
template: `
<joke *ngFor="let j of jokes" [joke]="j"></joke>
`
})
class JokeListComponent {
jokes: Joke[];
constructor() {
this.jokes = [
new Joke("What did the cheese say when it looked in the mirror?", "Hello-me (Halloumi)"),
new Joke("What kind of cheese do you use to disguise a small horse?", "Mask-a-pony (Mascarpone)"),
new Joke("A kid threw a lump of cheddar at me", "I thought ‘That’s not very mature’"),
];
}
}
@Component({
selector: 'app',
template: `
<joke-list></joke-list>
`
})
class AppComponent {
}
@NgModule({
imports: [BrowserModule],
declarations: [
AppComponent,
JokeComponent,
JokeListComponent
],
bootstrap: [AppComponent]
})
export class AppModule {
}
platformBrowserDynamic().bootstrapModule(AppModule);
<!DOCTYPE html>
<!--suppress ALL -->
<html>
<head>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/css/bootstrap.min.css">
<script src="https://unpkg.com/core-js/client/shim.min.js"></script>
<script src="https://unpkg.com/[email protected]?main=browser"></script>
<script src="https://unpkg.com/[email protected]"></script>
<script src="https://unpkg.com/[email protected]/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('script.ts').catch(function (err) {
console.error(err);
});
</script>
</head>
<body class="container m-t-1">
<app></app>
</body>
</html>