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

カレンダー生成

生成の流れ

  1. 対象月の日数を調べる
  2. カレンダー上で、1日の前に何日(何マス)あるか
  3. カレンダー上で、最終日の後ろに何日(何マス)あるか
  4. カレンダーに表示される日数を調べる
    • 前月、翌月も含んだ数
  5. 日数分の配列を生成
  6. chunkする
    • 最終的に[Array(7), Array(7), Array(7), Array(7), Array(7)]のような配列になる

サンプル

週の開始を変更したい場合

import chunk from 'lodash/fp/chunk';
import compose from 'lodash/fp/compose';
import map from 'lodash/fp/map';
import range from 'lodash/fp/range';
import * as moment from 'moment';

const DAYS_PER_WEEK = 7;

export function createCalendar(
  m = moment(),

  // 週の最初の曜日
  // default(0)は日曜
  startDayOfWeek = 0) {

  // 指定した月の1日
  const startOfMonth: moment.Moment = m.clone().startOf('month');

  // 月の日数
  const daysInMonth: number = m.clone().endOf('month').date();

  // カレンダー上で、1日の前に何日(何マス)あるか
  const daysBefore: number = getDaysBefore(startOfMonth, startDayOfWeek);

  // カレンダー上で、最終日の後ろに何日(何マス)あるか
  const daysAfter: number = getDaysAfter(daysBefore, daysInMonth);

  // カレンダーに表示する日数分の配列を生成
  const diffs: number[] = getDiffs(daysBefore, daysInMonth, daysAfter);

  return compose(
    chunk(DAYS_PER_WEEK),
    map((diff) => startOfMonth.clone().add(diff, 'd'))
  )(diffs);
}

function getDaysBefore(startOfMonth: moment.Moment, startDayOfWeek: number): number {
  const dayOfWeek = startOfMonth.day();
  return (dayOfWeek + DAYS_PER_WEEK - startDayOfWeek) % DAYS_PER_WEEK;
  // if (dayOfWeek > startDayOfWeek) {
  //   return dayOfWeek - startDayOfWeek;
  // } else {
  //   return dayOfWeek + DAYS_PER_WEEK - startDayOfWeek;
  // }
}

function getDaysAfter(daysBefore: number, daysInMonth: number): number {
  return DAYS_PER_WEEK - (daysBefore + daysInMonth) % DAYS_PER_WEEK;
}

function getDiffs(daysBefore, daysInMonth, daysAfter): number[] {
  return range((daysBefore * -1), daysInMonth + daysAfter)
}