この投稿は、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
内でハードコードされています。
この状態は持続可能ではありません。
データアクセスは別のサービスにリファクタリングし、データを必要とするコンポーネント間で共有する必要があります。
次のチュートリアルページ(原文)でサービスを作成する方法を学びます。