Angular コンポーネントと双方向データバインディング
Vue.jsと比較するためにAngularを勉強しています。前回はAngularの管理アプリであるAngular CLIをインストールし、公式のチュートリアルの教材である「Tour of heroes」アプリのプロジェクトの作成から補間までを作業しました。
今回はコンポーネントの作成から、インターフェース、双方向データバインディングまでを書きます。
新しいコンポーネントの作成
「ヒーローエディター」にしたがって、前回のプロジェクトに新しいコンポーネント(機能のかたまり)を追加します。
CUI(ここではWindows Powrshellを利用しています)の操作で、src¥appに移動して、「ng generate component コンポーネント名」と入力します。
PS> ng generate component heroes
...
appフォルダ内にheroesフォルダができ、中に次のファイルが作成されました。
- heroes.component.html
- heroes.component.spec.ts
- heroes.component.ts
- heroes.component.css
さらにapp¥app.module.tsに次の下線部の記述が追加されます。
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
@NgModule({
declarations: [
AppComponent,
HeroesComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
ファイルの構成は前回アプリとして初期化したファイル群と一緒ですが、heroes.componet.tsファイルを開くと前回と少し変わっていることに気づくと思います。
コンポーネントとモジュール
作成したコンポーネントは、app.module.tsに登録されました。コンポーネントはWeb画面を操作する機能のまとまり、モジュールはコンポーネントとまとまりとなります。
app.module.ts内で、deeclarationsの項目はこのモジュール(app.module)にあるコンポーネントのリストとなります。また、importsには利用するほかのモジュールが入ります。
モジュールに所属するコンポーネントのうち最初に起動されるのが、bootstrapに記載されたモジュールとなります。他、exportsという項目もありここには外部に公開するコンポーネントが記述されます。
heroes.componet.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
implementsはクラスの定義時にインターフェース(Interface)を指定するものです。ここではOnInitというインターフェースを指定しています。インターフェースはES(通常のJavaScript)には存在せず、TypeScriptにのみ存在する機能で、簡単に言うと特定のプロパティ(変数)やメソッド(関数)が存在することを保証するものです。OnInitインターフェースをimplementしたクラスではngOnLinit()関数が必ず存在することになります。
全体の仕掛けとしては、AngularはOnInitをimplementしているコンポーネントの作成直後にngOnInit()を呼び出すようになっています。
constructorの部分に関しては通常のJavascriptのクラスのコンストラクタです。クラス生成時に呼び出されます。
サンプルではheroes.component.htmlとheroes.componet.tsを次のように編集しています。
heroes.component.html
{{hero}}
heroes.componet.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
hero = 'Windstorm';
constructor() { }
ngOnInit(): void {
}
}
これでheroコンポーネントの作成は完了です。
app.module.tsにHeroesComponentをimportする文が加わったことで、前回作成したapp.component.html内で今回設定したコンポーネントのセレクタである「app-heroes」というHTML要素を書き込めばコンポーネントを利用できるようになっています。
app.component.html
<h1>{{title}}</h1>
<app-heroes></app-heroes>
app.componentという親でheroes.componentという子を読み込むイメージです。注意したいのは親側で子側のプロパティは読みだせないことです。app.component.html内で{{hero}}としてしまうとエラーになります。
インターフェースの使い方と、補間時のパイプ
プロパティを補間する方法までできたので、今度はそれを拡張していきます。
未来の実運用において、表示するデータには一定の型があると思います。その型をインターフェース(interface)で定義します。単独の.tsファイルとしてexportを指定して作成し、必要なアプリでimportできるようにします。クラス名同様にインターフェースはパスカルケース(先頭を大文字)で定義します。ちなみにTypeScriptのコーディング規約(非公式)等を読んで、自分なりのコーディング規約を作っておくとブレが少なくなると思います。
作成する場所はsrc¥appフォルダです。
hero.ts
export interface Hero {
id: number;
name: string;
}
「変数名: 型」の形でプロパティのメタデータを定義します。
先のheroes.component.tsに戻り、Heroインターフェースを読み込み、hero変数にセットします。
@angular/coreにはパスが通っているのでそのまま記述できますが、Heroインターフェースは「../hero」として作成した一つ上のフォルダを参照します。
チュートリアル上ではimportするファイルに対して拡張子がありません。この有無はバンドラ(複数のtsを束ねてくれるアプリ)によって必要だったり、不要だったり分かれるそうです。筆者の構築した環境では拡張子をセットするとエラーとなりました。
「hero: Hero ={...};」の箇所では、Heroインタフェースをheroとしてインスタンスを作成しています。作成時のパラメータとしてオブジェクトを渡しますが、このオブジェクトに書かれた変数が、インターフェースで定義したものと一致しないとエラーとなります。
heroes.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
hero: Hero = {
id: 1,
name: 'Windstorm'
};
constructor() { }
ngOnInit(): void {
}
}
heroの中身をオブジェクトにしたので、HTMLファイルの補間部分も修正します。
heroes.component.html
<h2>{{hero.name | uppercase}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div><span>name: </span>{{hero.name}}</div>
h2タグの「hero.name | uppercase」ですが、|はパイプ処理を示しています。hero.nameの値がuppercaseを通って大文字化されて出力されます。他のパイプの機能には通貨や日付フォーマットなどがあります。
双方向データバインディング
双方向データバインディングとは、スクリプト内で変数の値を変えるとHTMLでの表示が変わり、HTMLのINPUT要素の値を変えるとスクリプト内の変数の値も変わるという仕組みです。
定義の方法はinputの属性に [(ngModel)]="変数" とします。例えば先ほどのhero.nameに双方向データバインディングを設定するにはheroes.component.htmlを次のようにします。
labelタグはinputタグに見出しを付けるもので必須ではありません。この中にはボタンや見出し(h)を入れないように注意しましょう。
また、[(ngModel)]=の後は{{}}でなく""で囲むところも、最初は間違えやすいかもしれません。
heroes.component.html
<h2>{{hero.name | uppercase}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div><label>name: <input [(ngModel)]="hero.name"></label></div>
ngModelはFormsModuleをインポートしないと利用できない為、app.mudule.tsでインポートしてさらに設定(メタデータ)を記述しておく必要があります。
先のHeroesComponentはdeclarationsの配列に追加しましたが、FormsModuleはimportsの配列に追加します。
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
@NgModule({
declarations: [
AppComponent,
HeroesComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Angularでは、重要な設定(メタデータ)は、src¥app¥app.module.tsに記述し、そのほかの設定はコンポーネントの.tsファイルに記述します。
app.module.tsの@NgModuleに書かれた設定は、トップレベルのAppModuleに反映され全体を通して利用できるようになります。