以下のコードは、米国の州を選択できるオートコンプリートフォームコントロールを示しています。
<mat-form-field class="example-full-width">
<input matInput placeholder="State" aria-label="State" [matAutocomplete]="auto" [formControl]="stateCtrl">
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let state of filteredStates | async" [value]="state.name">
<img style="vertical-align:middle;" aria-hidden src="{{state.flag}}" height="25" />
<span>{{ state.name }}</span> |
<small>Population: {{state.population}}</small>
</mat-option>
</mat-autocomplete>
</mat-form-field>
ただし、アプリケーションでこのタイプの入力が必要な場所が多数ある場合、これをすべてのボイラープレートを繰り返す必要のないコンポーネント(ディレクティブ?)に変えることは理にかなっています。ただし、テンプレート駆動型またはモデル駆動型のいずれかでこれを使用し、プレースホルダー、検証などをコンテナコンポーネントごとに変更できるようにしたいと思っています。
これを達成するためのシンプルで堅牢な方法は何ですか?
Angularに推奨される一般的なアプローチを試しましたが、Angular Materialのさまざまな要件を考慮していません。たとえば、MatFormFieldControlを実装する必要があります。 Angularマテリアルは、既存のAngularマテリアルフォームコントロールを利用/ラップするのではなく、プリミティブ要素を使用して新しいフォームコントロールを作成することに重点を置いています。
目的は、次のようなことをフォームで実行できるようにすることです。
<mat-form-field>
<lookup-state placeholder="State of Residence" required="true" formControlName="resState">
</lookup-state>
</mat-form-field>
Angular Material。を使用して、コンポーネントの例を貼り付けます。カスタム入力コンポーネントを作成しました(2つの場合:単純な入力またはオートコンプリート):
これは私のInput.component.html
<mat-form-field color="accent" [hideRequiredMarker]="true" [class.mat-form-field-invalid]="hasErrors">
<ng-container *ngIf="autocomplete">
<input matInput [matAutocomplete]="auto" [type]="type" [placeholder]="placeholder" [disabled]="isDisabled" [value]="innerValue" (input)="autocompleteHandler($event)" (blur)="autocompleteBlur($event)">
<mat-autocomplete #auto [displayWith]="displayText" (optionSelected)="updateOption($event)">
<mat-option *ngFor="let choice of autocompleteChoices | async" [value]="choice">{{ choice.text }}</mat-option>
</mat-autocomplete>
</ng-container>
<input *ngIf="!autocomplete" matInput [type]="type" [placeholder]="placeholder" [disabled]="isDisabled" [value]="innerValue" (input)="inputHandler($event)" (blur)="setTouched()">
</mat-form-field>
これは私のInput.component.ts
import { Component, Input, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, NgModel } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material';
import { ChoiceList } from '../../../../models/choice-list';
import { ChoiceSource } from '../../../../models/choice-source';
import { getFlagAttribute } from '../../../../utils';
import { HintComponent } from '../hint/hint.component';
import { ErrorsComponent } from '../errors/errors.component';
import { FormField } from '../form-field';
import { ChoiceModel } from '../../../../models/choice-model';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/toPromise';
@Component({
selector: 'my-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
}]
})
export class InputComponent extends FormField implements ControlValueAccessor {
@Input() type = 'text';
@Input() placeholder: string;
@Input() autocomplete: ChoiceSource;
autocompleteChoices: ChoiceList;
@Input() set value(value: string) {
this.innerValue = value == null ? '' : String(value);
}
get value() {
return this.innerValue;
}
@Input() set disabled(value: any) {
this.setDisabledState(getFlagAttribute(value));
}
get disabled() {
return this.isDisabled;
}
private changeCallback: Function;
private touchedCallback: Function;
isDisabled = false;
innerValue = '';
displayText(value: ChoiceModel): string {
return value.text;
}
writeValue(value: any) {
if (!this.autocomplete) {
this.value = value;
}
}
registerOnChange(fn: Function) {
this.changeCallback = fn;
}
registerOnTouched(fn: Function) {
this.touchedCallback = fn;
}
setDisabledState(isDisabled: boolean) {
this.isDisabled = isDisabled;
}
inputHandler(event: Event) {
this.value = (<HTMLInputElement>event.target).value;
if (this.changeCallback) {
this.changeCallback(this.value);
}
}
autocompleteHandler(event: Event) {
const text = (<HTMLInputElement>event.target).value;
if (this.autocomplete) {
if (text) {
this.autocompleteChoices = this.autocomplete(text);
} else if (this.changeCallback) {
this.innerValue = '';
this.changeCallback(null);
}
}
}
autocompleteBlur(event: Event) {
(<HTMLInputElement>event.target).value = this.innerValue;
this.setTouched();
}
updateOption(event: MatAutocompleteSelectedEvent) {
if (this.changeCallback) {
const { value, text } = event.option.value;
this.value = text;
this.changeCallback(value);
}
}
setTouched() {
if (this.touchedCallback) {
this.touchedCallback();
}
}
}
次に、両方の使用例を示します。
シンプル入力ケース
<my-input type="text" name="myInputName" [(ngModel)]="myNgModel" placeholder="---" required pattern="[a-zA-Zàèìòù\'\s0-9\.]+">
</my-input>
オートコンプリート入力ケース
export myClass implements OnInit, AfterViewInit, ControlValueAccessor, AfterViewChecked {
@ViewChild('BirthTown') BirthTown: InputComponent; //from import
public autocompleteSourceBirthTown: Function;
this.autocompleteSourceBirthTown = (async function(input: string) {
if (input.trim().length > 2) {
const towns = await this.generalService.getListBirthTowns(input.trim());
return towns;
}
return [];
}).bind(this);
// only for text of town
ngAfterViewChecked() {
if (this.BirthTown && this.BirthTownNgModel) {
const textTown = this.stateService.getDataBirthTown(this.BirthTownNgModel);
if (textTown) {
this.textBirthTown = textTown;
}
}
<seg-input #BirthTown [(ngModel)]="BirthTownNgModel" placeholder="BirthTown" [autocomplete]="autocompleteSourceBirthTown" [value]="textBirthTown" required>
</seg-input>
希望が役立つ
オートコンプリート用のラッパーコンポーネントを作成するときにも同じ問題が発生しました。以下は、リアクティブおよびテンプレート駆動型で動作する私の実装です。これを実現するには、ControlValueAccessor
を実装する必要があります。コンポーネントに移動したい検証もある場合は、Validator
インターフェイスを実装することもできます。
フォームコントロールが実際に無効であっても、mat-form-field
が無効としてマークされないという問題に遭遇しました。 This 問題に関するコメント "FormFieldがカスタムコンポーネントでラップされている場合、スタイルは適用されません"および this 関連するプランカー私はそれを修正するのに役立ちました。
autocomplete.component.html:
<mat-form-field>
<input #input matInput type="text" class="form-control" [matAutocomplete]="autocomplete" (input)="valueChanged($event)" [readonly]="readonly"
(focus)="$event.target.select()" (blur)="onTouched()">
<mat-autocomplete #autocomplete="matAutocomplete" [displayWith]="displayFunction" (optionSelected)="onOptionSelected($event)">
<mat-option *ngFor="let option of filteredOptions" [value]="option">
{{ displayFunction(option) }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
autocomplete.component.ts:
import { MatAutocompleteTrigger, MatInput } from '@angular/material';
import {
Component,
Input,
AfterViewInit,
ViewChild,
OnChanges,
SimpleChanges,
forwardRef,
Injector
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl, Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';
import { forbiddenAutocompleteValue } from 'app/shared/directives/validators/autocomplete-validator.directive';
@Component({
selector: 'pp-autocomplete',
templateUrl: './autocomplete.component.html',
styleUrls: ['./autocomplete.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AutocompleteComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AutocompleteComponent),
multi: true
}
]
})
export class AutocompleteComponent implements AfterViewInit, OnChanges, ControlValueAccessor, Validator {
@Input() options: any[] = [];
@Input() readonly = false;
@Input() displayFunction: (value: any) => string = this.defaultDisplayFn;
@Input() filterFunction: (value: any) => any[] = this.defaultFilterFn;
@ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
@ViewChild(MatInput) matInput: MatInput;
filteredOptions: any[];
optionSelected = '';
onChange = (val: any) => {};
onTouched = () => {};
constructor(
private injector: Injector
) { }
ngAfterViewInit() {
this.trigger.panelClosingActions
.subscribe(
e => {
if (this.trigger.activeOption) {
const value = this.trigger.activeOption.value;
this.writeValue(value);
this.onChange(value);
}
}
);
// this is needed in order for the mat-form-field to be marked as invalid when the control is invalid
setTimeout(() => {
this.matInput.ngControl = this.injector.get(NgControl, null);
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes.options) {
this.filterOptions(this.optionSelected);
}
}
writeValue(obj: any): void {
if (obj) {
this.trigger.writeValue(obj);
this.optionSelected = obj;
this.filterOptions(obj);
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.matInput.disabled = isDisabled;
this.trigger.setDisabledState(isDisabled);
}
validate(c: AbstractControl): { [key: string]: any; } {
return forbiddenAutocompleteValue()(c);
}
valueChanged(event) {
const value = event.target.value;
this.optionSelected = value;
this.onChange(value);
this.filterOptions(value);
}
onOptionSelected(event) {
const value = event.option.value;
this.optionSelected = value;
this.onChange(value);
this.filterOptions(value);
}
filterOptions(value) {
this.filteredOptions = this.filterFunction(value);
}
private defaultFilterFn(value) {
let name = value;
if (value && typeof value === 'object') {
name = value.name;
}
return this.options.filter(
o => o.name.toLowerCase().indexOf(name ? name.toLowerCase() : '') !== -1
);
}
defaultDisplayFn(value) {
return value ? value.name : value;
}
}