やりたいこと
- カスタムコントロールをコンポーネント化したい
- コンポーネントに対して model-based form を使いたい
- バリデーション後のエラーメッセージ等をコンポーネント内に内包したい
カスタムコントロールを作るには
NgModelやFormControlNameに対応するにはControlValueAccessorを実装すればいいっぽい。
以下の記事を参照した。
Custom Form Controls in Angular 2 by thoughtram
コンポーネント内からバリデーションエラーを検知する
普通に作ろうとすると、内部からFormControlのerrorsオブジェクトにアクセスできず、何がバリデーションエラーになっているのかわからないため、メッセージの出しようがない。
結論としては、InjectorからNgControlを取り出せば可能のようだ。
最終的なコード
こんな感じになった
@Component({
selector: 'custom-input',
template: `
<input type="text" [(ngModel)]="value" (blur)="onBlur()"/>
<div *ngIf="ngControl && ngControl.invalid && (ngControl.touched || ngControl.dirty)" class="error">
<span *ngIf="ngControl.errors.required">必須項目です。</span>
</div>
`,
styles: [`
:host {
display: inline-block;
position: relative;
}
.error {
display: block;
top: calc(100% + 12px);
position: absolute;
padding: .5em 1em .4em;
background-color: #f66;
border: 1px solid #f00;
border-radius: 5px;
color: #fff;
z-index: 10;
}
.error:after, .error:before {
content: '';
position: absolute;
bottom: 100%;
left: 15px;
border: solid transparent;
}
.error:after {
margin-left: 1px;
border-bottom-color: #f66;
border-width: 7px;
}
.error:before {
border-bottom-color: #f00;
border-width: 8px;
}
`],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInput),
multi: true
}]
})
export class CustomInput implements ControlValueAccessor, OnInit {
private innerValue: any = '';
private ngControl: NgControl;
private onTouchedCallback = () => {};
private onChangeCallback = (_: any) => {};
constructor(private injector: Injector) { }
ngOnInit(): void {
this.ngControl = this.injector.get(NgControl);
}
get value(): any {
return this.innerValue;
};
set value(v: any) {
if (v !== this.innerValue) {
this.innerValue = v;
this.onChangeCallback(v);
}
}
onBlur() { this.onTouchedCallback() }
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
registerOnChange(fn: any) { this.onChangeCallback = fn }
registerOnTouched(fn: any) { this.onTouchedCallback = fn }
}