コンパイラかく語りき

import { Fun } from 'programming'

【抜粋】Angular アプリケーションプログラミングを読んだ

Angularアプリケーションプログラミング

Angularアプリケーションプログラミング

Angular アプリケーションプログラミングを読んだ

「Angular アプリケーションプログラミング」を読みながら書いた抜粋。

第1章 イントロダクション

Angularにおけるコンポーネントの概念は、HTML/CSSに加えてTSファイルによる@Component 情報も包含する。

第2章 Angularの基本

モジュールとは、関連するクラスのまとまり。 起動時に呼び出されるモジュールはメインモジュールあるいはルートモジュールと呼ばれる。それ以外はサブモジュール

@angular/some-package はすべてAngular標準のモジュール。

@angular/core > NGModuleはモジュール定義を行う。 @angular/platform-browser > BrowserModule はアプリをブラウザで起動させるためのもの。

Angularにおけるモジュールの実体は、TypeScriptのクラス。ただし、NGModuleデコレータでモジュール情報を宣言する必要がある。 NGModuleデコレータで利用できるパラメータは以下の通り。

名前 概要
imports 現在のモジュールで利用する他のモジュール一覧
exports 現在のモジュールから外部に公開するコンポーネントなど
declarations 現在のモジュールに属するコンポーネント
bootstrap アプリで最初に起動するべき最上位のコンポーネント(=ルートコンポーネント
id モジュールのID

命名規則のオススメは以下の通り。

種類 命名規則
クラス名 UpperCamelCase AppModule, FriendListComponent
ファイル名 名前.種類.ts (名前は KebabCaseで) app.module.ts, friend-list.component.ts
テストスクリプト テスト対象ファイルに +.spec.ts friend-list.component.spec.ts

コンポーネントの作成には@Comopnentデコレータを利用してクラス宣言を行い、外部から利用できるようにexportする。

@Component デコレータによるパラメータは以下の通り。

名前 概要
selector コンポーネント適用すべき要素を表すセレクター式
template コンポーネントに適用するビュー

コンポーネントのクラスプロパティが、Interporationを利用してビュー変数として参照できる。

アプリを起動するためのmain.ts とメインページとなるindex.htmlも必要。

第3章 データバインディング

Interpolationプロパティバインディングコンポーネント→ビューの片方向バインディング

Interpolation は…

  • JSとしてvalidな式を設定可能。(一部例外あり)
  • コンポーネントクラスのコンテキストにアクセスでき、グローバル空間にはアクセス不可。
  • 何度も評価されるためパフォーマンスに考慮する(シンプルで実行時間がかからず、冪等であるべき)

Interporation内では ?. (Safe navigation Operator) が利用可能。

プロパティバインディング はブラケットを利用することで、要素に値を設定できる。<img [src]="url" />

bind- やInterpolcation でも代用可能、<img bind-src="url" /> あるいは <img src="{{url}}" />

ブラケット構文を利用するのが無難。

InterpolationにHTML文字列を渡しても、エスケープして表示される。HTMLとして表示させたい場合は、innerHtml プロパティを利用。 しかし、<script />, <button />, <input /> など除去される要素もある。

この除去をパスするには DomSanitizer , SafeHtml を利用する。 ただし、HTML文字列の中身が検証されるわけではない。

プロパティが存在しない場合、<td[attr.rowspan]="len">結合</td>のように属性バインディングを行う。 属性とプロパティの両方が存在する時は、プロパティバインディングを優先する。

イベントバインディング がビュー(ユーザー)からコンポーネントに情報を引き渡すしくみを提供。 構文は <element (event)="exp" /> 。イベント名は onClick でも ng-click でもなく、単に click、のようになる。テンプレートステートメントである "exp" には多様な命令群をあてることができるが、役割分担という観点から、シンプルなものにとどめておくべき。 また、イベントハンドラーにイベントオブジェクトを渡すには、明示的に $event を渡す。 <button (click)="show($event)" />

テンプレート参照変数 を使うと、要素オブジェクト/要素値の受け渡しに関して、 $event 渡しよりも完結かつ直感的に書ける。

@Component({
  template: `
    <input #txt id="txt" name="txt" type="text" (input)=show(txt.value)" />
  `
})
export class AppComponent {
  show(input: string) {
     // do something with "input"
  }
}

また、テンプレート参照変数は、以下のようにテンプレート内の任意の場所からも参照できる。

@Component({
  template: `
    <labal>名: <input #name type="text" (change)="0"></label>
    <div>こんにちは、{{name.value}}さん!</div>
  `
})

また、 (change)="0" を設定することで、イベントが発生する度に、データバインディングが再実行される。

<element (keyup.enter)="exp" /> のような特殊なイベントにも注目。(キー情報を見て判定する、のようなコードが不要)

ここまでのバインディングは片方向。ビューの値とコンポーネントの値とを双方向に同期させるのが双方向バインディング

ルートモジュールにて FormModule を import しておく。そして、テンプレートにて ngModel ディレクティブを利用する。要素の識別のために、name属性が必須。

<element name="name" [(ngModel)]="exp" />

プロパティバインディングとイベントバインディングを組み合わせたシンタックス、と覚える。 それぞれを利用することで、双方向バインディングを実現することは可能。

第4章 標準パイプ/ディレクティブ

パイプ とはテンプレートに埋め込まれたデータを加工/整形する仕組み。 シンタックス{{ exp | somePipe [: parameter] }} で、例は {{ price | currency: 'JPY' }} 。 複数適用するには、 {{ price | currency: '\' | hoge }}

文字列操作系のパイプは uppercase, lowercase, titlecase、 など。

json パイプを利用すると、JavaScript オブジェクトを json 変換でき、デバッグ時のダンプなどに使える。(function, undefined は無視される)

他には slice, number, locale, date, i18nPlural, i18nSelect など。

ディレクティブの種類は大きく分けて三つ。 コンポーネント, 構造ディレクティブ, 属性ディレクティブ。

ngIf: 表示切り替え、スタイルの非常時と異なりツリー事態から排除する。(見た目の切り替えはstyleで制御するべき)else, then 句が使えることも覚えておく。

*ngIf のようにアスタリスクがつく。これは構造ディレクティブであることのしるし。

ngSwitch: 表示切り替え。いわゆる switch 文。

ngFor: 配列をループする。index, first, last, even, odd のような特殊変数が利用できる。 <tr *ngFor="let obj of data; index as i; first as first;"> のような形。 並列する複数要素を繰り返したい場合、<ng-container> というダミーのコンテナ要素を利用する。 trackBy 関数(ラッキング)を使うと、更新判定のキーを設定することができる。このキーが同一である限り、DOMツリー内での再生性は行われない。

@Component({
  template: `
    <li *ngFor="let b of books; trackBy: trackFn" />
  `
})
export class AppComponent {
  trackFn(index: any, book: any) {
    return book.isbn
  }
}

trackBy関数は引数として、現在のインデックスと値を受け取る。戻りとして一意の識別子を返す。

ngStyle: スタイルバインディングと異なり、複数のスタイルをまとめて設定できる。 ngClass: クラス指定。文字列、配列、オブジェクトを設定できる。

ngPlural: ngSwitch の動的バージョン。メッセージの分量が多い時は、パイプよりもディレクティブの方がすっきり表現できる。メッセージをコンポーネントで管理するならパイプでも良い。

ngTemplateOutlet: あらかじめ用意したテンプレートを、コンポーネント内の任意の場所に挿入できる。

@Component({
  template: `
    <ng-template #myTemp let-isbn="isbn" let-title="title">
      <!-- isbn と title を利用できる -->
    </ng-template>
    <ng-container *ngTemplateOutlet="myTemp; context: books[temp]"></ng-container>
  `
})
export class AppComponent {
  temp = 0;
  books = [...]
}

$implicit でデフォルトキーを指定することも可能。

ngComponentOutlet: あらかじめ用意したコンポーネントを動的にビューにインポートできる。@NgModule({ entryComponents: [ /* ここにコンポーネントをセットする必要あり */ ] })

import { OnInit } from "@angular/core"
import { ComponentA, ComponentB, ComponentC } from "some/path"

@Component({
  template: `
    <ng-container *ngComponentOutlet="banner"></ng-container>
  `
})
export class AppComponent implements OnInit {
  banner: ComponentA
  ngOnInit() {
    this.banner = /* ここで動的にコンポーネント設定するロジック */
  }
}

第5章 フォーム開発

まず、FormsModuleを "@angular/forms" から import して NgModule に含める。

<form #myForm="ngForm" (ngSubmit)="show()" novalidate /> でngFormディレクティブを変数にセットし、サブミット時に呼び出す処理をセットし、HTML5の検証処理を無効化。(Angular 4以降では、novalidate は自動付与されるので不要)

<input id="mail" name="mail" type="email" [(ngModel)]="user.mail" required email #mail="ngModel" /> name でフォーム要素識別のためのキーをセット、 [(ngModel)] でフォーム要素とプロパティの紐付け、 requiredemail が検証ルール、 #mail がフォーム要素参照のための変数宣言。

minlength, maxlength, min, max, patternなどの検証属性も存在する。

検証結果は 入力要素名.errors?.検証型 に true/false が格納されている。あるいは hasError メソッドを利用しても良い。 のように。 フォーム全体の検証エラー確認は フォーム名.invalid で取得できる。

すべての入力値デバッグ<pre>{{myForm.value|json}}</pre> とする。

その他の判定情報は name.pristine, name.dirty, name.touched, name.untouchedなど。それぞれ、入力要素が変更されたかどうか、一度でもフォーカスが当たったかどうかを判定できる。form.submitted でサブミット済かどうかを判定できる。

また、Angular は ng-valid, ng-invalid, ng-pristine, ng-dirty, ng-touched, ng-untouched, ng-submitted と言ったスタイルクラスを付与する。

ラジオボタンチェックボックス、選択ボックスの生成方法についても言及有り。

ここまでがテンプレート駆動形のフォーム、ここからは モデル駆動形(Reactive駆動形)のフォームについて。より柔軟に、複雑な要件を表現できる。

NgModule にて FormsModule ではなく ReactiveFormsModule を使う。

コンポーネントの値に初期値やバリデーションを含める。

import { FormControl, Validators } from "@angular/forms"
export class AppComponent {
  name = new FormControl(
    { value: '初期値', disabled: true },
    [ Validators.required, Validators.minLength(3) ]
  )

  onClick() {
    // リセット処理
    this.name.reset('別の初期値')
  }
}

FormBuilder と FormGroup を利用して、フォーム情報(FormControl)を一括管理。

import { FormBuilder, FormGroup } from "@angular/forms"

export class AppComponent {
  // FormGroup オブジェクトを生成
  myForm = this.builder.group({
    name: this.name,
  }),
  // FormBuilder オブジェクトを生成
  constructor(private builder: FormBuilder) { }
}

あとは、これらをテンプレート上の個々の要素と結びつける。

第6章 コンポーネント開発

複数コンポーネントの連携について。 属性として値を受け取るには、@Input デコレータを利用する。

// コンポーネント定義
@Component({
  selector: 'book-detail'
  template: `...`
})
export class DetailComponent {
  @Input() item: Book
}

// コンポーネント利用
<book-detail [item]="book"></book-detail>

またプロパティ名と属性名が異なる場合、以下のように書ける。

// コンポーネント定義
@Component({
  selector: 'book-detail'
  template: `...`
})
export class DetailComponent {
  @Input('data') item: Book
}

// コンポーネント利用
<book-detail [data]="book"></book-detail>

またプロパティではなく、@Input デコレータを付与したゲッタ・セッタメソッドを作ることも可能。値受け取しに際して、デフォルト値の設定、バリデーション、演算などを挟むことができる。

利用される側のコンポーネントは、利用する側のモジュールの declarations に登録する。

@Output デコレータを利用することで、子コンポーネントで発生したイベントを受け取ることができる。 これによって、子コンポーネントから親コンポーネントに値を渡すことができる。

// 子
export class EditComponent {
    @Input() item: Book;
    @Output() edited = new EventEmitter<Book>();
    
    onsubmit() {
      this.edited.emit(this.item)
    }
}

// 親
@Component({
  template: `<edit-book [item]="selected" (edited)="onedited($event)"></edit-book>`
})
export class AppComponent {
    onedited(book: Book) {
      // do something with book sent from child
    }
}

また、テンプレート参照変数なら子コンポーネントの情報にアクセスすることができる。 以下、先程のサンプルの一部抜粋・改変。

<p>編集中{{ edit.item?.title }}</p>
<edit-book #edit  [item]="selected" (edited)="onedited($event)"></edit-book>

コンポーネントだけではなく、モジュールの分割についても触れる。 サブモジュールでは、BrowserModule のインポートは不要。代わりに CommonModule をインポートする。CommonModule は ngIf/number など、基本的なディレクティブやパイプを提供する。 また、他のモジュール(ルートモジュール)から利用できるように、公開するコンポーネントは declarations だけではなく、exports にも含める。exports に含まないコンポーネントは、モジュール内部でのみ利用可能。

コンポーネントのライフサイクルについて。

  1. コンポーネントの生成
  2. コンストラクター
  3. ngOnChanges*: @Input 経由で入力値が(再)設定された時
  4. ngOnInit*: 入力値(@Input)が処理された後(コンポーネントの初期化時に一度だけ
  5. ngDoCheck*: 状態の変更を検出した時
  6. ngAfterContentInit: 外部コンテンツを初期化した時(最初の ngDoCheck メソッドの後で一度だけ
  7. ngAfterContentChecked: 外部コンテンツの変更をチェックした時
  8. ngAfterViewInit: 現在のコンポーネントと子コンポーネントのビューを生成した時
  9. ngAfterViewChecked: 現在のコンポーネントと子コンポーネントのビューが変更された時
  10. ngOnDestroyed*: コンポーネントが破棄される時
  11. コンポーネントの破棄

コンテンツとは <ng-conent> 要素のこと。 ビュー とは、コンポーネントで定義されたビューそのもの。

コンポーネントの初期化処理は ngOnInit に集約するのがベター。コンストラクタでは @Input の値は未準備。

ngOnChanges では、引数に SimpleChanges 型のオブジェクトを受け取る。オブジェクトには previousValue, currentValue, isFirstChange() が含まれる。 現在のビュー(テンプレート)に配置されたコンポーネントを取得するには @ViewChildren を利用する。

ngAfterViewInit/ngAfterViewChecked はビューの初期化と更新で呼ばれる。子コンポーネントとの関わりに注意。

ngAfterContentInit/ngAfterContentChecked について。 まず、 <ng-content> がルートコンポーネントでは利用できない点に注意。また、select 属性を使うと、埋め込み先を指定できる。

// 呼ぶ側
<my-content>
    <span class="header">タイトル</span>
    <small>テキスト</small>
</my-content>

// 呼ばれる側
<header>
  <ng-content select=".header"></ng-content>
</header>
<div>
  <ng-content select="small"></ng-content>
</div>

コンポーネントngAfterContentInit/ngAfterContentChecked が終わった後に、親コンポーネントngAfterContentInit/ngAfterContentChecked が走る。

コンポーネントのスタイル定義について。 @Component デコレータの styles/styleUrls で指定する。前者が文字列によるスタイルを、後者がスタイルシートのパスを期待している。 コンポーネントのスタイルはカプセル化されており、他コンポーネントセレクター衝突することはない。

コンポーネントスタイルで利用できる特殊セレクタがある。

:hostコンポーネント自身(<my-app>)などのセレクタ。また :host(.disabled) とすると、<my-app class="disabled"> を修飾できる。

:host-contextコンポーネント外部の状況に応じて、スタイルを適用する。 :host-context(.summer-themse) とすると、親コンポーネントに summer-theme クラスがある場合のスタイルを指定できる。

/deep/ セレクタエイリアス>>>)を使うと、子コンポーネントのスタイルを指定できる。例: :host /deep/ p { color: red } 。用法を間違えるとグローバルスタイルになるので注意。

アニメーションは @angular/animations が利用できる。 @Component の animations で定義し、テンプレートにて `[@name]="exp" でバインディング

@Component の templateUrl パラメータを使うことで、テンプレート定義を別ファイル化する。その際、url はコンポーネントからの相対パス

テンプレート内部に書いた <script />, <head />, <body /> などは無効化される。

第7章 サービス開発

コンポーネントはビュー(テンプレート)とのやりとりに徹し、アプリ固有のビジネスロジックはサービスに委ねるべき。

サービスクラスには、 @angular/core@Injectable を付与する。コンポーネントと異なり、 @NgModule の providers に登録する。 暗黙的に Provider クラスが生成され、指定された型(provide)をトークンにして、要求のたびに、インスタンスが生成される(useClass)

コンポーネントconstructor の引数でサービスを受け取る。これは、コンストラクタの引数型と、登録済みのサービスとを照合して、注入すべきオブジェクト(サービス)を決定する。という Angular の決まりによるもの。

依存性注入において、 useXXX プロパティを理解する。 useClass はサービスを毎回インスタンス化(new)する。 useValue は渡されたインスタンスが、複数のコンポーネントで再利用される。useAlias は定義済みの Provider へのエイリアスを作成、別名で利用可能となる。 useFactory はファクトリ関数経由でインスタンスを作成するので、処理を挟める。

InjectionToken を使うことで、クラス以外も注入することができる。 multi: true を使うことで、複数のサービスを1つのDIトークンに紐付けることができる。

ここからは内部的な高度な話題。 コンポーネントツリーと同じく、インジェクターツリーというものがある。アプリ全体で利用するサービスはルートモジュールでグローバルに登録、特定のコンポーネントで利用するサービスはコンポーネント側で登録する。

インジェクターツリーをまたいで同一トークンに対する Provider を宣言できる(Provider の再登録、オーバーライド)。ただし、可読性を損なう。

@Optional デコレータを使うと、Provider が見つからなかった場合の例外発生を防ぐことができる。

constructor(@Optional() private use: UseService) { }

viewProviders を使うと、有効範囲の異なるプライベートなサービスを実現できる。

第8章 ルーティング

Angular でルーティングを行うには、 RoutingModule を利用する。

基底パスは <base href="/"> のように指定しておく。

path, pathMatch でパス指定、component で表示するコンポーネントを指定、children で入れ子指定。 outlet が表示領域、redirectTo がリダイレクト、 canXxxx がガード。 data でルーティングにデータを引き渡し、 resolve で依存関係をマップ。

データ引き渡し: ルートパラメータ、クエリパラメータ、data プロパティ

ゾル を使うと、非同期データを取得してからコンポーネントを表示、のようなことができる。

import { Injectable } from "@angular/core"
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from "@angular/router"

import { SomeService } from "@angular/router"

@Injectable()
export class SomeResolver implements Resolve<any> {
  constructor(private some: SomeResolver) {  }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return this.some.requestGet(router.params['url'])
  }
}

作成したリゾルバは、ルーティング設定内で利用して、かつ NgModule の providers に含める。 ただし、AppModule ではなく独立したルーティングモジュールにまとめた方が見通しが良くなる。

ゾルバの戻り値は、 ActivatedRoute#data プロパティに保持されているので、コンポーネント側からこれにアクセスする。

また、表示領域の <router-outlet> は複数使用が可能。

ガード 機能を使うと、遷移の可否判定ができる。CanActivate, CanActivateChild, CanDeactivate, CanLoad など。

以下は日付の応じてアクセスの可否を判定するガード。

import { Injectable } from '@angular/core'
import { CanActivate, ActivateRouteSnapshot, RouterStateSnapshot } from '@angular/router'
import { Observable } from 'rxjs/Observable'

@Injectable()
export class TimeGuard implements CanActivate {
  canActivate(route: Activate, state: RouterStateSnapshot) {
     let limit = new Date(2017, 4, 30)
    let current = new Date()
    if(limit.getTime() > current.getTime()) {
      return true
    }  else {
      window.alert('期限切れ')
      return false
    }
  }
}

第9章 パイプ/ディレクティブの自作

パイプの自作。

クラスに @Pipe デコレータを付与し、セレクタを定義。クラスメソッドとして transform を実装する。PipeTransform インターフェースの実装は任意。 transform の第2引数以降で、パラメータを指定可能。パラメータには関数を渡すこともできる。

パイプには pure/impure という概念がある。pure なパイプでは、オブジェクト型の再評価に気をつける。参照が変わらなければ、新しい値がビューに適用されない。

@Pipe({ name: 'some', pure: false }) で、 impure なパイプを定義することができる。

ただし、impure なパイプはパフォーマンスに良くないので、濫用を避ける。なるべく pure なパイプを使用し、シンプルな実装に留めること。複雑になってきたらサービスにロジックを移譲する。

ディレクティブの自作。

ビューの操作/生成部分は自作ディレクティブとして切り出す。

import { Directive, ElementRef } from '@angular/core'

@Directive({
  selector: '[myColored]'
})
export class ColoredDirective {
  constructor() {
    el.nativeElement.style.backgroundColor = '#cff'
  }
}

作成したディレクティブは、モジュールの delarations に登録する。

ディレクティブには属性値を渡すことも可能。コンポーネントとお内↑う @Input デコレータを利用する。

import { Directive, ElementRef, Input, OnInit } from '@angular/core'

@Directive({
  selector: '[myColored]'
})
export class ColoredDirective implements OnInit {
  @Input() myBgcolor = '#cff'

  constructor(private el: ElementRef) {}

  ngOnInit() {
    this.el.nativeElement.style.backgroundColor = this.myBgcolor
  }
}

ディレクティブには、イベント処理をもたせることも可能。

import { Directive, ElementRef, HostListener, Input } from '@angular/core'

@Directive({
  selector: '[myColored]'
})
export class ColoredDirective {
  @Input('myColored') color = '#ffc'

  constructor(private el: ElementRef) {}

  @HostListener('mouseenter') onmouseenter() {
    this.el.nativeElement.style.backgroundColor = this.color
  }

  @HostListener('mouseleave') onmouseleave() {
    this.el.nativeElement.style.backgroundColor = ''
  }
}

イベントハンドラに値を引き渡す時は、@HostListener('mouseenter', ['$event.target']) onmouseenter(span: any) {} とする。

例えば、ngModel を受け取って検証をかけるディレクティブも作成可能。 Validator インターフェースを実装し、 validate メソッドで定義する。

構造ディレクティブについて。

<div *ngIf="true">foo</div><ng-template [ngIf]="true"><div>foo</div></ng-template>シンタックスシュガー。

構造ディレクティブとは、 テンプレート化された要素をもとにコンテンツを生成&ページに反映させる 機能を持ったディレクティブ。

第10章 テスト

(引用省略)

第11章 関連ライブラリ/ツール

ngx-bootstrap, ng2-validation, ng-xi18n など、よく使われがちなライブラリを紹介。

Angular CLI や AOT コンパイラに関する説明有り。

書籍リンク

Amazon

楽天ブックス