カスタムコンポーネント(独自コンポーネント)で ngModel
を使いたいというケースって結構あると思います。
ngModel
は、 [(ngModel)]
と書く通り、双方向バインドで あり、普通の @Input, @Output ではありません。
そのため、ちょっと特殊な書き方が必要になります。
今回作るコンポーネント
label+inputという簡単なコンポーネント
htmlは下記のような感じを想定
<div>
<label> {{ label }} </label>
<input [(ngModel)]="value">
</div>
ngModel を独自コンポーネントで実装する手順
最初に、手順をまとめると、下記のようになります。
-
@Component
にprovider
を追加 -
ControlValueAccessor
を implement - valueの
getter
,setter
を作成
@Component
に provider
を追加
@Component({
selector: 'app-test-ng-model',
templateUrl: './test-ng-model.component.html',
styleUrls: ['./test-ng-model.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => TestNgModelComponent),
},
],
})
上記 providers
を追加する。 ngModel
を利用するには、 ControlValueAccessor
を implement
する必要があるのだが、どうもそのためにこれが必要らしい。
ちなみに、これを書かなかった場合、下記のエラーとなる
Error: No value accessor for form control with name: 'xxx'
ControlValueAccessor
を implement
export class TestNgModelComponent implements ControlValueAccessor {
上記のように、 ControlValueAccessor
を implement
する。
これにより、
writeValue()
registerOnChange()
registerOnTouch()
setDisabledState()
のメソッドの実装が必要となる。
上記の各メソッドが何を行いどんなタイミングで呼び出されるものなのか、詳細が分かる人がいたら教えて欲しい(切実)が、とりあえず名前か導かれるような動作をするんだと推測している。
とりあえず、 ngModel
が動くように最低限の実装をしておく
export class TestNgModelComponent implements ControlValueAccessor {
private onTouchedCallback: () => void = () => {};
private onChangeCallback: (_: any) => void = () => {};
writeValue(text: string): void {
if (text !== this.value) {
this.value = text;
}
}
registerOnChange(fn: any): void {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any): void {
this.onTouchedCallback = fn;
}
setDisabledState(isDisabled: boolean): void {}
}
valueの getter
, setter
を作成
最後に、 value
の getter
と setter
を用意する
_value: string; // value を保存しておく変数を定義する。名前はなんでも良い
get value(): string {
return this._value;
}
@Input('value')
set value(text: string) {
if (this._value !== text) {
this._value = text;
this.onChangeCallback(text);
}
}
これで value
に対する getter
, setter
は用意できた。より複雑な仕様にしたい場合はこの setter
部分などを変更すると良いと思われる。
最終的なコード
コンポーネントのTypescript
@Component({
selector: 'app-test-ng-model',
templateUrl: './test-ng-model.component.html',
styleUrls: ['./test-ng-model.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => TestNgModelComponent),
},
],
})
export class TestNgModelComponent implements ControlValueAccessor {
@Input() label: string;
_value: string;
private onTouchedCallback: () => void = () => {};
private onChangeCallback: (_: any) => void = () => {};
get value(): string {
return this._value;
}
@Input('value')
set value(text: string) {
if (this._value !== text) {
this._value = text;
this.onChangeCallback(text);
}
}
writeValue(text: string): void {
if (text !== this.value) {
this.value = text;
}
}
registerOnChange(fn: any): void {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any): void {
this.onTouchedCallback = fn;
}
setDisabledState(isDisabled: boolean): void {}
}
コンポーネントのView
<div>
<label> {{ label }} </label>
<input [(ngModel)]="value">
</div>
このコンポーネントを使う親のコンポーネントのView
(キーワードという文言のInputという仮定で作成)
<div class="container">
<app-test-ng-model name="keyword" label="キーワード" [(ngModel)]="keyword"></app-test-ng-model>
</div>
最後に
Angularの公式は、こういう少し複雑なことを調べるのがかなり大変で、試行錯誤するしかない印象なので、こうやって溜めていきます。
ngModel
は結構めんどうなので、 ngmodelをやるためだけの親クラスとか作ると捗るかも。