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)
}

AngularのPipe

(Angular v.4.3.6を利用)

これは何か

Angular (4+)を始めたのでドキュメントを読んでいます。今回はPipeについて。
内容は以下ドキュメントからの抜粋のようなものです。

概要

  • PipeはHtmlテンプレート内でデータを意図した形式に変換する仕組み
<p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p>

Built-in

カスタムパイプ

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'custom'
})
export class CustomPipe implements PipeTransform {

  transform(value: any, args?: any): any {
    return null;
  }

}
  • Style Guide的にはShared feature moduleに入れる

Change detection

  • Pipeは通常のChange detectionと仕様が異なる
  • 例えば、配列に要素を追加しただけではPipeは実行されない
    • 参照を変更する必要がある。新しい配列で置き換えるた場合、Pipeは実行される
<!-- heroが追加されれば新しいhero.nameが表示される -->
<div *ngFor="let hero of heroes">
  {{hero.name}}
</div>

<!-- heroが追加されても表示は変わらない -->
<div *ngFor="let hero of (heroes | fooPipe)">
  {{hero.name}}
</div>

上記のような場合の対応策

  • impure pipeにする
  • Pipeを利用しないでComponent内で処理をする
    • The Angular team and many experienced Angular developers strongly recommend moving filtering and sorting logic into the component itself

Impure pipes

  • pure: falseにする
@Pipe({
  name: 'fooImpure',
  pure: false
})
export class FooImpurePipe extends FooPipe { }
  • pure pipepure changeを見つけたときに実行される
    • A pure change is either a change to a primitive input value (String, Number, Boolean, Symbol) or a changed object reference (Date, Array, Function, Object).

  • impure pipeの場合
    • Angular executes an impure pipe during every component change detection cycle. An impure pipe is called often, as often as every keystroke or mouse-move.

AsyncPipe

ServiceやComponent内でも利用したい

import { DatePipe } from '@angular/common';
class MyService {

  constructor(private datePipe: DatePipe) {}

  transformDate(date) {
    this.datePipe.transform(myDate, 'yyyy-MM-dd');
  }
}

DatePipeのlocaleを変更したい場合

その他

AngularのHttpClientメモ

(Angular v.4.3.6を利用)

参考

データを取得する

import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';

export interface Book {
  id?: number;
  title: string;
  author: string;
}

const url = 'http://localhost:3000';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  book: Book;

  constructor(
    private http: HttpClient
  ) { }

  ngOnInit(): void {
  }

  get(id: number) {
    this.http.get<Book>(`${url}/posts/${id}`)
      .subscribe(data => {
        this.book = data;
      }, (err: HttpErrorResponse) => {

        // エラー処理
        if (err.error instanceof Error) {

          // A client-side or network error occurred. Handle it accordingly.
          console.log('An error occurred:', err.error.message);
        } else {

          // The backend returned an unsuccessful response code.
          // The response body may contain clues as to what went wrong,
          console.log(`Backend returned code ${err.status}, body was: ${err.error}`);
        }
      });
  }
}

レスポンスデータの詳細を見る

this.http
  .get<Book>(`${url}/posts/1`, { observe: 'response' })
  .subscribe(resp => {
    console.log(resp);
  });

retry

RxJSにretryというオペレータがある。

import 'rxjs/add/operator/retry';
this.http
  .get<Book>(`${url}/posts/${id}`)

  // Retry this request up to 3 times.
  .retry(3)

  // Any errors after the 3rd retry will fall through to the app.
  .subscribe(...);

データを送信する

const body: Book = {
  title: 'foo',
  author: 'bar'
};
this.http
  .post<Book>(`${url}/posts`, body)
  .subscribe(resp => {
    this.book = resp;
  });

Headersを追加

this.http
  .post(`${url}/posts`, body, {
    headers: new HttpHeaders().set('Authorization', 'my-auth-token'),
  })
  // ...

URL Parametersを追加

this.http
  .post(`${url}/posts`, body, {
    params: new HttpParams().set('param', 'value')
  })
  // ...

進捗を確認

const body: Book = {
  title: 'foo' + new Date().valueOf(),
  author: 'bar' + new Date().valueOf()
};
const req = new HttpRequest('POST', `${url}/posts`, body, {
  reportProgress: true,
});

this.http.request(req).subscribe(event => {

  // Via this API, you get access to the raw event stream.
  // Look for upload progress events.
  if (event.type === HttpEventType.UploadProgress) {

    // This is an upload progress event. Compute and show the % done:
    const percentDone = Math.round(100 * event.loaded / event.total);
    console.log(`File is ${percentDone}% uploaded.`);
  } else if (event instanceof HttpResponse) {
    console.log('File is completely uploaded!');
  }
});

https://angular.io/guide/http#listening-to-progress-events

応用

Interceptorサービスを登録して、リクエスト・レスポンスに処理を追加できる。

import {NgModule} from '@angular/core';
import {HTTP_INTERCEPTORS} from '@angular/common/http';

@NgModule({
  // Interceptorは複数登録可能
  // 登録順に処理される
  providers: [{
    provide: HTTP_INTERCEPTORS,
    useClass: NoopInterceptor,
    multi: true,
  }],
})
export class AppModule {}

Interceptorを利用してヘッダーを追加

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authHeader = this.auth.getAuthorizationHeader();

    // HttpRequestはイミュータブル(直接、値を変更できない)
    const authReq = req.clone({ headers: req.headers.set('Authorization', authHeader) });
    return next.handle(authReq);
  }
}

Interceptorを利用してログを表示

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  const started = Date.now();
  return next
    .handle(req)

    // Rxのstreamに影響を与えずに処理を追加
    .do(event => {
      if (event instanceof HttpResponse) {
        const elapsed = Date.now() - started;

        // 経過時間を表示
        console.log(`Request for ${req.urlWithParams} took ${elapsed} ms.`);
      }
    });
}

https://angular.io/guide/http#logging

Interceptorを利用してキャッシュ機能を実装

constructor(private cache: HttpCache) { }

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

  if (req.method !== 'GET') {
    return next.handle(req);
  }

  const cachedResponse = this.cache.get(req);
  if (cachedResponse) {
    return Observable.of(cachedResponse);
  }

  // キャッシュがなかった場合
  return next.handle(req).do(event => {
    // Remember, there may be other events besides just the response.
    if (event instanceof HttpResponse) {
      this.cache.put(req, event);
    }
  });
}
/**
 * 以前にキャッシュされていれば、2つのResponseを返す
 */
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  if (req.method !== 'GET') {
    return next.handle(req);
  }

  // This will be an Observable of the cached value if there is one,
  // or an empty Observable otherwise. It starts out empty.
  let maybeCachedResponse: Observable<HttpEvent<any>> = Observable.empty();

  const cachedResponse = this.cache.get(req);
  if (cachedResponse) {
    maybeCachedResponse = Observable.of(cachedResponse);
  }

  const networkResponse = next.handle(req).do(event => {
    if (event instanceof HttpResponse) {
      this.cache.put(req, event);
    }
  });

  return Observable.concat(maybeCachedResponse, networkResponse);
}}

https://angular.io/guide/http#caching

XSRF

テスト