これは何か
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); } // ...