AngularのForm(Reactive Forms)メモ

ryotah.hatenablog.com 別記事でこんなのも書きました。


(Angular v.4.3.6を利用)

Reactive Formとは

Template-driven Formsとの比較

  • Template-driven Forms
    • ngModelのようなディレクティブを利用して、フォーム要素とデータモデルを紐づける。(FormControlは自動で生成されている。)入力内容が変更されるとミュータブルなデータモデルが更新される。
  • Reactive Form
    • 最初にFormControlのツリーを生成する。フォームとの紐付けはformControlNameプロパティなどでおこなう。

Template-driven Forms

<form #heroForm="ngForm">
  <input type="text" [(ngModel)]="model.name" name="name">
</form>

Reactive Forms

<form [formGroup]="heroForm">
  <input formControlName="name">
</form>
import { FormControl, FormGroup } from '@angular/forms';
...
heroForm = new FormGroup({
  name: new FormControl()
});

主要4クラス

  • AbstractControl
    • 基本クラス。他3クラスはこの抽象クラスをextendsしている。
  • FormControl
    • 個々のフォーム要素(<input>など)の値と有効性の状態を追跡
  • FormGroup
    • AbstractControlのグループの値と有効性の状態を追跡
  • FormArray
    • AbstractControlの配列の値と有効性の状態を追跡

FormGroup, FormArrayを利用した例

f:id:ryotah:20170831210750p:plain

<form [formGroup]="heroForm">
  <div>
    <label>Name:
      <input formControlName="name">
    </label>
  </div>

  <!-- アドレスの数は可変 -->
  <div formArrayName="addresses">
    <div *ngFor="let address of addresses.controls; let i=index" [formGroupName]="i">
      <div><b>Address {{i  + 1}}</b></div>
      <label>City:
        <input formControlName="city">
      </label>
    </div>
  </div>
  <button (click)="addAddress()">add</button>
</form>
<pre>Form value: {{ heroForm.value | json }}</pre>
<pre>Form status: {{ heroForm.status | json }}</pre>
heroForm: FormGroup;
constructor(private fb: FormBuilder) {

  // `FormBuilder`を利用した`FormGroup`の生成
  // https://angular.io/guide/reactive-forms#introduction-to-formbuilder
  this.heroForm = this.fb.group({
    name: '',
    addresses: this.fb.array([]),
  });
}

addAddress() {
  this.addresses.push(this.fb.group({ city: '' }));
}

get addresses(): FormArray {

  // 個々の`FormControl`, `FormGroup`, `FormArray`は `get`で取得する
  // https://angular.io/guide/reactive-forms#inspect-formcontrol-properties
  return this.heroForm.get('addresses') as FormArray;
}

フォームの値を変更する

フォームの変更を監視する

const nameControl = this.heroForm.get('name');
nameControl.valueChanges.forEach(
  (value: string) => console.log(value)
);

データモデルとフォームモデル

  • データモデルを使いフォームモデル(FormControl)を作成
  • ユーザー操作の結果、更新されるのはフォームモデル
  • Submitなどのタイミングでフォームモデルから送信用のデータモデルを作成
  • データモデルとフォームモデルの構造は一緒である必要はない(当然、似ている方が楽ではあるが)

バリデーション

(あとで追記)

AngularのForm(Template-driven Forms)メモ

(Angular v.4.3.6を利用)

<select>を利用する

  • *ngForを利用する
    • AngularJSのng-optionsのようなものはない
<select [(ngModel)]="model.power" name="power">
  <option *ngFor="let pow of powers" [value]="pow">{{pow}}</option>
</select>

NgFormとは

<form #heroForm="ngForm">

ngModelとname属性

  • ngModelを利用すると、自動的にFormControlインスタンスが生成される
  • FormControlインスタンスはname属性に割り当てられた名前でNgFormに登録される

NgModelインスタンスを取得

  • template reference variableを利用する
<input type="text" [(ngModel)]="model.name" name="name" #name="ngModel">

参考

Directive({
  selector: '[ngModel]:not([formControlName]):not([formControl])',
  providers: [formControlBinding],
  exportAs: 'ngModel'
})

バリデーション

<input>関連その他

参考

<input>操作時のイベントを取得したい

<input (keyup)="onKey($event)">
  • $eventオブジェクトのプロパティはDOMイベントのタイプによって変わる

値の確認

onKey(event: KeyboardEvent) {
  console.log((<HTMLInputElement>event.target).value);
}

$eventを利用せずに値を確認

(DOMイベント全体を渡さずに、値だけを渡したい。)

<!-- template reference variables を利用 -->
<input #box (keyup)="onKey(box.value)">

キーイベントをフィルタリングしたい

<!-- Enterキーに反応 -->
<input #box (keyup.enter)="onEnter(box.value)">

Angular Routerメモ

(Angular v.4.3.6を利用)

参考

リンクを設定したい

通常

<a routerLink="/path">link</a>

<!-- relative routerLink -->
<a routerLink="./path">link</a>

パラメータがついたリンク

<!-- route param (/path/1) -->
<a [routerLink]="['/path', 1 ]">link</a>

<!-- matrix param (/path;matrixParam=value) -->
<a [routerLink]="['/path', { matrixParam: 'value' } ]">link</a>

<!-- query param (/path?page=1) -->
<a [routerLink]="['/path']" [queryParams]="{ page: 1 }">link</a>

アンカー(#)がついたリンク

<!-- fragment (/path#anchor) -->
<a [routerLink]="['/path']" fragment="anchor">link</a>

アクティブなリンクにクラスを設定したい

  • routerLinkActiveを利用
<a routerLink="/path" routerLinkActive="active">link</a>

<a routerLink="/path" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">link</a>

<a routerLink="/path" [routerLinkActive]="['active', 'border']">link</a>

ts内で遷移の制御をしたい

通常

this.router.navigate(['/path']);

// relative link
this.router.navigate(['path'], { relativeTo: this.route });

パラメータをつける

// route param (/path/1)
this.router.navigate(['path', '1'], { relativeTo: this.route });

// matrix param (/path;matrixParam=value)
this.router.navigate(['/path',  { matrixParam: 'value' }]);

// query param (/path?page=1)
this.router.navigate(['/path'], { queryParams: { page: 1 } });

アンカー(#)をつける

// fragment (/path#anchor)
this.router.navigate(['/path'], { fragment: 'anchor' });

クエリを保存して遷移したい

参考

コンポーネント内でパラメータを取得したい

Observableで取得

constructor(private route: ActivatedRoute) {}

ngOnInit() {
  /**
   * Routeの設定が { path: 'view/:id' }
   * URLが /view/10;matrixParam=value?page=1 の場合
   */
  this.route.paramMap
    .subscribe((params: ParamMap) => {
      // params.keys.forEach(key => {

        const routeParam = params.get('id');
        // -> 10

        const matrixParam = params.get('matrixParam');
        // -> value
      // });
    });

  this.route.queryParamMap
    .subscribe((params: ParamMap) => {
      const queryParam = this.route.snapshot.queryParamMap.get('page');
      // -> 1
    });
}

route.snapshotから取得

constructor(private route: ActivatedRoute) {}

// ...

/**
 * Routeの設定が { path: 'view/:id' }
 * URLが /view/10;matrixParam=value?page=1 の場合
 */
const routeParam = this.route.snapshot.paramMap.get('id');
// -> 10

const matrixParam = this.route.snapshot.paramMap.get('matrixParam');
// -> value

const queryParam = this.route.snapshot.queryParamMap.get('page');
// -> 1

特定のRouteに紐付けないパラメータを利用したい

  • クエリパラメータを利用
    • https://angular.io/guide/router#query-parameters-and-fragments
      • In the route parameters example, you only dealt with parameters specific to the route, but what if you wanted optional parameters available to all routes? This is where query parameters come into play.

        ルートパラメータの例では、ルートに固有のパラメータのみを扱いますが、オプションのパラメータをすべてのルートで使用できるようにしたい場合はどうなりますか?ここでクエリパラメータが有効になります。

前回のURLを取得したい

アプリ初期時に処理を追加したい

  • (Routerとは直接関係ないけど)
  • APP_INITIALIZERを利用
  • 設定ファイルのロードや権限確認などに利用できそう
@NgModule({
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: (sites:SitesService) => () => sites.load(),
      deps: [SitesService],
      multi: true,
    }
  ]
})
export class AppModule { }
@Injectable()
export class SitesService {
  constructor() { }
  load(): Promise<any> {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        // doing something
        // ...

        resolve();
      }, 3000);
    });
  }
}

参考

Routerのナビゲーションイベントを確認したい

constructor(private router: Router) {

    // 遷移開始時のイベントを確認
    router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        console.log(event);

        // リダイレクトさせることも(一応)可能
        // if (event.url !== '/view-a') {
        //   router.navigate(['view-b']);
        // }
      }
    });
  }

その他

  • イベントのログをコンソールで確認(デバッグ用)
    • RouterModule.forRoot(appRoutes, { enableTracing: true })

コンポーネントの再利用について知りたい

  • routeConfigが同じならコンポーネントは再利用される
    • パラメータが変わっても再描画はされない
// https://github.com/angular/angular/blob/54e02449549448ebab6f255f2da0b4396665c6f0/packages/router/src/route_reuse_strategy.ts#L66
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
  return future.routeConfig === curr.routeConfig;
}

カスタマイズしたい

参考

RouterとActivatedRouteについて知りたい

その他

複数のRouteを表示したい

<router-outlet></router-outlet>

<!--popupという名前のoutletを設定 -->
<router-outlet name="popup"></router-outlet>

<!-- `path: 'compose'`に紐づけられているコンポーネントを表示 -->
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
// `path: 'compose'`に紐づけられているコンポーネントを表示
this.router.navigate([{ outlets: { popup: 'compose' } }]);

// null でクリアできる
this.router.navigate([{ outlets: { popup: null }}]);

「遷移前に認証チェックをしたい」「遷移前に未保存のデータがあるか確認をしたい」など

遷移前にデータを読み込みたい

遅延読み込み

基本設定

読み込み開始前にガードしたい

プリロードさせたい

RouterModule.forRoot(
  appRoutes,
  {
    // PreloadAllModules
    //   https://angular.io/api/router/PreloadAllModules
    //   guardされていないrouteが全てプリロードされる
    preloadingStrategy: PreloadAllModules
  }
)
@Injectable()
export class CustomPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {

    // routeの設定情報に `data: { preload: true }` があればプリロードする
    if (route.data && route.data['preload']) {
      return load();
    } else {
      return Observable.of(null);
    }
  }
}

Angular CLIでビルドされたファイルの容量を把握する

把握

  1. --stats-jsonオプションを有効にしてstats.jsonを出力
  2. webpack-bundle-analyzerを利用

–build-optimizer

容量の把握の話とは関係ないですが、Angular CLI 1.3から--build-optimizerが導入されています。

RxJSの各種オペレーターなどをimportする場合

全部をimportしない

// 禁止
import 'rxjs/Rx';

ライブラリ全体が読み込まれてしまいます。
TSLintでエラーが出るようにしておくと安全です。

Rule: import-blacklist

"import-blacklist": [
  true,
  "rxjs",
  "rxjs/Rx",
]

import用のファイルを用意する

rxjs-add.ts

import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/forkJoin';
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/throw';
// ...

import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/concatMap';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/merge';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/startWith';
// ...

app.module.ts

import './rxjs-add';

angular-cliユニットテスト対応

テスト起動ファイル(test.ts)にimport './app/rxjs-add';など記述するのを忘れないように

Angular CLIでメインアプリとは別にページ(html)を用意する

stories asset configuration · angular/angular-cli Wiki · GitHub

assetsに、対象のhtmlかそれを含むディレクトリを設定する。

"assets": [
  "assets",
  "favicon.ico",
  "static/test.html"
]

これでhttp://localhost:4200/static/test.htmlが利用できる。

単純なhtmlページではなく複数のAngularアプリケーションが必要な場合は以下の方法で。

stories multiple apps · angular/angular-cli Wiki · GitHub

AngularのDatePipeのlocaleを変更したい場合(タイムゾーンを変更したい場合、も)

(Angular v.4.3.6を利用)

起動時にlocaleを設定 (JIT)

@NgModule({
  imports: [
    BrowserModule,
  ],
  declarations: [
    AppComponent
  ],
  providers: [
    {
      provide: LOCALE_ID,
      useValue: navigator.language
      // useValue: 'ja'
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

動的にlocaleを変更

@Pipe({
  name: 'date'
})
export class DatePipeProxy implements PipeTransform {
  constructor(private localizationService: LocalizationService) {
  }
  public transform(value: any, pattern: string = 'mediumDate'): any {
    const ngPipe = new DatePipe(this.localizationService.locale);
    return ngPipe.transform(value, pattern);
  }
}

タイムゾーンを変更

  • 直接Intl.DateTimeFormatを利用するPipeをつくる
    • DateTimeFormatOptionstimeZoneを設定
    • DatePipeの内部でもDateTimeFormatを利用しているが、現状だとtimeZoneを変更するAPIはない

参考Url