The code below shows an Autocomplete form control that allows selection of a US state.
<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>
However, if in my application I have numerous locations where this type of input is required then it would make sense to turn this into a component (directive?) where all the boilerplate does not need to be repeated. However, I would still like to be able to use this in either template-driven or model-driven forms and allow the placeholders, validations etc. to be varied by the container component.
What is a simple and robust way to achieve this?
I have tried general approaches recommended for Angular but they do not take account of the various requirements of Angular Material. E.g. Needing to implement MatFormFieldControl. The guidance provided by Angular Material is directed more at creating a new form control using primitive elements rather than utilising/wrapping existing Angular Material form controls.
The objective is to be able to do something like this in a form:
<mat-form-field>
<lookup-state placeholder="State of Residence" required="true" formControlName="resState">
</lookup-state>
</mat-form-field>
i'm going to paste my example of component using Angular Material. I've created a custom Input component (two cases: simple input or autocomplete):
this is my 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>
this is my 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();
}
}
}
Now i'll going to put an example of using both of them:
simple input case
<my-input type="text" name="myInputName" [(ngModel)]="myNgModel" placeholder="---" required pattern="[a-zA-Zàèìòù\'\s0-9\.]+">
</my-input>
autocomplete input case
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>
hope will help