Tutorial: Master/Detail Components
REFERENCES |
---|
[1]: https://angular.io/tutorial/toh-pt3 "Master/Detail Components" |
At the moment, the HeroesComponent
displays both the list of heroes and the selected hero's details.
Keeping all features in one component as the application grows will not be maintainable. You'll want to split up large components into smaller sub-components, each focused on a specific task or workflow.
In this page, you'll take the first step in that direction by moving the hero details into a separate, reusable HeroDetailsComponent
.
The HeroesComponent
will only present the list of heroes. The HeroDetailsComponent
will present details of a selected hero.
Make the HeroDetailComponent
Use the Angular CLI to generate a new component named hero-detail
.
$ ng g component hero-detail --module=app.module
NOTES: Using --module=app.module
helps avoid this type of error:
Error: More than one module matches. Use skip-import option to skip importing the component into the closest module.
More than one module matches. Use skip-import option to skip importing the component into the closest module.
The command scaffolds the HeroDetailComponent
files and declares the component in AppModule
.
Write the template
Cut the HTML for the hero detail from the bottom of the HeroesComponent
template and paste it over the generated boilerplate in the HeroDetailComponent
template.
The pasted HTML refers to a selectedHero
. The new HeroDetailComponent
can present any hero, not just a selected hero. So replace "selectedHero" with "hero" everywhere in the template.
When you're done, the HeroDetailComponent
template should look like this:
src/app/hero-detail/hero-detail.component.html
<div class="row" *ngIf="hero">
<div class="col-sm-6 offset-sm-3 mt-3">
<mat-card class="mat-elevation-z3 mat-typography">
<mat-card-header>
<img mat-card-avatar class="avatar-image" src="{{ hero.avatarPath }}" alt="{{ hero.name }} image">
<mat-card-title>{{ hero.name | uppercase }}</mat-card-title>
<mat-card-subtitle><span>ID:</span> {{ hero.id }}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="{{ hero.imgPath }}" alt="{{ hero.name }} image">
<mat-card-content>
<mat-input-container class="d-flex flex-column w-100">
<input type="text" placeholder="Name:" matInput [(ngModel)]="hero.name" class="d-flex flex-column w-100">
</mat-input-container>
<p><b>Real Name:</b> {{ hero.realName }}</p>
<p><b>Powers:</b> {{ hero.powers }}</p>
<p><b>Abilities:</b> {{ hero.abilities }}</p>
</mat-card-content>
<mat-card-actions>
<button mat-button>LIKE</button>
</mat-card-actions>
</mat-card>
</div>
</div>
Add the @Input()
hero property
The HeroDetailComponent
template binds to the component's hero
property which is of type Hero
.
Open the HeroDetailComponent
class file and import the Hero
symbol.
src/app/hero-detail/hero-detail.component.ts
...
import { Hero } from '../hero';
...
The hero
property must be an Input property, annotated with the @Input()
decorator, because the external HeroesComponent
will bind to it.
Amend the @angular/core
import statement to include the Input
symbol.
src/app/hero-detail/hero-detail.component.ts
import { Component, OnInit, Input } from '@angular/core';
...
Add a hero
property, preceded by the @Input()
decorator.
src/app/hero-detail/hero-detail.component.ts
export class HeroDetailComponent implements OnInit {
@Input hero: Hero;
constructor() { }
ngOnInit() { }
}
That's the only change you should make to the HeroDetailComponent
class. There are no more properties. There's no presentation logic. This component simply receives a hero object through its hero
property and displays it.
Show the HeroDetailComponent
The HeroesComponent
is still a master/detail view.
It used to display the hero details on its own, before you cut that portion of the template. Now it will delegate to the HeroDetailComponent
.
The two components will have a parent/child relationship. The parent HeroesComponent
will control the child HeroDetailComponent
by sending it a new hero to display whenever the user selects a hero from the list.
You won't change the HeroesComponent
class but you will change its template.
Update the HeroesComponent
template
The HeroDetailComponent
selector is 'app-hero-detail'
. Add an <app-hero-detail>
element near the bottom of the HeroesComponent
template, where the hero detail view used to be.
Bind the HeroesComponent.selectedHero
to the element's hero
property like this.
src/app/heroes/heroes.component.html
<div class="row mt-3">
<div class="col-sm-6 offset-sm-3">
<mat-card class="mat-elevation-z3 mat-typography">
<mat-list class="">
<mat-list-item *ngFor="let hero of heroes" (click)="onSelect(hero)"
[class.selected]="hero === selectedHero">
<a href="#" mat-button class="mat-button" aria-disabled="false" style="padding: 8px">
<img matListAvatar src="{{ hero.avatarPath }}" alt="">
<p class="hero-list blue-grey-text text-lighten-3">{{ hero.id }}</p>
<p class="hero-list">{{ hero.name}}</p>
<div class="mat-button-ripple mat-ripple" mat-ripple></div>
<div class="mat-button-focus-overlay"></div>
</a>
</mat-list-item>
</mat-list>
</mat-card>
</div>
</div>
<!-- Bind the selectedHero to hero attribte from hero-detail.component.ts -->
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
[hero]="selectedHero"
is an Angular property binding.
It's a one way data binding from the selectedHero
property of the HeroComponent
to the hero
property of the target element, which maps to the hero
property of the HeroDetailComponent
.
Now when the user clicks a hero in the list, the selectedHero
changes. When the selectedHero
changes, the property binding updates hero
and the HeroDetailComponent
displays the new hero.
The browser refreshes and the app starts working again as it did before.
What changed?
As before, whenever a user clicks on a hero name, the hero detail appears below the hero list. Now the HeroDetailComponent
is presenting those details instead of the HeroesComponent
.
Refactoring the original HeroesComponent
into two components yields benefits, both now and in the future:
- You simplified the
HeroesComponent
by reducing its responsibilities. - You can evolve the
HeroDetailComponent
into a rich hero editor without touching the parentHeroesComponent
. - You can evolve the
HeroesComponent
without touching the hero detail view. - You can re-use the
HeroDetailComponent
in the template of some future component.
Summary
- You created a separate, reusable
HeroDetailComponent
.
- You used a property binding to give the parent
HeroesComponent
control over the childHeroDetailComponent
.
- You used the
@Input
decorator to make thehero
property available for binding by the externalHeroesComponent
.