← Back to list

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?

Raiyan Rashid Prodhan · 2025-07-22 14:21 · 62 claps · 5.5 min read
#angular #angular-material #dynamic-form #searchable-dropdown #dropdown
Open on Medium ↗
Wiki topics: 🌐 · Web Development

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

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