Build a Dynamic Multi-Select Form in Angular That Just Works
Want to create a smart Angular form that feels intuitive and delightful to use?
Build a Dynamic Multi-Select Form in Angular That Just Works
Want to create a smart Angular form that feels intuitive and delightful to use?
In this short and simple guide, I’ll show you how to build a dynamic form that lets users select multiple locations using chips and autocomplete. It’s perfect for anything that needs multi-select logic.
This form looks good, feels fast, and is actually fun to interact with. Let’s build it step by step.
🍽️ What We’re Building
In this example, we’ll be building a restaurant location tracker form. It will let users add multiple restaurant entries, and for each one, select several locations using an easy chip-based interface.
🧠 Step 1: Create the Form Group
We start by building a dynamic form structure using FormArray. For this example we will be using a restaurant location form. Each restaurant gets its own group, and inside each group is a multi-select location field.
🔹 Code:
ngOnInit(): void {
this.restaurantForm = this.fb.group({
restaurant_list: this.fb.array([this.createRestaurantGroup()]),
});
this.setupLocationSearch(0);
}
createRestaurantGroup(): FormGroup {
return this.fb.group({
location: [[], Validators.required],
});
}
This gives us a flexible structure to add or remove restaurants as needed.
🔍 Step 2: Enable Location Search with Autocomplete
To allow smart searching, we create a separate FormControl for each restaurant’s search input.
🔹 Code:
setupLocationSearch(index: number): void {
this.locationSearchControls[index] = new FormControl("");
this.filteredOptions[index] = this.filterLocations("", index);
}
This control listens to input and filters location suggestions.
🧹 Step 3: Filter Locations Dynamically
When the user types, we show matching locations that are not already selected.
🔹 Code:
filterLocations(value: string, index: number): Location[] {
const search = value.toLowerCase();
const selected = this.getSelectedLocationIds(index);
return this.locations.filter(
(loc) =>
loc.name.toLowerCase().includes(search) && !selected.includes(loc.id)
);
}
Now your suggestions stay fresh and prevent duplicates.
➕ Step 4: Select Locations and Add as Chips
Once a user selects a location, we add its ID to the form and reset the search box.
🔹 Code:
selectLocation(id: number, index: number): void {
const current = this.getSelectedLocationIds(index);
if (!current.includes(id)) {
this.restaurantList.at(index).get("location")?.setValue([...current, id]);
}
this.locationSearchControls[index].setValue("");
this.filteredOptions[index] = this.filterLocations("", index);
}
Each selected location shows up as a chip inside the input.
❌ Step 5: Remove Chips on Click
Let users remove a selected location easily by clicking the “X” on a chip.
🔹 Code:
removeLocation(id: number, index: number): void {
const updated = this.getSelectedLocationIds(index).filter(
(locId) => locId !== id
);
this.restaurantList.at(index).get("location")?.setValue(updated);
this.filteredOptions[index] = this.filterLocations(
this.locationSearchControls[index].value,
index
);
}
This keeps the UX clean and easy to control.
🧱 Step 6: Build the HTML Template
We use Angular Material components like mat-chip-grid, mat-autocomplete, and reactive forms to bring everything together.
🔹 Code:
<mat-form-field class="full-width">
<mat-label>Location</mat-label>
<mat-chip-grid #chipGrid>
<mat-chip-row *ngFor="let locId of item.get('location')?.value" (removed)="removeLocation(locId, i)">
{{ getLocationName(locId) }}
<button matChipRemove><mat-icon>cancel</mat-icon></button>
</mat-chip-row>
</mat-chip-grid>
<input
placeholder="Search location"
[formControl]="locationSearchControls[i]"
[matAutocomplete]="auto"
[matChipInputFor]="chipGrid"
(input)="onSearchChange($event, i)"
(matChipInputTokenEnd)="addLocation($event, i)"
/>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selectLocation($event.option.value, i)">
<mat-option *ngFor="let loc of filteredOptions[i]" [value]="loc.id">
{{ loc.name }}
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="item.get('location')?.hasError('required')">
Location is required
</mat-error>
</mat-form-field>
This gives us a smooth multi-select user interface for each restaurant.
📤 Step 7: Submit and Display Results
Finally, submit the form and show the selected data on the screen.
🔹 Code:
onFormSubmit(): void {
if (this.restaurantForm.valid) {
this.submittedData = this.restaurantForm.value;
} else {
this.restaurantForm.markAllAsTouched();
}
}
And in the template:
<div class="output-container">
<h3>Submitted Data</h3>
<pre *ngIf="submittedData">{{ submittedData | json }}</pre>
</div>
🎨 Styling to Make It Pretty
A clean layout makes a big difference. We used simple SCSS to space things out.
🔹 Code:
.flex-container {
display: flex;
gap: 2rem;
padding: 2rem;
flex-wrap: wrap;
}
.form-container {
flex: 1 1 50%;
min-width: 300px;
}
.output-container {
flex: 1 1 40%;
background: #f3f3f3;
padding: 1rem;
border-radius: 8px;
}
.card {
padding: 1rem;
margin-bottom: 1.5rem;
border: 1px solid #ddd;
border-radius: 6px;
background-color: white;
}
.full-width {
width: 100%;
}
🧾 Full Source Code
Here’s the full code used in this guide. Everything is ready to copy and drop into your Angular app.
🧱 HTML
<div class="flex-container">
<div class="form-container">
<form [formGroup]="restaurantForm" (ngSubmit)="onFormSubmit()">
<div formArrayName="restaurant_list">
<div *ngFor="let item of restaurantList.controls; let i = index" [formGroupName]="i" class="card">
<h4>Restaurant {{ i + 1 }}</h4>
<mat-form-field class="full-width">
<mat-label>Location</mat-label>
<mat-chip-grid #chipGrid>
<mat-chip-row *ngFor="let locId of item.get('location')?.value" (removed)="removeLocation(locId, i)">
{{ getLocationName(locId) }}
<button matChipRemove><mat-icon>cancel</mat-icon></button>
</mat-chip-row>
</mat-chip-grid>
<input
placeholder="Search location"
[formControl]="locationSearchControls[i]"
[matAutocomplete]="auto"
[matChipInputFor]="chipGrid"
(input)="onSearchChange($event, i)"
(matChipInputTokenEnd)="addLocation($event, i)"
/>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selectLocation($event.option.value, i)">
<mat-option *ngFor="let loc of filteredOptions[i]" [value]="loc.id">
{{ loc.name }}
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="item.get('location')?.hasError('required')">
Location is required
</mat-error>
</mat-form-field>
</div>
</div>
<button mat-raised-button color="primary" type="submit" [disabled]="!restaurantForm.valid">Submit</button>
<button mat-button type="button" (click)="addRestaurant()">Add Another Restaurant</button>
</form>
</div>
<div class="output-container">
<h3>Submitted Data</h3>
<pre *ngIf="submittedData">{{ submittedData | json }}</pre>
</div>
</div>
🎨 SCSS
.flex-container {
display: flex;
gap: 2rem;
padding: 2rem;
flex-wrap: wrap;
}
.form-container {
flex: 1 1 50%;
min-width: 300px;
}
.output-container {
flex: 1 1 40%;
background: #f3f3f3;
padding: 1rem;
border-radius: 8px;
}
.card {
padding: 1rem;
margin-bottom: 1.5rem;
border: 1px solid #ddd;
border-radius: 6px;
background-color: white;
}
.full-width {
width: 100%;
}
🧠 TypeScript
import { Component } from "@angular/core";
import {
FormArray,
FormBuilder,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
import { MatChipsModule } from "@angular/material/chips";
import { MatAutocompleteModule } from "@angular/material/autocomplete";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatIconModule } from "@angular/material/icon";
import { MatInputModule } from "@angular/material/input";
import { MatButtonModule } from "@angular/material/button";
import { CommonModule } from "@angular/common";
interface Location {
id: number;
name: string;
}
@Component({
selector: "app-multi-select",
standalone: true,
imports: [
FormsModule,
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatChipsModule,
MatAutocompleteModule,
MatIconModule,
MatButtonModule,
CommonModule,
],
templateUrl: "./multi-select.html",
styleUrl: "./multi-select.scss",
})
export class MultiSelect {
restaurantForm!: FormGroup;
locationSearchControls: FormControl[] = [];
filteredOptions: Location[][] = [];
submittedData: any;
locations: Location[] = [
{ id: 1, name: "Bangladesh" },
{ id: 2, name: "Canada" },
{ id: 3, name: "Morocco" },
{ id: 4, name: "Sweden" },
{ id: 5, name: "New Zealand" },
{ id: 6, name: "Argentina" },
{ id: 7, name: "Vietnam" },
{ id: 8, name: "Kenya" },
{ id: 9, name: "Turkey" },
{ id: 10, name: "Portugal" },
];
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.restaurantForm = this.fb.group({
restaurant_list: this.fb.array([this.createRestaurantGroup()]),
});
this.setupLocationSearch(0);
}
createRestaurantGroup(): FormGroup {
return this.fb.group({
location: [[], Validators.required],
});
}
get restaurantList(): FormArray {
return this.restaurantForm.get("restaurant_list") as FormArray;
}
addRestaurant(): void {
const index = this.restaurantList.length;
this.restaurantList.push(this.createRestaurantGroup());
this.setupLocationSearch(index);
}
deleteRestaurant(index: number): void {
if (this.restaurantList.length > 1) {
this.restaurantList.removeAt(index);
this.locationSearchControls.splice(index, 1);
this.filteredOptions.splice(index, 1);
}
}
setupLocationSearch(index: number): void {
this.locationSearchControls[index] = new FormControl("");
this.filteredOptions[index] = this.filterLocations("", index);
}
filterLocations(value: string, index: number): Location[] {
const search = value.toLowerCase();
const selected = this.getSelectedLocationIds(index);
return this.locations.filter(
(loc) =>
loc.name.toLowerCase().includes(search) && !selected.includes(loc.id)
);
}
getSelectedLocationIds(index: number): number[] {
return this.restaurantList.at(index).get("location")?.value || [];
}
onSearchChange(event: Event, index: number): void {
const input = event.target as HTMLInputElement;
this.filteredOptions[index] = this.filterLocations(input.value, index);
}
selectLocation(id: number, index: number): void {
const current = this.getSelectedLocationIds(index);
if (!current.includes(id)) {
this.restaurantList
.at(index)
.get("location")
?.setValue([...current, id]);
}
this.locationSearchControls[index].setValue("");
this.filteredOptions[index] = this.filterLocations("", index);
}
addLocation(event: any, index: number): void {
const input = event.input;
const value = (event.value || "").trim();
if (value) {
const match = this.locations.find(
(loc) => loc.name.toLowerCase() === value.toLowerCase()
);
if (match) this.selectLocation(match.id, index);
}
if (input) input.value = "";
}
removeLocation(id: number, index: number): void {
const updated = this.getSelectedLocationIds(index).filter(
(locId) => locId !== id
);
this.restaurantList.at(index).get("location")?.setValue(updated);
this.filteredOptions[index] = this.filterLocations(
this.locationSearchControls[index].value,
index
);
}
getLocationName(id: number): string {
return this.locations.find((loc) => loc.id === id)?.name || "";
}
onFormSubmit(): void {
if (this.restaurantForm.valid) {
this.submittedData = this.restaurantForm.value;
} else {
this.restaurantForm.markAllAsTouched();
}
}
}
🎯 Final Result

final_output.png
✨ What You’ve Built
✅ Dynamic form with multiple sections ✅ Multi-select using chips and autocomplete ✅ Real-time feedback and filtering ✅ Simple, readable and clean UI
This is something you can plug into almost any Angular project. Whether it’s restaurants, categories, tags or anything else — this approach will scale.
Want More Like This?
If this saved your time or gave you ideas:
👉 Smash that Like button 💬 Drop a Comment with your use case 👥 Hit Follow for more Angular tutorials made simple
Stay curious, keep building — and make your forms awesome!
메타데이터
- post_id
- c22cb11608fc
- slug
- build-a-dynamic-multi-select-form-in-angular-that-just-works-c22cb11608fc
- url
- https://medium.com/@rrprodhan1/build-a-dynamic-multi-select-form-in-angular-that-just-works-c22cb11608fc
- canonical_url
- https://medium.com/@rrprodhan1/build-a-dynamic-multi-select-form-in-angular-that-just-works-c22cb11608fc
- author_url
- https://medium.com/@rrprodhan1
- status
- ok
- fetched_at
- 2026-07-26 20:10:45