π3. Create a feature component
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 won't be maintainable. This tutorial splits up large components into smaller subcomponents, each focused on a specific task or workflow.
The first step is to move the hero details into a separate, reusable HeroDetailComponent and end up with:
A
HeroesComponentthat presents the list of heroes.A
HeroDetailComponentthat presents the details of a selected hero.
Make the HeroDetailComponent
HeroDetailComponentUse this ng generate command to create a new component named hero-detail.
ng generate component hero-detailThe command scaffolds the following:
Creates a directory
src/app/hero-detail.
Inside that directory, four files are created:
A SCSS file for the component styles.
An HTML file for the component template.
A TypeScript file with a component class named
HeroDetailComponent.A test file for the
HeroDetailComponentclass.
Write the template
Cut the HTML for the hero detail from the bottom of the HeroesComponent template and paste it over the boilerplate content in the HeroDetailComponent template.
The pasted HTML refers to a selectedHero. The new HeroDetailComponent can present any hero, not just a selected hero. Replace selectedHero with hero everywhere in the template.
When you're done, the HeroDetailComponent template should look like this:
@if (hero(); as hero) {
<div>
<h2>{{hero.name | uppercase}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div>
<label for="hero-name">Hero name: </label>
<input id="hero-name" [(ngModel)]="hero.name" placeholder="name">
</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.
import { Hero } from '../hero';The hero property input signal, because the external HeroesComponent binds to it like this.
<app-hero-detail [hero]="selectedHero" />Amend the @angular/core import statement to include the input symbol.
import { Component, input } from '@angular/core';Add a hero property, preceded by the input function.
hero = input<Hero>();That's the only change you should make to the HeroDetailComponent class. There are no more properties. There's no presentation logic. This component only receives a hero object through its hero property and displays it.
Show the HeroDetailComponent
HeroDetailComponentThe HeroesComponent used to display the hero details on its own, before you removed that part of the template. This section guides you through delegating logic to the HeroDetailComponent.
The two components have a parent/child relationship. The parent, HeroesComponent, controls the child, HeroDetailComponent by sending it a new hero to display whenever the user selects a hero from the list.
You don't need to change the HeroesComponent class, instead change its template.
Update the HeroesComponent template
HeroesComponent templateThe 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.
<app-hero-detail [hero]="selectedHero" />[hero]="selectedHero" is an Angular property binding.
It's a one-way data binding from the selectedHero property of the HeroesComponent 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 revised HeroesComponent template should look like this:
<h2>My Heroes</h2>
<ul class="heroes">
@for (hero of heroes; track hero.id; let i = $index) {
<li>
<button [class.selected]="hero === selectedHero" type="button" (click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span>
<span class="name">{{hero.name}}</span>
</button>
</li>
}
</ul>
<app-hero-detail [hero]="selectedHero" />The browser refreshes and the application 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 reduced the
HeroesComponentresponsibilities.You can evolve the
HeroDetailComponentinto a rich hero editor without touching the parentHeroesComponent.You can evolve the
HeroesComponentwithout touching the hero detail view.You can re-use the
HeroDetailComponentin the template of some future component.
Final code review
Here are the code files discussed on this page.
import { Component, input } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: ['./hero-detail.component.scss'],
standalone: true
})
export class HeroDetailComponent {
hero = input<Hero>();
}@if(hero(); as hero) {
<div>
<h2>{{hero.name | uppercase}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div>
<label for="hero-name">Hero name: </label>
<input id="hero-name" [(ngModel)]="hero.name" placeholder="name">
</div>
</div>
}<h2>My Heroes</h2>
<ul class="heroes">
@for (hero of heroes; track hero.id; let i = $index) {
<li>
<button [class.selected]="hero === selectedHero" type="button" (click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span>
<span class="name">{{hero.name}}</span>
</button>
</li>
}
</ul>
<app-hero-detail [hero]="selectedHero" />Summary
You created a separate, reusable
HeroDetailComponent.You used a property binding to give the parent
HeroesComponentcontrol over the childHeroDetailComponent.You used the input function to make the
heroproperty available for binding by the externalHeroesComponent.
Last updated