Contents

Building Dynamic Forms with Angular Material

Contents

Building Dynamic Forms with Angular Material

Using Autocomplete and Chips input fields.

Our end result will look like the following:

https://cdn-images-1.medium.com/max/800/1*yeR4dOnEsOVwvTPg9iCmDw.png

We select a technician using an autocomplete dropdown. Then assign services to the selected technician using chips autocomplete.

I will share code excerpts that are meant for illustration purposes only. The full running code is in StackBlitz and GitHub.

I also assume that you are familiar with Angular and Angular Material. Let’s jump right in.

Let’s dissect the essential parts of the code in the succeeding sections.

Line items

A “line item” is a single item with a pair of technician and services input fields.

https://cdn-images-1.medium.com/max/800/1*qUMrU3OYhyWuapZFVcn8gA.png

We are building a dynamic form comprising an array of line items. We can add or remove line items that we need, depending on our input.

https://cdn-images-1.medium.com/max/800/1*I3JCZ_Nz-Gg1OleWxPcnwQ.png

We use Angular’s form group to wrap these line items. This makes our line items accessible as a property. We can also add or remove items in the line items array.

export class DynamicFormComponent implements OnInit {
    serviceTechnicianForm: FormGroup;
    
    // More code here.
    
    get lineItems(): FormArray {
        return this.serviceTechnicianForm.get("lineItems") as FormArray
    }
    
    public addLineItem(): void {
        this.lineItems.push(this.newLineItem());
        // More code here.
    }
    
    public removeLineItem(index: number): void {
        this.lineItems.removeAt(index);
    }
}

TheaddLineItem()andremoveLineItem()methods will be called by our add (+) and remove (-) buttons, respectively. You can find its corresponding component template code below.

Add a line item button.

<button (click)="addLineItem()" mat-mini-fab color="secondary" aria-label="Example icon button with a plus one icon">
    <mat-icon>add</mat-icon>
</button>

Remove the line item button.

<button class="delete" *ngIf="lineIndex > 0" (click)="removeLineItem(lineIndex)" mat-mini-fab color="warn" aria-label="Example icon button with a plus one icon">
    <mat-icon>delete</mat-icon>
</button>

In our component’s constructor, we initialize our form group with an empty array of line items using the form builder.

export class DynamicFormComponent implements OnInit {
    // ..
    constructor(private formBuilder: FormBuilder) {
        this.serviceTechnicianForm = this.formBuilder.group({
            lineItems: this.formBuilder.array([]),
        });
        // ..
    }
    // ..
}

Using the form group’s line items, we use a for loop in our template to render the individual line items.

<form [formGroup]="serviceTechnicianForm" (ngSubmit)="onSubmit()"> <div formArrayName="lineItems"> <div *ngFor="let lineItem of lineItems.controls; let lineIndex = index"> <div class="service-technician-form-fields" [formGroupName]="lineIndex"> <!--Technician--> <mat-form-field class="technician" appearance="fill"> <mat-label>Technician</mat-label> <!--Input field code here--> </mat-form-field> <!--Mat chips--> <mat-form-field class="services" appearance="fill"> <mat-label>Services</mat-label> <!--Input field code here--> </mat-form-field> <!--Remove button--> <!--Remove--> <button class="delete" *ngIf="lineIndex > 0" (click)="removeLineItem(lineIndex)" mat-mini-fab color="warn" aria-label="Example icon button with a plus one icon"> <mat-icon>delete</mat-icon> </button> </div> </div> </div></form><div> <button (click)="addLineItem()" mat-mini-fab color="secondary" aria-label="Example icon button with a plus one icon"> <mat-icon>add</mat-icon> </button></div>

Adding and removing line items are straightforward. However, implementing autocomplete and chips autocomplete into the line items is another story.

Let’s dissect the autocomplete implementation.

Autocomplete

We are usingAngular Material’s Autocompletefor selecting the technicians.

https://cdn-images-1.medium.com/max/800/1*l27vHSz0hba506DgkSJLTw.png

We will first initialize the values of our autocomplete dropdown.

export class DynamicFormComponent implements OnInit { // .. ngOnInit(): void { // code here... this.technicians = [ {employee_name: 'John', id: '1'}, {employee_name: 'Paul', id: '2'}, {employee_name: 'George', id: '3'}, {employee_name: 'Ringo', id: '4'}, ]; this.initTechnicianForm(0); // code here... }}

ThefilterTechnicians()method handles the filtering of the entered values in our input box. This is so our app knows what values it should display in the dropdown based on the user’s input.

export class DynamicFormComponent implements OnInit { // .. filteredTechnicians: (Observable<Technician[]> | undefined)[] = []; ngOnInit(): void { // code here... this.technicians = [ {employee_name: 'John', id: '1'}, {employee_name: 'Paul', id: '2'}, {employee_name: 'George', id: '3'}, {employee_name: 'Ringo', id: '4'}, ]; this.initTechnicianForm(0); // code here... } initTechnicianForm(index: number): void { const arrayControl = this.serviceTechnicianForm.get("lineItems") as FormArray; this.filteredTechnicians[index] = arrayControl.at(index).get("technician")?.valueChanges.pipe( startWith(''), map(value => this.filterTechnicians(value || '')), ); } filterTechnicians(value: string): Technician[] { const filterValue = value.toLowerCase(); return this.technicians.filter( option => option.employee_name.toLowerCase().includes(filterValue) ); } // ..}

TheinitTechnicianForm()method initializes the form’s observable. Each line item has one technician form control entry. Hence we need theindexfor each observable.

A value change in the technician autocomplete dropdown invokes thefilterTechnicians()method. It filters the list of technicians based on the user’s input in the autocomplete textbox.

Themat-autocompletecomponent in the template uses the technician option values. ThengOnInit()method initializes these values.

<mat-form-field class="technician" appearance="fill"> <mat-label>Technician</mat-label> <input type="text" placeholder="Pick one" aria-label="Technician" matInput [matAutocomplete]="autoTechnicians" formControlName="technician"> <mat-autocomplete #autoTechnicians="matAutocomplete" (optionSelected)="updateTechnicianId(lineIndex, $event)"> <mat-option *ngFor="let option of filteredTechnicians[lineIndex] | async" [value]="option.employee_name"> {{option.employee_name}} </mat-option> </mat-autocomplete> </mat-form-field>

Chips Autocomplete

We are usingAngular Material’s Chips Autocompleteto handle every selected technician’s services.

The Chips Autocomplete’s implementation with dynamic forms is similar to our Autocomplete in the previous section. It only needs to handle a few other extra functionalities that the autocomplete doesn’t:

  • Keep track of the multiple selected services. The component displays these selected services as chips.
  • Ability to remove selected services.

https://cdn-images-1.medium.com/max/800/1*5qV5T7vFbIVIL59rvCvrVw.png

The code below initializes theservicesproperty with the list in our services dropdown. The chips autocomplete component uses this list to populate the services options dropdown.

export class DynamicFormComponent implements OnInit { // .. ngOnInit(): void { // code here... this.services = [ {service_name: 'Service 1', id: '1', price: 100}, {service_name: 'Service 2', id: '2', price: 200}, {service_name: 'Service 3', id: '3', price: 300}, {service_name: 'Service 4', id: '4', price: 400}, {service_name: 'Service 5', id: '5', price: 500}, ]; this.initServiceItemForm(0); // .. } initServiceItemForm(index: number): void { const arrayControl = this.serviceTechnicianForm.get("lineItems") as FormArray; this.filteredServices[index] = arrayControl.at(index).get("services")?.valueChanges.pipe( startWith(''), map(value => this.filterServices(value || '')), ); } // .. }

ThefilterServices()method handles the filtering. This is similar to how we filter technicians in the previous section. It accepts a string input, uses the JavaScriptfilter()method to filter the results fromserviceslist, and returns the filtered list.

export class DynamicFormComponent implements OnInit { // .. private filterServices(service: string): Service[] { const filterValue = service.toLowerCase(); return this.services.filter( service => service.service_name.toLowerCase().includes(filterValue) ); } //..}

We will use theselectService()method to keep track of the selected services to be displayed as chips. TheremoveService()method is called when the user removes a selected service chip i.e. by clicking the (x) on the chip.

export class DynamicFormComponent implements OnInit { //.. selectService(lineIndex: number, event: MatAutocompleteSelectedEvent): void { const selectedService = this.services.find( service => service.service_name === event.option.value ); if (this.selectedServices[lineIndex] === undefined) { this.selectedServices[lineIndex] = []; } if (selectedService !== undefined) { this.selectedServices[lineIndex]?.push(selectedService); const arrayControl = this.serviceTechnicianForm.get("lineItems") as FormArray; arrayControl.at(lineIndex).get("services")?.setValue(null); } this.updateServiceIds(lineIndex); } // .. updateServiceIds(lineIndex: number): void { this.lineItemParams[lineIndex].serviceIds = this.selectedServices[lineIndex]?.map( service => service.id ) || []; } removeService(lineIndex: number, serviceId: string): void { this.selectedServices[lineIndex] = this.selectedServices[lineIndex]?.filter( service => service.id !== serviceId ); } //..}

The code below is part of the component template that uses thechips autocomplete component.

<mat-form-field class="services" appearance="fill"> <mat-label>Services</mat-label> <mat-chip-grid #chipGrid aria-label="Select services"> <mat-chip-row *ngFor="let service of selectedServices[lineIndex]" (removed)="removeService(lineIndex, service.id)"> {{service.service_name}} : ${{service.price}} <button matChipRemove [attr.aria-label]="'remove ' + service.service_name"> <mat-icon>cancel</mat-icon> </button> </mat-chip-row> </mat-chip-grid> <input type="text" placeholder="Select service..." matInput [matChipInputFor]="chipGrid" [matAutocomplete]="autoServices" [matChipInputSeparatorKeyCodes]="separatorKeysCodes" formControlName="services"> <mat-autocomplete #autoServices="matAutocomplete" (optionSelected)="selectService(lineIndex, $event)"> <mat-option *ngFor="let service of filteredServices[lineIndex] | async" [value]="service.service_name"> {{service.service_name}} : ${{service.price}} </mat-option> </mat-autocomplete></mat-form-field>
  • It displays the services dropdown.
  • It keeps track of the selected services by callingselectService()method when an option is selected.
  • It callsremoveService()method when a selected chip is removed.

Constructing the data object

ThelineItemParamsproperty that stores the final data object that we could submit in our backend. It keeps track of the selected employees and services in the form.

export interface LineItemParams { technicianId: string; serviceIds: string[]; additionalPrice: number;}export class DynamicFormComponent implements OnInit { //.. lineItemParamsList: LineItemParams[] = []; //.. ngOnInit(): void { //.. this.lineItemParamsList.push({ technicianId: '', serviceIds: [], additionalPrice: 0, }); //.. } //.. updateServiceIds(lineIndex: number): void { this.lineItemParamsList[lineIndex].serviceIds = this.selectedServices[lineIndex]?.map(service => service.id) || []; } updateTechnicianId(lineIndex: number, event: MatAutocompleteSelectedEvent): void { const technician = this.technicians.find(technician => technician.employee_name === event.option.value); this.lineItemParamsList[lineIndex].technicianId = technician?.id || ''; } //.. onSubmit(): void { console.log(this.serviceTechnicianForm.value); console.log('lineItemParams:', this.lineItemParamsList); } //.. public addLineItem(): void { this.lineItems.push(this.newLineItem()); const index = this.lineItems.length - 1; this.initTechnicianForm(index); this.initServiceItemForm(index); this.lineItemParamsList[index] = { technicianId: '', serviceIds: [], additionalPrice: 0, }; } //..}

The constructed data objectlineItemParamsListis printed in the console for your reference when you click Submit.

You can find the full source code in Stackblitz and GitHub.

Stackblitz

GitHub

More content atPlainEnglish.io. Sign up for ourfree weekly newsletter. Join ourDiscordcommunity and follow us onTwitter,LinkedInandYouTube.

Learn how to build awareness and adoption for your startup withCircuit.