AngularのForm(応用)
これは何か
AngularConnect 2017のセッション「Angular Forms - Advanced Topics」を自分用にまとめたものです。 セッションではCustom Form Controls、ControlValueAccessor、Formを入れ子にする場合のベストプラクティスなどについて話されています。
Angular v5.0.2で動作確認しています。
- スライド
- 動画
- セッション一覧
Custom Form Controls
- 自作のフォームパーツをつくる
ControlValueAccessor
を利用する- バリデーションが必要な場合は
Validator
も利用する- 外部から(利用するテンプレートから)
required
など既存のバリデーションを付与することも可能だが、コンポーネントの機能として必須な条件があれば、内部で定義した方がいい
- 外部から(利用するテンプレートから)
NG_VALUE_ACCESSOR
を利用する
@Component({ // ... providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: FooComponent, multi: true }, { provide: NG_VALIDATORS, useExisting: FooComponent, multi: true } ] }) export class FooComponent implements ControlValueAccessor, Validator { // ControlValueAccessor writeValue(obj: any) { } registerOnChange(fn: any) { } registerOnTouched(fn: any) { } setDisabledState(disabled: boolean) { } // Validator validate(ctrl: AbstractControl) { } }
NgControlを利用する
- コンポーネント内でcontrolにアクセスしたい場合(control.errorsを利用したい)
[control]=dir.control #dir="ngModel"
や[control]=form.get('foo') formControlName="foo"
でもできるが、毎回書くのは煩わしいNgModel
やFormControlName
は利用しないでNgControl
を利用する(Template-driven、Reactive両方で利用できるようにするため)
provide: ...
は不要になる- @Selfにする意味
export class FooNgControlComponent implements ControlValueAccessor { constructor(@Self() public controlDir: NgControl) { controlDir.valueAccessor = this; } ngOnInit() { const control = this.controlDir.control; const validators = control.validator ? [control.validator, Validators.required] : Validators.required; control.setValidators(validators); control.updateValueAndValidity(); } // ControlValueAccessor writeValue(obj: any) { } registerOnChange(fn: any) { } registerOnTouched(fn: any) { } setDisabledState(disabled: boolean) { } }
サンプルコード
- angular-form-custom-control-foms - StackBlitz
RequiredTextComponent
とRequiredTextNgControlComponent
- それぞれ、Template-drivenとReactive、両方から利用
メモ
- ControlValueAccessor
- writeValue
- Viewに値を反映するメソッド
- registerOnChange
- UI上でcontrolの値が変更されたときに呼ばれるコールバックを登録
- registerOnTouched
- setDisabledState
- writeValue
- Validator
- NgControl
FormControl
をDOMにバインドする- サブクラス
フォームの入れ子
ControlValueAccessorを利用
FormGroup
の値を利用して、データの受け渡しをする- Template-drivenとReactive両方で利用できる
@Component({ selector: 'app-address', template: ` <div [formGroup]="form"> <input type="text" formControlName="street"> <input type="text" formControlName="city"> </div>`, providers: [ { provide: NG_VALUE_ACCESSOR, /* ... */ }, { provide: NG_VALIDATORS, /* ... */ }, ] }) export class AddressComponent implements OnInit, ControlValueAccessor { form: FormGroup; constructor(private fb: FormBuilder) { } ngOnInit() { this.form = this.fb.group({ street: '', city: '' }); } // ControlValueAccessor writeValue(obj: any) { if (obj) { this.form.setValue(obj, { emitEvent: false }); } } registerOnChange(fn: any) { this.form.valueChanges.subscribe(fn); } registerOnTouched(fn: any) { } }
Template-driven専用
- シンプルですぐ実装可能
ngModelGroup
を利用するためにControlContainer
をinjectする- viewProvidersを利用する
@Component({ selector: 'app-address-template', template: ` <div ngModelGroup="address"> <input type="text" name="street" ngModel> <input type="text" name="city" ngModel> </div> `, viewProviders: [ { provide: ControlContainer, useExisting: NgForm } ] }) export class AddressTemplateComponent { }
Reactive専用
- シンプルですぐ実装可能
- FormGroupに子コンポーネントが自分でコントローラを登録する
- ちょっと奇妙な書き方に感じる
@Component({ selector: 'app-address-reactive', template: ` <div formGroupName="address"> <input type="text" formControlName="street"> <input type="text" formControlName="city"> </div> `, viewProviders: [ { provide: ControlContainer, useExisting: FormGroupDirective } ] }) export class AddressReactiveComponent { constructor(private parent: FormGroupDirective) { } ngOnInit() { // FormGroupを取得 const form = this.parent.form; // 自身で、親のFormGroupにコントローラを登録する form.addControl('address', new FormGroup({ street: new FormControl(''), city: new FormControl(''), })); } }
サンプルコード
- angular-form-nested-forms - StackBlitz
AddressComponent
ControlValueAccessor
を利用- Template-driven、Reactive両方で利用可能
AddressTemplateComponent
- Template-drivenで利用可能
AddressReactiveComponent
- Reactive両方で利用可能
メモ
- ControlContainer
- 複数の
NgControl
を含むディレクティブ - サブクラス
- AbstractFormGroupDirective
This is a base class for code shared between NgModelGroup and FormGroupName.
- NgModelGroup
- FormGroupName
- NgForm
- FormGroupDirective
- FormArrayName
- AbstractFormGroupDirective
- https://docs.google.com/presentation/d/e/2PACX-1vTS20UdnMGqA3ecrv7ww_7CDKQM8VgdH2tbHl94aXgEsYQ2cyjq62ydU3e3ZF_BaQ64kMyQa0INe2oI/pub?start=false&loop=false&delayms=3000&slide=id.g293d7d2b9d_1_1473
- 複数の
Error Aggregator
- フォーム内の各コントローラのエラーを集約して表示させたい場合
- formをprojectionするのはさせよう
- Template-driven、Reactive、どちらかでしか利用できないような設計になる
- 難しくなるし、バグが発生しやすくなる
- もうすこし良い設計があるはず
@Component({ selector: 'error-aggregator', template: ` <div *ngFor="let error of errors">{{error | json}}</div>`, }) export class ErrorAggregatorComponent implements OnInit { @Input() form: FormGroup; errors; constructor() { } ngOnInit() { // NgFormを間違って渡していないか確認 if (this.form instanceof FormGroup) { this.form.statusChanges.subscribe(val => { this.errors = this.reduceErrors(); }); } } private reduceErrors() { return pickErrors(this.form); } } // for demo function pickErrors(formGroup: FormGroup) { debugger; const keys = Object.keys(formGroup.controls); return keys.map(key => { const control = formGroup.controls[key]; if (control instanceof FormControl) { return control.errors; } else if (control instanceof FormGroup) { return pickErrors(control); } }); }
サンプルコード
参考
- AngularのForm(Template-driven Forms)メモ - ryotah’s blog
- angular-form-basic-template-driven - StackBlitz
- Template-drivenサンプル
- 基本的なバリデーション、入れ子構造フォーム
- angular-form-basic-template-driven - StackBlitz
- AngularのForm(Reactive Forms)メモ - ryotah’s blog
- angular-form-basic-reactive - StackBlitz
- Reactiveサンプル
- 基本的なバリデーション、入れ子構造フォーム
- angular-form-basic-reactive - StackBlitz
- Custom Form Controls in Angular by thoughtram
ControlValueAccessor
- Angular Forms in Depth – nrwl
メモ
- 以前のコード
// ... @Input() model?: string; @Input() form: NgForm; @ViewChild('ngModel') ngModel: NgModel; ngOnInit() { this.form.addControl(this.ngModel); } // ...
10月メモ・リンク集
これは何か
- 10月に調べたことのメモ
- 基本形式は、「参考URLとそれに関するメモとコメント」
ngrx関連
ngrxとフォーム
- FormControl Freaks: Redux Edition - Daniel Figueiredo Caetano & Renee Vrantsidis - YouTube
- Reduxを使ったフォームの構築
- Template-driven Formsを利用
- ngrxでも考えは一緒
- validはデータとして持つのではなく、selectorで
- formcontrol-freaks/chapter.md at master · renvrant/formcontrol-freaks · GitHub
- スライド版
- Reduxを使ったフォームの構築
Introducing @ngrx/entity – @ngrx – Medium
- 新パッケージ
entity
に関して - platform/docs/entity at master · ngrx/platform · GitHub
状態のリセット
- angular - How to reset all states of ngrx/store? - Stack Overflow
- 各 feature moduleで
metaReducers
を利用してリセット(初期値)に戻す方法
// foo.module.ts ... StoreModule.forFeature( 'foo', fromCalendar.reducers, { metaReducers: [fromFoo.resetReducer] } ), ... // reducers/index.ts export function resetReducer(reducer: ActionReducer<FooState, reset.Actions>) { return function (state, action) { if (action.type === RESET) { return reducer(undefined, action); } else { return reducer(state, action); } }; }
How to share state between reducers best practice · Issue #159 · ngrx/example-app · GitHub
ActionReducerMap
とcombineReducers
- Beyond combineReducers · Redux
- Reduxにおけるreducer分割とcombineReducersについて - Qiita
Rx関連
unsubscribe
- RxJS: Don’t Unsubscribe – Ben Lesh – Medium
- Unsubscribe処理を必要以上に実行しないですむようなRxの書き方について
takeUntil
,takeWhile
など
- Angular/RxJs When should I unsubscribe from
Subscription
- Stack Overflow- Angularのコンポーネント内で作成したsubscriptionをどのようにunsubscribeするか
ngOnDestroy
,Subject
,takeUntil
を効果的に利用する
その他
- Observable.throw()が返すオブジェクトの型はErrorObservable - ryotah’s blog
- 「テキスト入力 -> 自動で検索開始」を実現するためのRx使い方 - ryotah’s blog
- Http - Observable completed function not firing · Issue #7865 · angular/angular · GitHub
- completeはエラー時には実行されない
- finallyを利用
Angular関連
view操作
- angular4 forms - Load Dynamic templates within a single component using Angular 4 - Stack Overflow
- 前月から引き続き
- templateを動的に外部から読み込んで表示させる #angular
- サンプル
- javascript - How to use Angular structural directive with multiple inputs - Stack Overflow
- Structural Directiveで複数のInput渡し方
<div *permissionIf="permissions;except:'Read'"></div>
- 最終的には以下のようになる
<template [permissionIf]="permissions" [permissionIfExcept]="'Read'">...
- 最終的には以下のようになる
- 動的に、対象要素の内部にコンポーネントを追加する #angualr · GitHub
- 「host要素の次に配置」ではなく、内部に追加したい場合は
renderer.appendChild
を使う - Using Renderer2 in Angular
- 「host要素の次に配置」ではなく、内部に追加したい場合は
ngOnInit() { const componentFactory = this.componentFactoryResolver.resolveComponentFactory(SpinnerComponent); const componentRef = componentFactory.create(this.viewContainer.injector); this.renderer.appendChild( this.viewContainer.element.nativeElement, componentRef.location.nativeElement ); }
- RouterLinkActiveとそれをtemplate variableとして利用したときに #angular · GitHub
exportAs
の重要性- 上手く使えば簡単でシンプルなコードを提供できるようになる
フォーム
export class ChildComponent implements OnInit { ... @ViewChild("model") ngModel: NgModel; ngOnInit() { this.parentForm.addControl(this.ngModel); // <== add this this.parentForm.valueChanges.subscribe(changes => { console.log(JSON.stringify(changes)); }); } }
その他
null vs undefined
- null - JavaScript | MDN
- APIにおいては、通常はオブジェクトが返されるところで、関連したオブジェクトがない場合に
null
がよく渡されます。
- APIにおいては、通常はオブジェクトが返されるところで、関連したオブジェクトがない場合に
null
vs.undefined
in TypeScript land – BASARAT – MediumDon’t initialize optional sub properties
は確かに
TypeScript
- Revised Revised 型の国のTypeScript | Revised Revised TypeScript in Definitelyland](http://typescript.ninja/typescript-in-definitelyland/)
スニペット
Bootstrap4, デスクトップファースト
// Media of at most the maximum breakpoint width. No query for the largest breakpoint. // Makes the @content apply to the given breakpoint and narrower. @mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) { $max: breakpoint-max($name, $breakpoints); @if $max { @media (max-width: $max) { @content; } } @else { @content; } }
Htmlの<base href>
を取得
// <base href="/ja/"> const bases = document.getElementsByTagName('base'); if (bases.length > 0) { const baseHref = bases[0].href; // -> 'https://foo.com/ja/' }
Bugsnag
import { ErrorHandler, Injectable } from '@angular/core'; import { environment } from '../../environments/environment'; @Injectable() export class GlobalErrorHandler implements ErrorHandler { constructor() { Bugsnag.releaseStage = environment.stage; } handleError(error) { Bugsnag.notifyException(error); throw error; } }
ローダーのオンオフ、簡易版
function disable() { const elm: HTMLElement | null = document.getElementById('blocker'); if (elm) { elm.classList.add('active'); } } function enable() { const elm: HTMLElement | null = document.getElementById('blocker'); if (elm) { elm.classList.remove('active'); } }
Clickメモ
Array.apply(null, document.querySelectorAll('input[type=radio][value="3"]')).forEach(elm => elm.click());
Observable.throw()が返すオブジェクトの型はErrorObservable
import { Observable } from 'rxjs/Observable'; import { ErrorObservable } from 'rxjs/Observable/ErrorObservable'; // ... handleError(err): Observable<any>| ErrorObservable { if (/* 何か */) { return Observable.of('ok'); } else { return Observable.throw(err); } }
「テキスト入力 -> 自動で検索開始」を実現するためのRx使い方
- example-app/book.ts at ee0f331bf808525e003efa264b5065964c7f942b · ngrx/example-app · GitHub
- 元ネタ(NgRx4のサンプルアプリより)
シュミレート用サンプルコード
テキスト入力の代わりに、画面クリックで一連の流れが確認できる簡易版コードです。 ブラウザのコンソールで実行させれば、動作確認できます。
(http://reactivex.io/rxjs/などで実行してみてください。)
const sub = new Rx.BehaviorSubject(); const http = () => Rx.Observable.of('{ data }').delay(500); document.addEventListener('click', (e) => { sub.next(e); }); sub.debounceTime(1000) .distinctUntilChanged() .switchMap(() => { console.log('start http'); const next$ = sub.skip(1); return http().takeUntil(next$); }) .subscribe(val => console.log('get', val));
説明
.debounceTime
- 入力中に送信されないようにする
.distinctUntilChanged
- 前回と同じ値を送信しないようにする
.switchMap
- 未完了のhttp通信があった場合、無視する
.takeUntil(next$)
- debounceTimeで遅延させている間にhttp通信が完了する場合があるので、何かしらの入力変更があったらhttp通信を無視できるようにする
9月リンク集
- これは何か
- NgRx, アプリケーションの状態管理
- Rx関連
- Angular関連
- angular - @HostBinding and @HostListener: what do they do and what are they for? - Stack Overflow
- コンポーネントを動的に表示
- Change Detectionについて
- 私がMVCフレームワークをもはや使わない理由
- Running Protractor tests on Webdriver 2.47.1 gets - Error: Server terminated early with status 1 · Issue #2638 · angular/protractor · GitHub
- RouteReuseStrategy
- テスト
これは何か
- 9月に調べたことのメモ。基本形式は、「参考URLとそれに関するメモとコメント」。
- Angular関連がほとんど。
NgRx, アプリケーションの状態管理
A Comprehensive Introduction to @ngrx/store - Companion to Egghead.io Series
- 単純化したngrxの実装をしながら、Rxの機能、Reduxの考えかた、Rxによる実装方法がわかる
- Action -> Reducer -> Stateの流れ、Storeの実装方法、Reducerの合成、ミドルウェアなど
BehaviorSubject
,scan
,let
,distinctUntilChanged
,combineLatest
,withLatestFrom
など- Storeからデータを所得する方法、Store(State)のデータと表示用のデータをどう分けて管理するか #ngrx #angular · GitHub
- 個別のselectで取得, combineLatestでまとめる, projection functionを抽出して別ファイルor関数をつくる
// combine multiple state slices Observable.combineLatest( store.select('people'), store.select('events'), (people, events) => { // projection here })
Using NgRx 4 to Manage State in Angular Applications
- NgRxを使った状態管理について
- アプリケーションのStateを以下6種類に分けて考える
- Server state
- Persistent state
- The URL and router state
- Client state
- Transient client state
- Local UI state
- Using NgRx 4 to Manage State in Angular Applicationsのメモ #angular #rx #ngrx #redux · GitHub
- 日本語メモ
NgRx: Patterns and Techniques – nrwl
- アクションを3種類に分ける
- コマンド、ドキュメント、イベント
- エフェクト
- アクションの決定、アクションの変形、サイドエフェクト
- Reducerがやらないこと全てをおこなう
- NgRx: Patterns and Techniquesのメモ #angular #ngrx #redux #rx · GitHub
- 日本語メモ
platform/README.md at master · ngrx/platform · GitHub
- From Inactive to Reactive with ngrx Brandon Roberts & Mike Ryan - YouTube
- この動画 -> 公式のExampleアプリという順でみると理解しやすい
- https://github.com/ngrx/platform/blob/master/docs/store/README.md
- StoreからStateにアクセス
- StoreはStateのObservableでありActionのObserver
- Actionを受け取り、Stateを返す
- Reducer
export interface ActionReducer<T, V extends Action = Action> { (state: T | undefined, action: V): T; }
- https://github.com/ngrx/platform/blob/master/docs/effects/README.md
- Controlling Effects
- (メモ)
- Controlling Effects
- https://github.com/ngrx/platform/blob/master/docs/router-store/README.md
- どのタイミングでアクションをDispatchしているのか
- https://github.com/ngrx/platform/blob/0528d2ddea5a0a772d7130f7296984e82369961a/modules/router-store/src/router_store_module.ts#L176
(<any>this.router).hooks.beforePreactivation
- https://github.com/ngrx/platform/blob/0528d2ddea5a0a772d7130f7296984e82369961a/modules/router-store/src/router_store_module.ts#L216
this.router.events.subscribe
を利用- こっちは想像通り
- https://github.com/ngrx/platform/blob/0528d2ddea5a0a772d7130f7296984e82369961a/modules/router-store/src/router_store_module.ts#L176
- どのタイミングでアクションをDispatchしているのか
Rx関連
Rxのオペレータメモ #rx · GitHub
- 自分用のメモ
mergeMapとswitchMap
- RxJSのconcatMap, mergeMap, switchMapの違いを理解する(中級者向け) - Qiita
- rxjs - SwitchMap vs MergeMap in the #ngrx example - Stack Overflow
with the switchMap you can cancel the previous network request if it's running?
- 配列を返すことも可能
RxJS を学ぼう #2 – よく使う ( と思う ) オペレータ15選 – NET BIZ DIV. TECH BLOG
Angular関連
angular - @HostBinding and @HostListener: what do they do and what are they for? - Stack Overflow
- @HostBindingと@HostListenerのシンプルな利用例
コンポーネントを動的に表示
- Dynamically add components to the DOM with Angular – Frontend Weekly – Medium
-
@ViewChild('dynamic', { read: ViewContainerRef }) viewContainerRef: ViewContainerRef
read
の存在知らなかった
-
- NgComponentOutlet
- 別の方法
- 表示するだけならこっちの方がシンプルに実装できる
- NgTemplateOutletもある
- angular - What are projectable nodes in angular2 - Stack Overflow
createComponent
の引数であるprojectableNodes
について
- angular4 forms - Load Dynamic templates within a single component using Angular 4 - Stack Overflow
- まだしっかりみてない
ng-template
を取得するためにGetTemplateDirectiveというDirectiveをつくる
Change Detectionについて
- 日本語訳:Angular 2 Change Detection Explained - Qiita
- わかりやすい。ありがたい
- イベント、XHR、タイマー
- Change And Its Detection In JavaScript Frameworks
- Angularに限らない話
markForCheck
とdetectChanges
の違いはわかるがどっちを使うべきかの説明がまだしっくりきていない- ルートからChange Detectionをはじめる
- Everything you need to know about change detection in Angular
Its content is based on the newest Angular version — 4.0.1. The way how change detection mechanism is implemented under the hood in this version is different from the earlier 2.4.1.
私がMVCフレームワークをもはや使わない理由
Running Protractor tests on Webdriver 2.47.1 gets - Error: Server terminated early with status 1 · Issue #2638 · angular/protractor · GitHub
- Macを買い換えたらE2Eのテストがこけてしまった
- 原因はJavaのバージョン
export JAVA_HOME="/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home"
- 原因はJavaのバージョン
RouteReuseStrategy
- 実際にはこのような使い方をしていないがメモとして
/** * コンポーネントの再描画のルールを変更 * https://medium.com/@juliapassynkova/angular-2-component-reuse-strategy-9f3ddfab23f5 */ export class CustomRouteReuseStrategy implements RouteReuseStrategy { // copy from DefaultRouteReuseStrategy shouldDetach(route) { return false; } store(route, detachedTree) { } shouldAttach(route) { return false; } retrieve(route) { return null; } // override shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { return this._shouldReuseRoute(future, curr) && this.isUnChanged(future, curr); } private _shouldReuseRoute(future, curr) { return future.routeConfig === curr.routeConfig; } /** * 追加ルール * 特定コンポーネントかつ、`key`が変更した時 */ private isUnChanged(future, curr) { const name = future.component && (<any>future.component).name; if (name === 'BarComponent') { if (future.paramMap.get('key') && curr.paramMap.get('key') && future.paramMap.get('key') !== curr.paramMap.get('key')) { return false; } } return true; } }
テスト
selenium - Debugging “Element is not clickable at point” error - Stack Overflow
Angular (4)で設定したLocale IDを取得
LOCALE_ID
を利用。
以下のようにAOTでコンパイルしていない場合、初期値のen-US
になります。
ng serve --aot --locale ja
import { Component, OnInit, LOCALE_ID, Injector } from '@angular/core'; export class FooComponent implements OnInit { constructor(private injector: Injector) { } ngOnInit() { const locale = this.injector.get(LOCALE_ID); // -> 'ja' } }
雑メモはGist (Lepton) に
プログラムに関する備忘録をこのブログに書いていましたが、それくらいの文章やコードならGistにあげればいいのかと思い始めました。
Leptonというアプリがあるのでしばらくそれを使ってみようかと思います。
Quiver、SnippetsLabなど他のアプリも試してみましたが、今のところLeponが自分の用途にはあってそうです。
- Lepton
- GIstのクライアントアプリ
- 無料
- Quiver
- SnippetsLab
以下ページも参考になりそうです。