RxJSの各種オペレーターなどをimportする場合
全部をimportしない
// 禁止 import 'rxjs/Rx';
ライブラリ全体が読み込まれてしまいます。
TSLintでエラーが出るようにしておくと安全です。
"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アプリケーションが必要な場合は以下の方法で。
AngularのDatePipeのlocaleを変更したい場合(タイムゾーンを変更したい場合、も)
(Angular v.4.3.6を利用)
起動時にlocaleを設定 (JIT)
LOCALE_ID
を設定
@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をつくるDateTimeFormatOptions
のtimeZone
を設定- DatePipeの内部でも
DateTimeFormat
を利用しているが、現状だとtimeZone
を変更するAPIはない
参考Url
カレンダー生成
生成の流れ
- 対象月の日数を調べる
- カレンダー上で、1日の前に何日(何マス)あるか
- カレンダー上で、最終日の後ろに何日(何マス)あるか
- カレンダーに表示される日数を調べる
- 前月、翌月も含んだ数
- 日数分の配列を生成
- 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を利用)
- これは何か
- 概要
- Built-in
- カスタムパイプ
- Change detection
- Impure pipes
- AsyncPipe
- ServiceやComponent内でも利用したい
- DatePipeのlocaleを変更したい場合
- その他
これは何か
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 pipeはpure 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
- Built-in pipe
- impure pipeの参考になる
Promise
やObservable
を入力値として受け取る- 詳細はコードを見た方がはやい
ServiceやComponent内でも利用したい
- Angular 2/4 use pipes in services and components - Stack Overflow
- DIを利用する
- providersに登録する必要がある
import { DatePipe } from '@angular/common'; class MyService { constructor(private datePipe: DatePipe) {} transformDate(date) { this.datePipe.transform(myDate, 'yyyy-MM-dd'); } }
DatePipeのlocaleを変更したい場合
その他
- Pipeをつなげることも可能
{{ birthday | date | uppercase}}
- AngularJSに実装されていた
filter
,orderBy
などがない理由- https://angular.io/guide/pipes#appendix-no-filterpipe-or-orderbypipe
- パフォーマンスの問題
- Minifyされたコードで実行保証ができないことがあるため
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
テスト
削除したリモートブランチがローカルで表示される場合
# リモートのブランチを表示 git branch --remote # リモートに存在しない(削除された)ブランチを削除 git fetch --prune