Angular Material Custom mat-select with search
Have you ever found yourself having trouble managing Angular HTML lines of code? It is easy to create components that use Angular Forms but in a matter of time, they tend to get out of hand. So let's think of having a form that requires a lot of select options and some of them will require server-side filtering. The first option in my head is a library called “Ng-select”, but is that a really great choice?
As you can see above is a sample of a select control using Ng-select, but having two or more selection controls in your form will be hard to be manageable, and the code will be smelly and easily breakable. I have tried once this library, but I ended up creating another layer to remove all the boilerplate code. Consequently ended up with a “tech debt” and depended on another external API in my layout. After all of this, I removed that library and continued with Angular Material. Ng-Select supports Material layout and all form fields appearances, I mostly like ‘outline’ appearance. But in this case “Ng-select” it’s not so professional and it has a poor implementation of ‘outline’ appearance.
Usually, I prefer working with Angular Material UI because it’s easy and has a lot of documentation in there. Also, I am more of a backend guy and I am not too much behind doing things with pure HTML and CSS or even Bootstrap.
In my case, It was critical to have asap a Mat-select input with search functionality statically and server-side like in the image above. By simple thinking, it can be achieved by having a mat-select with a mat-option that has inside a matInput. This matInput purpose is to do the filtering, so we will need a FormControl for this field. Afterward, we will subscribe to formControl valueChanges and will do all the logic inside it. Below we will develop a mat-select that will be reusable.
Reusable Input Component (mat-select-search)
This component will be a child component and it’s recommended to be used as a shared component. Knowledge of Parent-Child communication is required. The main idea is to break the “parent” form into multiple reusable child components and avoid redundant code.
Create ”MatSelectSearchComponent”
Use Angular schematics to create this component. As you can see let’s add some inputs to our components so Parent can send data to the child.
@Component({
selector: 'app-mat-select-search',
templateUrl: './mat-select-search.component.html',
styleUrls: ['./mat-select-search.component.css'],
})
export class MatSelectSearchComponent implements OnInit, OnChanges {@Input() appearance: 'legacy' | 'standard' | 'fill' | 'outline' = 'legacy';
@Input() placeholder: string = "Select X";
@Input() items: Array<any> | Observable<Array<any>> = [];
@Input() bindValueKey: string = "value";
@Input() bindLabelKey: string = "label";
@Input() searchPlaceholder: string = "Search your item ...";
}
filterFormControl: FormControl = new FormControl('') private isServerSide: boolean = true;
praivate currentStaticItems: Array<any> = []; ngOnInit(): void {
}
ngOnChanges(changes: SimpleChanges): void {
if (this.items instanceof Array) {
this.currentStaticItems = this.items;
this.items = of(this.items);
this.isServerSide = false;
}
}
}
Expect the inputs we don’t have anything special, so let us discuss declared variables:
- appearance: will keep the information in regard to our mat-form-field layout
- placeholder: the value that mat-select placeholder will get.
- items: in other words the options that mat-select will have. As you see, I have made it possible to support items of type Array<any> and Observable<Array<any>>. We can pass a cold observable on “items Input” and we will make it hot or subscribe inside our custom component.
- bindValueKey & bindLabelKey: Since the items input is just a list at the end of the day, this list should be a list of objects. And the mentioned keys are used to get the value and label when we iterate items. By default to get the value we use the “value” key and to take the label we use the “label” key. Example: {label: “London”, value:51}, {label: “Cairo”, value:43}.
- searchPlaceholder: will be used for matInput placeholder that we will use to search.
Accordingly, this is the basic interface for our custom component but don’t get happy the black box will be implemented below. Above I have mentioned two other private fields which are:
- filterFormControl: is responsible to control and access data that is typed in matInput that will be used to filter “items” (front or back filter).
- isServerSide: a flag that will be using different logic of filtering. This field is mentioned on ngOnChanges (this method is called every time when the parent passes new data on input fields) and it will be ‘isServerSide=false’ when the “items” input is an Array type and in this case, we will have a static search (we have all date and we will filter only in frontend). As well, if “items” is an observable type it will be emitted a signal to the parent with the value of the word we searched.
- currentStaticItems: it will keep a copy of old array type items that the parent has passed. We will use this field to do static filtering (it’s like storage for our options). This field will be used only if we have static filtering.
So far let’s take a look at our custom component HTML:
<mat-form-field [appearance]="appearance">
<mat-label>{{placeholder}}</mat-label>
<mat-select>
<!-- matInput inside mat-option which will be used to do the filtering -->
<!-- by subscribing to "filterFormControl.valueChanges.subscribe" -->
<mat-option disabled="true">
<mat-form-field [floatLabel]="'never'" fxFill>
<input matInput [placeholder]="searchPlaceholder"
[formControl]="filterFormControl">
<button matSuffix mat-icon-button aria-label="company"
(click)="filterFormControl.setValue('')">
<mat-icon>clear</mat-icon>
</button>
</mat-form-field>
</mat-option>
<!-- When items will be called by template we are sure that items is an observable -->
<!-- async pipe will do the subscription -->
<mat-option *ngFor="let item of items | async" [value]="item[bindValueKey]">
<span>{{item[bindLabelKey]}}</span>
</mat-option>
</mat-select>
</mat-form-field>`
Imagine if we don’t do this reusable component, we will have this piece of HTML everywhere, and what about having in a form more than 2 selects with server search filtering. Above we will see our reusable component called by its selector from Parent component:
<app-mat-select-search
appearance="outline"
placeholder="Employment type"
searchPlaceholder="Search Item..."
[items]="jobTypeList"
>
</app-mat-select-search>
For the moment we are a little restricted because filtering logic is determined by “items” Input, so if Observable<Array> it will be a server search.
If’ “items” Array static frontend serach.
This can be imporved by having “isServerSide” field as Input, but it will require to cover some other edge cases.
In order to do server-search filtering, we need to communicate with the parent component. In this case, we will use Output annotation so when we type some words in matInput it will stream the typed word to the Parent and it will make the external request. In some of the terms I will use please consult with docs for more information.
Implement from “ControlValueAccessor” interface
For the moment our component will be working and it will be okay visually, but only search matInput is connected to a FormControl and we have mat-select with options, but when we type on search input nothing happens. This is because we haven’t connected the Parent form with our mat-select. There are two ways of achieving this connection:
- Use “@Input() exampleFormControl” and pass from parent the FormControl object that’s needed to be connected with our form control. A getter like this can be used:
getFormControl(name:string): FormControl {
return this.parentDataForm.get(name) as FormControl;
}<app-mat-select-search
[exampleFormControl]="getFormControl('job_type')"
...
[items]="jobTypeList"
>
</app-mat-select-search>
The above code will do the job but there is being passed the FormControl Object reference and it will not look the same as Angular Material form elements where is passed the famous “formControlName” as a string and everything works by magic. (To make it work some logic is needed)
- Use ControlValueAccessor on your reusable component
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
This accessor does the magic by defining an interface that acts as a bridge between the Angular forms and a native element in the DOM. In basic words, it will make it possible to create a connection between mat-select and parent formControl which will use this custom component. We need to implement this interface with all its methods and apply formControlName on our custom component. For more please refer to the official documentation.
How to make a custom form Select component with Angular Material
Now let us jump in code and analyze it.
1. Implement ControlValueAccessor
In order to have more structured and dry code, we should create an abstract component where CotrolValueAccessor is implemented. This abstract component can be extended by every custom component which purpose is to use formControlName from parent component.
Let’s interpret some hot topics on the code sample:
- line10 — we have declared a new field called “FormControlDirective” which is used to synchronize a standalone FormControl instance to a form control element <form [FormControlDirective] =”name”> Exported from: ReactiveFormsModule. In our case, this is mat-select that will be synchronized with FormControl retrieved from the parent component.
- line12 — we are using Input “formCotrol” to pass a FormControl Object as we discussed before
- line16 — This Input will make the sector behave like a Material mat-input-form so you pass formControlName=”location”. In the child component, it will come as a string but with the power of controlContainer and FormControlDirective we will synchronize it with parent FormGroup
- line34 — we are using getter “controlContainer” to retrieve parent form.
2. Implement fitlering
On our custom select search component let’s extend the above abstract component and make it richer by implementing filtering functions for each case (we discussed it earlier in this post).
Let’s analyze major changes on this component:
- line12 — After extending from the abstract component we need to provide “NG_VALUE_ACCESSOR” from angular forms on our component.
- line26 — @ Output() itemFilterServerSide will emit values to parent Component. In our case we will do the emmit manually and only for server-side search will be used output event emitter.
- line37 — listenToFilterFormControlChanges() method is responsible to listen on filter form value changes, when we write a word on search input it will be read (line42). If “isServerSide” it will emit a new value on the parent component. Otherwise, it will do a frontend static search on the items list.
Full upgraded HTML template:
Parent Component:
<app-mat-select-search
fxFill
appearance="outline"
placeholder="Location"
formControlName="location"
searchPlaceholder="Search your location..."
[items]="countriesList"
(itemFilterServerSide)="waitForFilterResponse($event)"
bindLabelKey="name"
bindValueKey="id"
>
</app-mat-select-search>waitForFilterResponse(value: string) {
this.getCountryList({name: value});
}private getCountryList(params: object = {}): void {
// params will be converted in url query params ?name={{value}} this.countriesList = this.geoService.countryList(params);
}
After all this coding experience, we can check above how short and structured our code is now. Still, there are a ton of possibilities to achieve a solution to this problem. For your Information fxFill is a FlexLayoutModule directive.
You can declare this component to a shared module like below, also here are the needed modules to make this component work:
const AngularMaterialModules = [
MatSelectModule,
MatIconModule,
MatButtonModule,
MatInputModule,
]
@NgModule({
declarations: [
MatSelectSearchComponent
],
exports: [
MatSelectSearchComponent,
CommonModule,
ReactiveFormsModule,
],
imports: [
CommonModule,
AngularMaterialModules,
ReactiveFormsModule,
FlexLayoutModule,
]
})
export class SharedModule {
}
Summary
Angular makes it very easy to implement a reusable custom form control using ControlValueAccessor or by just assigning FormControl to your child component. It's easier following dry principles even it will drain some time from developing features, than developing redundant smelly code. Which is impossible to be changed and maintained.
Thanks for reading my post and a Happy New Year! 🎉🎉🎉