Multiple Components
Refactor the master/detail view into separate components.
The AppComponent
is doing everything at the moment. In the beginning, it showed details of a single hero. Then it became a master/detail form with both a list of heroes and the hero detail. Soon there will be new requirements and capabilities. You can't keep piling features on top of features in one component; that's not maintainable.
You'll ned to break it up into sub-components, each focused on a specific task or workflow. Eventually, the AppComponent
could become a simple shell that hosts those sub-components.
In this page, you'll take the first step in that direction by carving out the hero details into a separate, reusable component. When you're done, the app should look like this live example.
Where you left off
Before getting started on this page, verify that you have the following structure from earlier in the Tour of Heroes. If not, go back to the previous pages.
angular-tour-of-heroes src app app.component.ts app.module.ts main.ts index.html styles.css systemjs.config.js tsconfig.json node_modules ... package.json
Keep the app transpiling and running while you build the Tour of Heroes by entering the npm start
command in a terminal window as you did before.
Make a hero detail component
Add a file named hero-detail.component.ts
to the app/
folder. This file will hold the new HeroDetailComponent
.
The file and component names follow the standard described in the Angular style guide.
-
The component class name should be written in upper camel case and end in the word "Component". The hero detail component class is
HeroDetailComponent
. -
The component file name should be spelled in lower dash case, each word separated by dashes, and end in
.component.ts
. TheHeroDetailComponent
class goes in thehero-detail.component.ts
file.
Start writing the HeroDetailComponent
as follows:
app/hero-detail.component.ts (initial version)
import { Component } from '@angular/core'; @Component({ selector: 'hero-detail', }) export class HeroDetailComponent { }
To define a component, you always import the Component
symbol.
The @Component
decorator provides the Angular metadata for the component. The CSS selector name, hero-detail
, will match the element tag that identifies this component within a parent component's template. Near the end of this tutorial page, you'll add a <hero-detail>
element to the AppComponent
template.
Always export
the component class because you'll always import
it elsewhere.
Hero detail template
To move the hero detail view to the HeroDetailComponent
, cut the hero detail content from the bottom of the AppComponent
template and paste it into a new template
property in the @Component
metadata.
The HeroDetailComponent
has a hero, not a selected hero. Replace the word, "selectedHero", with the word, "hero", everywhere in the template. When you're done, the new template should look like this:
src/app/hero-detail.component.ts (template)
@Component({ selector: 'hero-detail', template: ` <div *ngIf="hero"> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name"/> </div> </div> ` })
Add the hero property
The HeroDetailComponent
template binds to the component's hero
property. Add that property to the HeroDetailComponent
class like this:
src/app/hero-detail.component.ts (hero property)
hero: Hero;
The hero
property is typed as an instance of Hero
. The Hero
class is still in the app.component.ts
file. Now there are two components that need to reference the Hero
class. The Angular style guide recommends one class per file anyway.
Move the Hero
class from app.component.ts
to its own hero.ts
file.
src/app/hero.ts
export class Hero { id: number; name: string; }
Now that the Hero
class is in its own file, the AppComponent
and the HeroDetailComponent
have to import it. Add the following import
statement near the top of both the app.component.ts
and the hero-detail.component.ts
files.
import { Hero } from './hero';
The hero property is an input property
Later in this page, the parent AppComponent
will tell the child HeroDetailComponent
which hero to display by binding its selectedHero
to the hero
property of the HeroDetailComponent
. The binding will look like this:
<hero-detail [hero]="selectedHero"></hero-detail>
Putting square brackets around the hero
property, to the left of the equal sign (=), makes it the target of a property binding expression. You must declare a target binding property to be an input property. Otherwise, Angular rejects the binding and throws an error.
First, amend the @angular/core
import statement to include the Input
symbol.
src/app/hero-detail.component.ts (excerpt)
import { Component, Input } from '@angular/core';
Then declare that hero
is an input property by preceding it with the @Input
decorator that you imported earlier.
src/app/hero-detail.component.ts (excerpt)
@Input() hero: Hero;
Read more about input properties in the Attribute Directives page.
That's it. The hero
property is the only thing in the HeroDetailComponent
class.
export class HeroDetailComponent { @Input() hero: Hero; }
All it does is receive a hero object through its hero
input property and then bind to that property with its template.
Here's the complete HeroDetailComponent
.
src/app/hero-detail.component.ts
import { Component, Input } from '@angular/core'; import { Hero } from './hero'; @Component({ selector: 'hero-detail', template: ` <div *ngIf="hero"> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name"/> </div> </div> ` }) export class HeroDetailComponent { @Input() hero: Hero; }
Declare HeroDetailComponent in the AppModule
Every component must be declared in one—and only one—Angular module.
Open app.module.ts
in your editor and import the HeroDetailComponent
so you can refer to it.
src/app/app.module.ts
import { HeroDetailComponent } from './hero-detail.component';
Add HeroDetailComponent
to the module's declarations
array.
src/app/app.module.ts
declarations: [ AppComponent, HeroDetailComponent ],
In general, the declarations
array contains a list of application components, pipes, and directives that belong to the module. A component must be declared in a module before other components can reference it. This module declares only the two application components, AppComponent
and HeroDetailComponent
.
Read more about Angular modules in the NgModules guide.
Add the HeroDetailComponent to the AppComponent
The AppComponent
is still a master/detail view. It used to display the hero details on its own, before you cut out that portion of the template. Now it will delegate to the HeroDetailComponent
.
Recall that hero-detail
is the CSS selector
in the HeroDetailComponent
metadata. That's the tag name of the element that represents the HeroDetailComponent
.
Add a <hero-detail>
element near the bottom of the AppComponent
template, where the hero detail view used to be.
Coordinate the master AppComponent
with the HeroDetailComponent
by binding the selectedHero
property of the AppComponent
to the hero
property of the HeroDetailComponent
.
app.component.html (excerpt)
<hero-detail [hero]="selectedHero"></hero-detail>
Now every time the selectedHero
changes, the HeroDetailComponent
gets a new hero to display.
The revised AppComponent
template should look like this:
app.component.ts (excerpt)
template: ` <h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <hero-detail [hero]="selectedHero"></hero-detail> `,
What changed?
As before, whenever a user clicks on a hero name, the hero detail appears below the hero list. But now the HeroDetailView
is presenting those details.
Refactoring the original AppComponent
into two components yields benefits, both now and in the future:
-
You simplified the
AppComponent
by reducing its responsibilities. -
You can evolve the
HeroDetailComponent
into a rich hero editor without touching the parentAppComponent
. -
You can evolve the
AppComponent
without touching the hero detail view. -
You can re-use the
HeroDetailComponent
in the template of some future parent component.
Review the app structure
Verify that you have the following structure:
angular-tour-of-heroes src app app.component.ts app.module.ts hero.ts hero-detail.component.ts main.ts index.html styles.css systemjs.config.js tsconfig.json node_modules ... package.json
Here are the code files discussed in this page.
import { Component, Input } from '@angular/core'; import { Hero } from './hero'; @Component({ selector: 'hero-detail', template: ` <div *ngIf="hero"> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name"/> </div> </div> ` }) export class HeroDetailComponent { @Input() hero: Hero; }
import { Component } from '@angular/core'; import { Hero } from './hero'; const HEROES: Hero[] = [ { id: 11, name: 'Mr. Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, { id: 15, name: 'Magneta' }, { id: 16, name: 'RubberMan' }, { id: 17, name: 'Dynama' }, { id: 18, name: 'Dr IQ' }, { id: 19, name: 'Magma' }, { id: 20, name: 'Tornado' } ]; @Component({ selector: 'my-app', template: ` <h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <hero-detail [hero]="selectedHero"></hero-detail> `, styles: [` .selected { background-color: #CFD8DC !important; color: white; } .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: .3em 0; height: 1.6em; border-radius: 4px; } .heroes li.selected:hover { background-color: #BBD8DC !important; color: white; } .heroes li:hover { color: #607D8B; background-color: #DDD; left: .1em; } .heroes .text { position: relative; top: -3px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; } `] }) export class AppComponent { title = 'Tour of Heroes'; heroes = HEROES; selectedHero: Hero; onSelect(hero: Hero): void { this.selectedHero = hero; } }
export class Hero { id: number; name: string; }
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { HeroDetailComponent } from './hero-detail.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, HeroDetailComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
The road you’ve travelled
Here's what you achieved in this page:
- You created a reusable component.
- You learned how to make a component accept input.
- You learned to declare the required application directives in an Angular module. You listed the directives in the
NgModule
decorator'sdeclarations
array. - You learned to bind a parent component to a child component.
Your app should look like this live example.
The road ahead
The Tour of Heroes app is more reusable with shared components, but its (mock) data is still hard coded within the AppComponent
. That's not sustainable. Data access should be refactored to a separate service and shared among the components that need data.
You’ll learn to create services in the next tutorial page.
Next Step
Services
© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v2.angular.io/docs/ts/latest/tutorial/toh-pt3.html