この投稿は、Angular公式チュートリアルの 4. Multiple Components を和訳した記事です。
職場で行うAngular勉強会を円滑に進めることを目的としています。
筆者自身もAngularについては初級者で、英語の知識も乏しいため、記事内容への指摘がありましたら是非コメントをお願いします。
なお、この記事はAngular v4に対応したチュートリアルの和訳記事です。
最新版のチュートリアルではありませんのでご注意ください。
【チュートリアル一覧】
1. Introduction : 和訳ページ / 原文
2. The Hero Editor : 和訳ページ / 原文
3. Master/Detail : 和訳ページ / 原文
4. Multiple Components : このページです / 原文
5. Services : 和訳ページ作成中 / 原文
6. Routing : 和訳ページ作成中 / 原文
7. HTTP : 和訳ページ作成中 / 原文
Multiple Components
AppComponentは現時点ですべて処理を実行しています。
最初は、単一のヒーローの詳細を示しました。
その後、ヒーローのリストとヒーローの詳細の両方を持つ_Master/Detail_フォームになりました。
まもなく新しい要件と機能が生まれます。
しかし、1つのコンポーネントに機能を積み重ね続けることはできません。
なぜならメンテナンス性が低いからです。
特定の責務や機能に焦点を絞ったサブコンポーネントに分割する必要があります。
最終的に、AppComponentは、それらのサブコンポーネントを司るシンプルな骨格になる可能性があります。
このページでは、そういった方針へ向けた最初のステップとして、ヒーローの詳細ビューを再利用可能な別のコンポーネントに分割します。
このページが完成したら、アプリはこの例のようになります。
https://v4.angular.io/generated/live-examples/toh-pt3/eplnkr.html
前回どこで終わったか
このページを開始する前に、Tour of Heroesの初期の構造と同じ構造であることを確認してください。 そうでない場合は、前のページに戻ります。
これまでと同じようにターミナルウ画面にnpm startコマンドを入力して、
Tour of Heroesを構築している間にアプリをコンパイル・実行し続けるようにしておいてください。
HeroDetailComponent(ヒーロー詳細コンポーネント)を作る
app/フォルダーにhero-detail.component.tsという名前のファイルを追加します。
このファイルには、新しいHeroDetailComponentというコンポーネントを持たせます。
ファイル名とコンポーネント名は、Angularスタイルガイドに記載されている標準に従います。
- Componentクラス名は、アッパーキャメルケースで書かれ、 "Component"という単語で終わるべきです。 ヒーロー詳細コンポーネントクラス名は
HeroDetailComponentです。 - コンポーネントファイルの名前は、ダッシュ記号で綴る必要があります。各単語はダッシュで区切り、
.component.tsで終わります。HeroDetailComponentクラスはhero-detail.component.tsファイルに格納されます。
次のようにHeroDetailComponentを書き始めてみましょう。
import { Component } from '@angular/core';
@Component({
selector: 'hero-detail',
})
export class HeroDetailComponent {
}
コンポーネントを定義するには、常にComponentシンボルをimportします。
@Componentデコレータは、コンポーネントのAngluarメタデータを提供します。
hero-detailというCSSセレクタ名は、親コンポーネントのテンプレート内のこのコンポーネントを識別する要素タグと一致します。
このチュートリアルの最後で、<hero-detail>要素をAppComponentテンプレートに追加します。
コンポーネントを他の場所でもインポートできるようにするため、常にコンポーネントクラスをエクスポートしてください。
ヒーロー詳細テンプレート
ヒーロー詳細ビューをHeroDetailComponentに移動するには、ヒーロー詳細コンテンツをAppComponentテンプレートの下部から切り取り、@Componentメタデータの新しいtemplateプロパティに貼り付けます。
HeroDetailComponentは、「選択されたヒーロー」ではなく、「(1人の)ヒーロー」の情報を持ちます。
テンプレート内のすべてのselectedHeroという単語を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>
`
})
heroプロパティを追加する
HeroDetailComponentのテンプレートは、コンポーネントのheroプロパティにバインドされます。
heroプロパティをHeroDetailComponentクラスに追加します。
hero: Hero;
heroプロパティはHeroクラスのインスタンスとして型付けされます。
Heroクラスはまだapp.component.tsファイルにあります。
しかし今、Heroクラスを参照する必要がコンポーネントが2つになりました。
Angularのスタイルガイドでは、ファイルごとに1つのクラスを推奨しています。
Heroクラスをapp.component.tsから独自のhero.tsファイルに移動しましょう。
export class Hero {
id: number;
name: string;
}
Heroクラスが独自のファイルになったので、AppComponentとHeroDetailComponentはそれをインポートする必要があります。
app.component.tsファイルとhero-detail.component.tsファイルの上部に次のimport文を追加します。
import { Hero } from './hero';
heroプロパティは「入力」プロパティ
このページの後半では、親であるAppComponentが、選択されたヒーローをHeroDetailComponentのheroプロパティにバインドすることによって、
表示するヒーローを子のHeroDetailComponentに伝えます。
バインディングする方法は次のようになります。
<hero-detail [hero]="selectedHero"></hero-detail>
heroプロパティの周りを角括弧で囲んで、等号(=)の左側に置くと、プロパティバインディング式の対象になります。
対象のバインディングプロパティ(今回の場合はhero)は子のHeroDetailComponentで「入力」プロパティとして宣言する必要があります。
それ以外の場合、Angularはバインディングを拒否し、エラーをスローします。
最初に、Inputシンボルをimportするために@angular/coreのimport文を修正します。
import { Component, Input } from '@angular/core';
その後、importした@Inputデコレータをheroの宣言前に置くことで、heroが「入力」プロパティであることを宣言します。
@Input() hero: Hero;
「入力」プロパティの詳細については、属性ディレクティブのページを参照してください。
これで終わりです。
heroプロパティは、HeroDetailComponentクラスが持つ唯一のプロパティです。
export class HeroDetailComponent {
@Input() hero: Hero;
}
HeroDetailComponentクラスが行うのは、「入力」プロパティであるheroプロパティでHeroオブジェクトを受け取った後、自身のテンプレートのheroプロパティにバインドすることだけです。
HeroDetailComponentの全体像は以下のようになります。
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;
}
AppModuleでHeroDetailComponentを宣言する
すべてのコンポーネントは、NgModuleで(NgModuleでのみ)宣言する必要があります。
エディタでapp.module.tsを開き、HeroDetailComponentを参照できるようにインポートします。
import { HeroDetailComponent } from './hero-detail.component';
HeroDetailComponentをモジュールのdeclarations配列に追加します。
declarations: [
AppComponent,
HeroDetailComponent
],
一般に、declarations配列には、モジュールに属するアプリケーションコンポーネント、パイプ、およびディレクティブのリストが含まれています。
あるコンポーネントが他のコンポーネントを参照するには、そのモジュール内でコンポーネントを宣言する必要があります。
このモジュールは、AppComponentとHeroDetailComponentの2つのアプリケーションコンポーネントのみを宣言しています。
NgModulesの詳細については、NgModulesガイドを参照してください。
AppComponentにHeroDetailComponentを追加する
AppComponentはまだmaster/detailビューのままです。
テンプレートのヒーロー詳細部分を切り取る前なので、ヒーロー詳細はまだAppComponent自身が表示しています。
今度はヒーロー詳細部分をHeroDetailComponentに委譲します。
hero-detailはHeroDetailComponentメタデータのCSSセレクタであることを思い出してください。
これは、HeroDetailComponentを表す要素のタグ名です。
ヒーロー詳細ビューを使用していたAppComponentテンプレートの下部に<hero-detail>要素を追加します。
AppComponentのselectedHeroプロパティをHeroDetailComponentのheroプロパティにバインドして、
親であるAppComponentをHeroDetailComponentと調整します。
<hero-detail [hero]="selectedHero"></hero-detail>
selectedHeroが変更されるたびに、HeroDetailComponentは新しいヒーローを取得して表示します。
修正されたAppComponentのテンプレートは、次のようになります。
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>
`,
何が変わったか?
以前同様、ユーザーがヒーロー名をクリックするたびに、ヒーローの詳細がヒーローリストの下に表示されます。
しかし今は、HeroDetailViewがその詳細を提示しています。
元のAppComponentを2つのコンポーネントに分けるリファクタリングは、現在、そして将来に渡って利益をもたらします。
-
AppComponentの責任を軽減することによってAppComponentが単純化されました。 - 親である
AppComponentに手を入れることなく、HeroDetailComponentを豊かなヒーローエディタに進化させることができます。 - ヒーローの詳細ビューに触れることなく、
AppComponentを進化させることができます。 - 将来の親コンポーネントのテンプレートで
HeroDetailComponentを再利用することができます。
アプリの構造を確認する
このページで解説したコードは次のとおりです。
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 { }
まとめ
このページであなたが達成したことは次のとおりです:
- 再利用可能なコンポーネントを作成しました。
- コンポーネントが入力を受け入れる方法を学びました。
- 必要なアプリケーションディレクティブを
NgModuleで宣言することを学びました。@NgModuleデコレータのdeclarations配列にディレクティブを列挙しました。 - 親コンポーネントを子コンポーネントにバインドする方法を学びました。
あなたのアプリはこの例のように見えるはずです。
https://v4.angular.io/generated/live-examples/toh-pt3/eplnkr.html
次のステップ
Tour of Heroesアプリは共有コンポーネントでより再利用しやすくなりましたが、
そのモックデータはまだAppComponent内でハードコードされています。
この状態は持続可能ではありません。
データアクセスは別のサービスにリファクタリングし、データを必要とするコンポーネント間で共有する必要があります。
次のチュートリアルページ(原文)でサービスを作成する方法を学びます。
