下面的代码显示了一个允许选择美国州的自动填充表单控件。
<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 Material提供的指导更多的是使用原始元素创建新的表单控件,而不是利用/包装现有的Angular Material表单控件。
目标是能够以这样的形式做这样的事情:
<mat-form-field>
<lookup-state placeholder="State of Residence" required="true" formControlName="resState">
</lookup-state>
</mat-form-field>
答案 0 :(得分:7)
我将使用Angular Material粘贴我的组件示例。我创建了一个自定义输入组件(两种情况:简单输入或自动完成):
这是我的 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>
&#13;
这是我的 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();
}
}
}
&#13;
现在我将举一个使用它们的例子:
简单输入案例
<my-input type="text" name="myInputName" [(ngModel)]="myNgModel" placeholder="---" required pattern="[a-zA-Zàèìòù\'\s0-9\.]+">
</my-input>
&#13;
自动填充输入案例
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;
}
}
&#13;
<seg-input #BirthTown [(ngModel)]="BirthTownNgModel" placeholder="BirthTown" [autocomplete]="autocompleteSourceBirthTown" [value]="textBirthTown" required>
</seg-input>
&#13;
希望有所帮助
答案 1 :(得分:3)
当想要为自动完成创建包装器组件时,我遇到了同样的问题。下面是我的实现,它以反应式和模板驱动的形式工作。为了实现这一目标,您需要实现ControlValueAccessor
。如果您还要进行某些验证,那么您也可以实现Validator
界面。
我确实遇到了mat-form-field
未被标记为无效的问题,即使表单控件实际上无效。 This对“如果FormField由自定义组件包装”未应用样式的问题发表评论“this相关的plunker帮助我解决了这个问题。
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;
}
}