Angular Material as a base for reusable components

Alan Buscaglia
Mar 27 · 5 min read

Hello everyone ! let’s talk about Angular Material, a super cool UI library created by google that we angular developers love to use in our projects. But …. what if we have been using it wrong ? I get it, some components that the library uses are super simple, just with a copy and paste we can get them working, and the same with more complex elements like the auto complete…. right ?.

Lets see some examples:

https://material.angular.io/components/input/overview

HTML for Angular Material inputs

<input matInput placeholder="Favorite food" value="Sushi">

Easy enough! we can still change it in a way so we can use it with a reactive form:

TS:

export class OurComponent implements OnInit {
form: FormGroup;
constructor(fb: FormBuilder){}
ngOnInit() {

this.form = this.fb.group({
sushi: [""],
});

}
}

HTML:

<form [formGroup]="form">    <mat-form-field>       <mat-placeholder>Your Sushi Input</mat-placeholder>       <textarea matInput formControlName="sushi"></textarea>

</mat-form-field>
</form>

Super simple !, but….what happens with something a little bit more complex ?

https://material.angular.io/components/autocomplete/overview

HTML for Angular Material autocomplete with filter

<form class="example-form">

<mat-form-field class="example-full-width">

<input type="text" placeholder="Pick one" aria-label="Number" matInput [formControl]="myControl" [matAutocomplete]="auto">

<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{option}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</form>

TS:

export class AutocompleteFilterExample implements OnInit {
myControl = new FormControl();
options: string[] = ['One', 'Two', 'Three'];
filteredOptions: Observable<string[]>;
ngOnInit() {
this.filteredOptions = this.myControl.valueChanges
.pipe(
startWith(''),
map(value => this._filter(value))
);
}
private _filter(value: string): string[] {
const filterValue = value.toLowerCase();
return this.options.filter(option => {
option.toLowerCase().includes(filterValue));
}
}
}

Still easy to implement….but if we only do it once….. lets see what happens if we have more than one :

HTML:

<form class="example-form">

<mat-form-field class="example-full-width">

<input type="text" placeholder="Pick one" aria-label="Number" matInput [formControl]="myControl" [matAutocomplete]="auto">

<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{option}}
</mat-option>
</mat-autocomplete>
<mat-form-field class="example-full-width">

<input type="text" placeholder="Pick another one" aria-label="Number" matInput [formControl]="myControl2" [matAutocomplete2]="auto2">

<mat-autocomplete #auto="matAutocomplete2">
<mat-option *ngFor="let option of filteredOptions2 | async" [value]="option">
{{option}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</form>

TS:

export class AutocompleteFilterExample implements OnInit {
myControl = new FormControl();
options: string[] = ['One', 'Two', 'Three'];
options2: string[] = ['Four', 'Five', 'Six'];
filteredOptions: Observable<string[]>;
filteredOptions2: Observable<string[]>;
ngOnInit() {
this.filteredOptions = this.myControl.valueChanges
.pipe(
startWith(''),
map(value => this._filter(value))
);
this.filteredOptions2 = this.myControl2.valueChanges
.pipe(
startWith(''),
map(value => this._filter(value))
);
}
private _filter(value: string): string[] {
const filterValue = value.toLowerCase();
return this.options.filter(option => {
option.toLowerCase().includes(filterValue));
}
}
}

It´s starting to get a little bloated right ?, imagine now a form that uses 10 auto complete components….. we need to create 10 filteredOptions with their respective valueChanges in a single component… we can do better !!!

So let’s try to think it different:

First lets create our new auto complete component based on Angular Material, in the meantime we can also increase its functionality:

HTML:

<ng-container *ngIf="_options.length > 0; else noOptionsTemplate">
<mat-form-field class="full-width">
<input type="text" matInput [placeholder]="completePlaceholder" [formControl]="control" [matAutocomplete]="auto" />
<mat-autocomplete
#auto="matAutocomplete"
[displayWith]="getLabelName.bind(this)"
(optionSelected)="selectionChange ? selectionChange($event) : null"
>
<mat-option *ngFor="let option of filteredOptions | async" [value]="option.value" [title]="option.label">
{{ option.label }}
</mat-option>
</mat-autocomplete>
<mat-error>
<app-input-error-messages [control]="control"></app-input-error-messages>
</mat-error>
</mat-form-field>
</ng-container>
<ng-template #noOptionsTemplate>
<mat-form-field class="full-width">
<input type="text" matInput [placeholder]="completePlaceholder" [formControl]="dummyControl" />
</mat-form-field>
</ng-template>

TS:

@Component({
selector: 'app-auto-complete',
templateUrl: './autocomplete.component.html',
styles: ['.full-width { width: 100% }']
})
export class AutoCompleteComponent implements OnInit {
@Input() control = new FormControl();
@Input() set options(value: any[]) {
this._options = [...(value ? value : [])];
this.disableControl(this._options.length === 0);
this.checkValidations();
}
@Input() placeholder: string;
@Input() selectionChange: Function;
@Input() set disabled(value: boolean) {
this.disableControl(value);
}
@Input() set required(value: boolean) {
this._required = value;
this.checkValidations();
this.completePlaceholder = `${this.placeholder} ${this._required ? '*' : ''}`;
}
private lastValue: string;
dummyControl = new FormControl('');
completePlaceholder = 'Elija una opción';
_options: SelectOption[];
_required: boolean;
filteredOptions: Observable<SelectOption[]>;
constructor(private validationService: ValidationService) {
this.dummyControl.disable();
}
ngOnInit() {
this.filteredOptions = this.control.valueChanges.pipe(
startWith(''),
debounceTime(300),
map(value => this._filter(value))
);
this.completePlaceholder = `${this.placeholder} ${this._required ? '*' : ''}`;
}
private checkControlValue() {
return this.control && this.control.value ? this.control.value : '';
}
private checkValidations() {
const validators = !!this._required
? [this.validationService.requireMatch(this._options)]
: [this.validationService.requireMatchWithEmpty(this._options)];
this.control.setValidators(validators);
this.control.updateValueAndValidity();
}
private _filter(value: string): SelectOption[] {
const filterValue = value ? value.toString().toLowerCase() : '';
if (this.lastValue && filterValue !== this.lastValue && filterValue === '' && this.selectionChange)
this.selectionChange({ option: { value: this.checkControlValue() } });
this.lastValue = filterValue;
return this._options ? this._options.filter(option => option.label.toLowerCase().includes(filterValue)) : this._options;
}
private disableControl(value: boolean) {
if (value) {
this.control.disable({ emitEvent: false });
} else {
this.control.enable({ emitEvent: false });
}
}
getLabelName(value: string) {
const result = this._options ? this._options.find(option => option.value === value) : null;
return result ? result.label : undefined;
}
}

A custom Validation service to validate that the user’s input matches some of the options:

import { Injectable } from "@angular/core";
import { AbstractControl } from "@angular/forms";

@Injectable()
export class ValidationService {
getValidatorErrorMessage(validatorName: string, validatorValue?: any) {
const config = {
required: "This field is required",
requireMatch: "Please, enter a valid option"
};
return config[validatorName];
}
requireMatch(options) {
return (control: AbstractControl) => {
const selection: any =
options && options.find(o => o.value === control.value)
? null
: { requireMatch: true };
return selection;
};
}
requireMatchWithEmpty(options) {
return (control: AbstractControl) => {
const selection: any =
(options && options.find(o => o.value === control.value)) ||
!control.value
? null
: { requireMatch: true };
return selection;
};
}
}

How are we going to use it ?

HTML:

<form class="example-form">   <app-auto-complete
[control]="form.get('myControl')"
[options]="options1"
placeholder="Pick one"
[required]="true"
>
</app-auto-complete>

<app-auto-complete
[control]="form.get('myControl2')"
[options]="options1"
placeholder="Pick another one"
[required]="true"
>
</app-auto-complete>
</form>

TS:

export class AutocompleteFilterExample implements OnInit {
myControl = new FormControl();
myControl2 = new FormControl();
options: string[] = ['One', 'Two', 'Three'];
options2: string[] = ['Four', 'Five', 'Six'];
ngOnInit() {}
}

Easier to read right ?

Lets check the improvements of creating this component, these also apply for every other you create this way :

1- We divide the logic of our component from the Angular Material’s component logic.

2- Easier to read code.

3- Easier to test, because it’s a reusable component, once it passes the tests it’s going to work the same across the whole app.

4- Same style, because it’s a reusable component it’s going to have the same styling across the whole app, evading mistakes.

5- We can increase the Angular Material’s component functionality, in this case we check for options so when it’s empty the input is going to be disabled, we also check that the user’s input matches one of the options.

So that’s all ! I hope this helps your code and project in a future, thanks for reading !

I want also to invite you to my Front End Community over Discord ! where I do mentoring and give classes trough my YouTube channel.
Happy coding everyone !!

Nerd For Tech

Nerd For Tech

NFT is an Educational Media House. Our mission is to bring the invaluable knowledge and experiences of experts from all over the world to the novice. To know more about us, visit https://www.nerdfortech.org/.

Alan Buscaglia

Written by

I'm an Engineer Front End Developer Architect with expertise in Angular with ngrx/store. Huge experience in big data flow applications.

Nerd For Tech

NFT is an Educational Media House. Our mission is to bring the invaluable knowledge and experiences of experts from all over the world to the novice. To know more about us, visit https://www.nerdfortech.org/.