この記事は Angular #2 Advent Calendar 2019 の 17 日目の記事です。
はじめに
あるプロジェクトで結構複雑めなフォームを作った時にReactive Formsを体系的に調べていたのですが、その中でReactive Formsに型欲しいなと感じたので、型をつけていきます。
備忘録も兼ねて、AngularのTemplate-driven Formsの書き方とも比較したりしながらまとめていきます。
初Advent Calendarということで、周りの先輩方の記事を模倣しながら、いい感じに記事をかけていけたらと思いま〜す!
Angularにおけるフォームの実装
ログインフォーム、決済フォーム、会員登録フォームなどみなさん一度はフォームを実装したことがあると思います。
Angularにはフォームを実装する方法が2つ用意されており、それぞれ特徴があるので、どちらが自分のプロジェクトにあっているか考えていきましょう。
テンプレート駆動フォーム(Template-driven Forms)
特徴
- FormsModuleを使い、テンプレート側でディレクティブとともに定義されるので、テンプレート側がごちゃごちゃしがち
- テンプレート側にもフォームの情報が記載されているため、フォームの構造をテンプレートとコンポーネントどちらも見なければならない
- DOMを用意しないとテストが出来ない
- Array構造のフォームを作成できない
サンプル
<h1>Login Form - Template-driven Forms</h1>
<form #form="ngForm" class="container" (ngSubmit)="submitForm()">
<div class="item">
<label>ID: </label>
<input type="text" name="id" [(ngModel)]="id" required>
</div>
<div class="item">
<label>Mail: </label>
<input type="text" name="mail" [(ngModel)]="mail" required>
</div>
<div class="item">
<label>Password: </label>
<input type="password" name="password" [(ngModel)]="password" required>
</div>
<button type="submit" [disabled]="!form.valid">Submit</button>
</form>
@Component({
selector: 'app-template-driven-forms',
templateUrl: './template-driven-forms.component.html',
styleUrls: ['./template-driven-forms.component.css']
})
export class TemplateDrivenFormsComponent implements OnInit {
id: number;
mail: string;
password: string;
constructor() { }
ngOnInit() {
this.initForm();
}
submitForm() {
console.log('submit');
console.log(this.id);
console.log(this.mail);
console.log(this.password);
}
private initForm() {
this.id = 0;
this.mail = 'a';
this.password = 'b';
}
}
リアクティブフォーム
特徴
- ReactiveFormsModuleを使い、コンポーネント側で明示的に構造を記述するので、コンポーネント側がごちゃごちゃしがち
- コンポーネント側にフォームの情報がすべて記載されるため、コンポーネント側を見ればフォームの構造がわかる
- DOMを用意しなくてもテストができる
- 値が変更された時の変化をsubscribeできる
サンプル
<h1>Login Form - Reactive Forms</h1>
<form class="container" [formGroup]="form" (ngSubmit)="submitForm()">
<div class="item">
<label>ID: </label>
<input type="text" formControlName="id">
</div>
<div class="item">
<label>Mail: </label>
<input type="text" formControlName="mail">
</div>
<div class="item">
<label>Password: </label>
<input type="password" formControlName="password">
</div>
<button type="submit" [disabled]="!form.valid">Submit</button>
</form>
@Component({
selector: 'app-reactive-forms',
templateUrl: './reactive-forms.component.html',
styleUrls: ['./reactive-forms.component.css']
})
export class ReactiveFormsComponent implements OnInit {
form: FormGroup;
constructor(
private fb: FormBuilder,
) { }
ngOnInit() {
this.initForm();
}
submitForm() {
console.log('submit');
console.log(this.form.get('id'));
console.log(this.form.get('mail'));
console.log(this.form.get('password'));
}
private initForm() {
this.form = this.fb.group({
id: [0, Validators.required],
mail: [''],
password: ['a', Validators.required],
});
}
}
どちらを使う?混在は可能?
普段私はフォームの構造も見やすく、テストもしやすい(あまり書けてませんが、、)ので、Reactive Formsを採用して開発を行っています。
フォームの混在は可能ですが、以下理由のためどちらかを採用するのが無難でしょう。
- チームメンバーの学習曲線が上がる
- FormsModule, ReactiveFormsModuleどちらのモジュールもロードしないといけないのでバンドルサイズが大きくなる
もともとテンプレート駆動フォームはAngularJSの見た目を汲んだフォームの記述方法なので、AngularJS->Angular2系の移行時、単純な構造のフォームしか使わない場合はテンプレート駆動フォームを使っても良いかもしれません。
しかし、以上のコードを書いていくとわかるのですが、Angular Reactive Formsには型をつけることが出来ません。
この状態で開発を行っていくと、うっかり違う型の値を入れてしまったり、存在しないプロパティを呼び出す可能性があります。
型をつける
Interfaceを定義する場合
Type-safe Formについて議論が行われているIssueにて dmorosinotto
が ReactiveFormsModuleのextends interfaceを定義しています。
https://github.com/angular/angular/issues/13721#issuecomment-468745950
https://stackblitz.com/edit/typedforms
これを自前のmodel interface型にすることで、型安全なフォームを作ることが出来ます。
サンプル
上記のinterface群を TypedForms.d.ts
として配置します。
Form用のinterfaceを定義して、そのinterface定義から型安全な FormGroupTyped<interface>
を呼び出します。
interface IFormType {
id: number;
mail: string;
password: string;
}
form: FormGroupTyped<IFormType>;
private initForm() {
this.form = this.fb.group({
id: [0, Validators.required],
mail: ['', Validators.required],
password: ['', Validators.required],
}) as FormGroupTyped<IFormType>;
let num: number = this.form.value.id; // no Error
let str: string = this.form.value.id; // [Error] Type 'number' is not assignable to type 'string'.
this.form.get('id').setValue(0); // no Error
this.form.get('id').setValue(true); // [Error] Argument of type '"a"' is not assignable to parameter of type 'never'.
}
すると form.value.id
の部分で型補完が効くようになり、型情報も追加されるので、コンポーネント内の操作で型を間違えることがなくなります!🎉
テンプレート側でも補完が効くようになります!
ちなみにこの記事を書くときも password
を passowrd
とタイポしていることに、型補完を効かせてから気付きましたw
ライブラリを使う場合
もっと簡単な方法で済ませたいということであれば、npmパッケージを使うことが出来ます。
スター数で言うと、
KostyaTretyak/ng-stack
no0x9d/ngx-strongly-typed-forms
あたりが使われているのではないかと思われます。
今回はKostyaTretyak/ng-stackを使ってみます。
以下コマンドでライブラリをインストールします。
$ npm i -s @ng-stack/forms
READMEに従って、 app.module.ts
にモジュールをインポートします
import { NgStackFormsModule } from '@ng-stack/forms';
// ...
@NgModule({
// ...
imports: [
NgStackFormsModule
]
// ...
});
あとは @angular/forms
の代わりに、 @ng-stack/forms
を用いることで、型安全なフォームを実装することが出来ます
import { FormGroup, FormControl, FormArray } from '@ng-stack/forms';
@Component({
selector: 'app-reactive-forms',
templateUrl: './reactive-forms.component.html',
styleUrls: ['./reactive-forms.component.css']
})
export class ReactiveFormsComponent implements OnInit {
private initForm() {
const formControl = new FormControl('some string');
const value = formControl.value; // some string
formControl.setValue(123); // Error: Argument of type '123' is not assignable...
}
}
こちらはAngularのバージョンアップへの対応が遅れる場合も考えられるので、頻繁にAngularバージョンあげてない && 手っ取り早く型安全フォームを入れたいということであれば、ライブラリを使うのもアリかもしれません。
標準実装を待つ場合
AngularのStrongly Type-safe Reactive Formsについてはかなり昔から議論されており、Issueも各種上がっています。
特に議論が行われているのは
https://github.com/angular/angular/issues/13721
で、このIssueをもとにプロポーザルも作られています
https://github.com/angular/angular/issues/31963
近い将来より型安全なフォームが標準化されるかもしれません。
まとめ
Angularを触り始めて1年半ほど経過し、型安全の素晴らしさと開発のしやすさ、
Angularの初学者が触ってもある程度の質を保ち学びながら実装できる素晴らしさに気づくことが出来ました。
それとは別にOSSはGitHub上での議論が活発で、今回の解法もほぼGitHubの議論から辿ったりしたので、
もっとAngularの思想も、内部構造も、(英語も)理解してOSS貢献に参加していきたいですね!
以上拙い記事ですが、最後まで読んでいただきありがとうございました!!
明日は @fusho-takahashi さんです!