Forms
ng-select supports Signal Forms, Reactive Forms and Template-driven Forms through the same control value accessor.
Signal Forms
Section titled “Signal Forms”Import FormField from @angular/forms/signals and bind a field from the tree returned by form(). FormsModule is not required for this integration.
import { signal } from '@angular/core';import { form, FormField, required } from '@angular/forms/signals';import { NgSelectComponent } from '@ng-select/ng-select';
@Component({ imports: [FormField, NgSelectComponent],})export class CityEditor { readonly model = signal({ cityId: null as number | null }); readonly cityForm = form(this.model, (path) => required(path.cityId));}<ng-select [items]="cities" bindLabel="name" bindValue="id" [formField]="cityForm.cityId" />The example also covers initial values before async items arrive, multiple selection, validation, disabled state, and recreating the control with @if.
import { JsonPipe } from '@angular/common';import { ChangeDetectionStrategy, Component, signal } from '@angular/core';import { disabled, form, FormField, minLength, required } from '@angular/forms/signals';import { NgSelectComponent } from '@ng-select/ng-select';
interface City { id: number; name: string;}
@Component({ selector: 'ng-forms-signal-example', templateUrl: './forms-signal-example.component.html', styleUrls: ['./forms-signal-example.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, imports: [FormField, NgSelectComponent, JsonPipe],})export class FormsSignalExampleComponent { readonly cities: City[] = [ { id: 1, name: 'New York' }, { id: 2, name: 'London' }, { id: 3, name: 'Beijing' }, { id: 4, name: 'New Delhi' }, { id: 5, name: 'Paris' }, ]; readonly asyncCities = signal<readonly City[]>([]); readonly cityDisabled = signal(false); readonly multipleVisible = signal(true); readonly model = signal({ cityId: 2 as number | null, cityIds: [1, 3] as number[], }); readonly cityForm = form(this.model, (path) => { required(path.cityId, { message: 'Choose a city' }); minLength(path.cityIds, 1, { message: 'Choose at least one city' }); disabled(path.cityId, { when: () => this.cityDisabled() }); });
loadCities(): void { this.asyncCities.set([...this.cities]); }
clearCities(): void { this.asyncCities.set([]); this.cityForm.cityIds().value.set([1, 3]); }
toggleCityDisabled(): void { this.cityDisabled.update((value) => !value); }
toggleMultipleVisible(): void { this.multipleVisible.update((value) => !value); }}<div class="form-group"> <label for="signal-city">Signal Forms single select</label> <ng-select [items]="cities" bindLabel="name" bindValue="id" labelForId="signal-city" placeholder="Select a city" [formField]="cityForm.cityId" /> @if (cityForm.cityId().touched() && cityForm.cityId().invalid()) { <small class="text-danger">{{ cityForm.cityId().errors()[0].message }}</small> } <br /> <button type="button" class="btn btn-sm btn-secondary" (click)="cityForm.cityId().reset(null)">Reset</button> <button type="button" class="btn btn-sm btn-secondary" (click)="toggleCityDisabled()">Toggle disabled</button></div>
<div class="form-group"> <label for="signal-cities">Async Signal Forms multiselect</label> @if (multipleVisible()) { <ng-select [items]="asyncCities()" bindLabel="name" bindValue="id" labelForId="signal-cities" [multiple]="true" placeholder="Select cities" [formField]="cityForm.cityIds" /> } @if (cityForm.cityIds().touched() && cityForm.cityIds().invalid()) { <small class="text-danger">{{ cityForm.cityIds().errors()[0].message }}</small> } <p class="form-text text-muted"> The form starts with two selected IDs before the item list exists. Load the items to see them mapped to options, and hide/show the control to see the values preserved. </p> <button type="button" class="btn btn-sm btn-secondary" (click)="loadCities()">Load items</button> <button type="button" class="btn btn-sm btn-secondary" (click)="clearCities()">Reset demo</button> <button type="button" class="btn btn-sm btn-secondary" (click)="toggleMultipleVisible()">Show/Hide</button></div>
<pre>{{ model() | json }}</pre>Validation classes
Section titled “Validation classes”Signal Forms keeps validation state on the field tree and does not add Angular's legacy ng-valid, ng-invalid, ng-touched, and ng-dirty classes unless configured. Applications that use those classes with an ng-select theme can enable the compatibility preset:
import { provideSignalFormsConfig } from '@angular/forms/signals';import { NG_STATUS_CLASSES } from '@angular/forms/signals/compat';
bootstrapApplication(AppComponent, { providers: [provideSignalFormsConfig({ classes: NG_STATUS_CLASSES })],});Reactive Forms
Section titled “Reactive Forms”Import ReactiveFormsModule and bind ng-select with formControl, formControlName, or a containing FormGroup.
import { JsonPipe } from '@angular/common';import { ChangeDetectionStrategy, Component } from '@angular/core';import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';import { NgSelectComponent } from '@ng-select/ng-select';
@Component({ selector: 'ng-forms-reactive-example', templateUrl: './forms-reactive-example.component.html', styleUrls: ['./forms-reactive-example.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ReactiveFormsModule, NgSelectComponent, JsonPipe],})export class FormsReactiveExampleComponent { readonly cities = [ { id: 1, name: 'New York' }, { id: 2, name: 'London' }, { id: 3, name: 'Beijing' }, { id: 4, name: 'New Delhi' }, { id: 5, name: 'Paris' }, ]; readonly cityForm = new FormGroup({ cityId: new FormControl<number | null>(2, Validators.required), });
toggleDisabled(): void { const control = this.cityForm.controls.cityId; if (control.disabled) { control.enable(); } else { control.disable(); } }}<form [formGroup]="cityForm" novalidate> <div class="form-group"> <label for="reactive-city">Reactive Forms select</label> <ng-select [items]="cities" bindLabel="name" bindValue="id" labelForId="reactive-city" placeholder="Select a city" formControlName="cityId" /> @if (cityForm.controls.cityId.touched && cityForm.controls.cityId.invalid) { <small class="text-danger">Choose a city</small> } <br /> <button type="button" class="btn btn-sm btn-secondary" (click)="cityForm.controls.cityId.reset()">Reset</button> <button type="button" class="btn btn-sm btn-secondary" (click)="toggleDisabled()">Toggle disabled</button> </div></form>
<pre>{{ cityForm.getRawValue() | json }}</pre>Single select with required validation
Section titled “Single select with required validation”import { Component, OnInit, inject, ChangeDetectionStrategy } from '@angular/core';import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';import { NgbModal } from '@ng-bootstrap/ng-bootstrap';import { NgSelectComponent } from '@ng-select/ng-select';
@Component({ selector: 'ng-forms-single-select-example', templateUrl: './forms-single-select-example.component.html', styleUrls: ['./forms-single-select-example.component.scss'], changeDetection: ChangeDetectionStrategy.Eager, imports: [FormsModule, ReactiveFormsModule, NgSelectComponent],})export class FormsSingleSelectExampleComponent implements OnInit { private fb = inject(FormBuilder); private modalService = inject(NgbModal);
heroForm: FormGroup; ages: any[] = [ { value: '<18', label: 'Under 18' }, { value: '18', label: '18' }, { value: '>18', label: 'More than 18' }, ];
ngOnInit() { this.heroForm = this.fb.group({ age: [null, Validators.required], }); }
toggleAgeDisable() { if (this.heroForm.controls.age.disabled) { this.heroForm.controls.age.enable(); } else { this.heroForm.controls.age.disable(); } }
showConfirm(content) { this.modalService.open(content); }}<form [formGroup]="heroForm" novalidate> <div class="form-group"> <label for="age">Age</label> <ng-select #agesSelect [items]="ages" [selectOnTab]="true" bindValue="value" labelForId="age" (ngModelChange)="showConfirm(confirmation)" placeholder="Select age" formControlName="age"> </ng-select> <br /> <button class="btn btn-secondary btn-sm" (click)="toggleAgeDisable()">Toggle disabled</button> <button (click)="agesSelect.focus()" class="btn btn-sm btn-secondary">Focus select</button> </div></form>
<ng-template #confirmation let-c="close" let-d="dismiss"> <div class="modal-header">Next Step</div> <div class="modal-body">Do you wish to continue?</div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-dismiss="modal" (click)="d()">Cancel</button> <button type="button" class="btn btn-primary" (click)="c()">Yes</button> </div></ng-template>Multi select with clear button
Section titled “Multi select with clear button”import { Component, OnInit, inject, ChangeDetectionStrategy } from '@angular/core';import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';import { NgSelectComponent } from '@ng-select/ng-select';
@Component({ selector: 'ng-forms-multi-select-example', templateUrl: './forms-multi-select-example.component.html', styleUrls: ['./forms-multi-select-example.component.scss'], changeDetection: ChangeDetectionStrategy.Eager, imports: [FormsModule, ReactiveFormsModule, NgSelectComponent],})export class FormsMultiSelectExampleComponent implements OnInit { private fb = inject(FormBuilder);
heroForm: FormGroup; isCitiesControlVisible = true; cities: any[] = [ { id: 1, name: 'New York' }, { id: 2, name: 'London' }, { id: 3, name: 'Beijing' }, { id: 4, name: 'New Delhi (Disabled)', disabled: true }, { id: 5, name: 'Paris' }, ];
ngOnInit() { this.heroForm = this.fb.group({ selectedCitiesIds: [], }); }
toggleCitiesControl() { this.isCitiesControlVisible = !this.isCitiesControlVisible; }
clearCities() { this.heroForm.get('selectedCitiesIds').patchValue([]); }}<form [formGroup]="heroForm"> <div class="form-group"> <label for="state">City</label> @if (isCitiesControlVisible) { <ng-select [items]="cities" bindLabel="name" bindValue="id" labelForId="state" [multiple]="true" placeholder="Select cities" clearAllText="Clear" formControlName="selectedCitiesIds"> </ng-select> } <br /> <button (click)="toggleCitiesControl()" class="btn btn-sm btn-secondary">Show/Hide</button> <button (click)="clearCities()" class="btn btn-sm btn-secondary">Clear</button> </div></form>Reactive form using ng-option
Section titled “Reactive form using ng-option”import { Component, OnInit, inject, ChangeDetectionStrategy } from '@angular/core';import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';import { NgOptionComponent, NgSelectComponent } from '@ng-select/ng-select';
@Component({ selector: 'ng-forms-with-options-example', templateUrl: './forms-with-options-example.component.html', styleUrls: ['./forms-with-options-example.component.scss'], changeDetection: ChangeDetectionStrategy.Eager, imports: [FormsModule, ReactiveFormsModule, NgSelectComponent, NgOptionComponent],})export class FormsWithOptionsExampleComponent implements OnInit { private fb = inject(FormBuilder);
basePath; heroForm: FormGroup;
ngOnInit() { // The docs site is served under /ng-select (locally and on GitHub Pages); StackBlitz serves at the root. this.basePath = window.location.pathname.startsWith('/ng-select') ? '/ng-select' : ''; this.heroForm = this.fb.group({ heroId: 'batman', agree: null, }); }}<form [formGroup]="heroForm" novalidate> <div class="form-row"> <div class="form-group col-md-6"> <label for="yesno">Not searchable</label> <ng-select appearance="outline" #agreeSelect labelForId="yesno" [searchable]="false" formControlName="agree"> <ng-option [value]="true">Yes</ng-option> <ng-option [value]="false">No</ng-option> </ng-select> <br /> <button (click)="agreeSelect.focus()" class="btn btn-sm btn-secondary">Focus select</button> </div> <div class="form-group col-md-6"> <label for="heroId">Basic select</label> <ng-select appearance="outline" [searchable]="false" [clearable]="false" labelForId="heroId" formControlName="heroId"> <ng-option value="batman"> <img src="{{ basePath }}/assets/batman.png" width="20px" height="20px" /> Batman </ng-option> <ng-option value="spidey"> <img src="{{ basePath }}/assets/spidey.png" width="20px" height="20px" /> Spider-Man & Goblin </ng-option> <ng-option value="thor"> <img src="{{ basePath }}/assets/thor.png" width="20px" height="20px" /> Thor </ng-option> </ng-select> </div> </div></form>Reactive Forms using async data
Section titled “Reactive Forms using async data”import { Component, OnInit, inject, ChangeDetectionStrategy } from '@angular/core';import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';import { NgOptionTemplateDirective, NgSelectComponent, NgSelectComponent as NgSelectComponent_1 } from '@ng-select/ng-select';import { delay } from 'rxjs/operators';import { DataService } from '../data.service';import { NgOptionHighlightDirective } from '@ng-select/ng-option-highlight';
@Component({ selector: 'ng-forms-async-data-example', templateUrl: './forms-async-data-example.component.html', styleUrls: ['./forms-async-data-example.component.scss'], changeDetection: ChangeDetectionStrategy.Eager, imports: [FormsModule, ReactiveFormsModule, NgSelectComponent_1, NgOptionTemplateDirective, NgOptionHighlightDirective],})export class FormsAsyncDataExampleComponent implements OnInit { private fb = inject(FormBuilder); private dataService = inject(DataService);
heroForm: FormGroup; albums = []; allAlbums = [];
ngOnInit() { this.loadAlbums(); this.heroForm = this.fb.group({ album: '', }); }
openSelect(select: NgSelectComponent) { select.open(); }
closeSelect(select: NgSelectComponent) { select.close(); }
selectAlbumsRange(from, to) { this.albums = this.allAlbums.slice(from, to); }
selectFirstAlbum() { this.heroForm.get('album').patchValue(this.albums[0].id); }
private loadAlbums() { this.dataService .getAlbums() .pipe(delay(500)) .subscribe((albums) => { this.allAlbums = albums; this.albums = [...this.allAlbums]; this.selectFirstAlbum(); }); }}<form [formGroup]="heroForm"> <div class="form-group"> <label for="album">Loading async data</label> <ng-select [items]="albums" #select bindLabel="title" dropdownPosition="auto" bindValue="id" labelForId="album" placeholder="Select album" [virtualScroll]="true" formControlName="album"> <ng-template ng-option-tmp let-item="item" let-search="searchTerm"> <div> <span>Title: </span><span [ngOptionHighlight]="search">{{ item.title }}</span> </div> <small><b>Id:</b> {{ item.id }} | <b>UserId:</b> {{ item.userId }}</small> </ng-template> </ng-select> <small class="form-text text-muted">Albums data from backend using HttpClient.</small> <br /> <button class="btn btn-secondary btn-sm" (click)="selectFirstAlbum()">Select first album</button> <button class="btn btn-secondary btn-sm" (click)="selectAlbumsRange(0, 10)">Set 0-10 albums</button> <button class="btn btn-secondary btn-sm" (click)="selectAlbumsRange(10, 20)">Set 10-20 albums</button> <button (click)="openSelect(select)" class="btn btn-sm btn-secondary">Open</button> <button (click)="closeSelect(select)" class="btn btn-sm btn-secondary">Close</button> </div></form>Reactive Forms with a custom template
Section titled “Reactive Forms with a custom template”import { Component, OnInit, inject, ChangeDetectionStrategy } from '@angular/core';import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';import { NgbModal } from '@ng-bootstrap/ng-bootstrap';import { DataService } from '../data.service';import { NgLabelTemplateDirective, NgOptionTemplateDirective, NgSelectComponent } from '@ng-select/ng-select';import { NgOptionHighlightDirective } from '@ng-select/ng-option-highlight';
@Component({ selector: 'ng-forms-custom-template-example', templateUrl: './forms-custom-template-example.component.html', styleUrls: ['./forms-custom-template-example.component.scss'], changeDetection: ChangeDetectionStrategy.Eager, imports: [FormsModule, ReactiveFormsModule, NgSelectComponent, NgLabelTemplateDirective, NgOptionTemplateDirective, NgOptionHighlightDirective],})export class FormsCustomTemplateExampleComponent implements OnInit { private fb = inject(FormBuilder); private modalService = inject(NgbModal); private dataService = inject(DataService);
heroForm: FormGroup; photos = [];
ngOnInit() { this.loadPhotos();
this.heroForm = this.fb.group({ photo: '', }); }
selectFirstPhoto() { this.heroForm.get('photo').patchValue(this.photos[0].thumbnailUrl); }
openModal(content) { this.modalService.open(content); }
changePhoto(photo) { this.heroForm.get('photo').patchValue(photo ? photo.thumbnailUrl : null); }
togglePhotoDisabled() { const photo = this.heroForm.get('photo'); if (photo.disabled) { photo.enable(); } else { photo.disable(); } }
private loadPhotos() { this.dataService.getPhotos().subscribe((photos) => { this.photos = photos; this.selectFirstPhoto(); }); }}<form [formGroup]="heroForm"> <div class="form-group"> <label for="photos">Custom templates</label> <ng-select [items]="photos" bindLabel="title" bindValue="thumbnailUrl" placeholder="Select photo" labelForId="photos" [virtualScroll]="true" formControlName="photo"> <ng-template ng-label-tmp let-item="item"> <img height="15" width="15" [src]="item.thumbnailUrl" /> <span>{{ item.title }}</span> </ng-template> <ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm"> <img height="15" width="15" [src]="item.thumbnailUrl" /> <span [ngOptionHighlight]="search">{{ item.title }}</span> </ng-template> </ng-select> <small class="form-text text-muted">5000 items with virtual scroll</small> <br /> <button class="btn btn-secondary btn-sm" (click)="selectFirstPhoto()">Select first photo</button> <button class="btn btn-secondary btn-sm" (click)="openModal(modalContent)">Open in modal</button> <button class="btn btn-secondary btn-sm" (click)="togglePhotoDisabled()">Toggle disabled</button> </div>
<ng-template #modalContent let-c="close" let-d="dismiss"> <div class="modal-header"> <h4 class="modal-title">Select in modal</h4> <button type="button" class="close" aria-label="Close" (click)="d('Cross click')"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <div class="form-group"> <label for="album">Favorite photo</label> <ng-select [items]="photos" (change)="changePhoto($event)" #select bindLabel="title" bindValue="thumbnailUrl" placeholder="Select photo" (keyup.esc)="$event.preventDefault()" [virtualScroll]="true" formControlName="photo"> <ng-template ng-label-tmp let-item="item"> <img height="15" width="15" [src]="item.thumbnailUrl" /> <span>{{ item.title }}</span> </ng-template> <ng-template ng-option-tmp let-item="item" let-index="index"> <img height="15" width="15" [src]="item.thumbnailUrl" /> <span>{{ item.title }}</span> </ng-template> </ng-select> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-outline-dark" (click)="c('Close click')">Close</button> </div> </ng-template></form>Template-driven Forms
Section titled “Template-driven Forms”Import FormsModule, bind with [(ngModel)], and provide name when the control is inside a form.
import { JsonPipe } from '@angular/common';import { ChangeDetectionStrategy, Component } from '@angular/core';import { FormsModule } from '@angular/forms';import { NgSelectComponent } from '@ng-select/ng-select';
@Component({ selector: 'ng-forms-template-driven-example', templateUrl: './forms-template-driven-example.component.html', styleUrls: ['./forms-template-driven-example.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, imports: [FormsModule, NgSelectComponent, JsonPipe],})export class FormsTemplateDrivenExampleComponent { readonly cities = [ { id: 1, name: 'New York' }, { id: 2, name: 'London' }, { id: 3, name: 'Beijing' }, { id: 4, name: 'New Delhi' }, { id: 5, name: 'Paris' }, ]; readonly model = { cityId: 2 as number | null, };}<form #cityForm="ngForm" novalidate> <div class="form-group"> <label for="template-city">Template-driven Forms select</label> <ng-select [items]="cities" bindLabel="name" bindValue="id" labelForId="template-city" placeholder="Select a city" [(ngModel)]="model.cityId" #cityId="ngModel" name="cityId" required /> @if (cityId.touched && cityId.invalid) { <small class="text-danger">Choose a city</small> } <br /> <button type="button" class="btn btn-sm btn-secondary" (click)="cityForm.resetForm({ cityId: null })">Reset</button> </div></form>
<pre>{{ model | json }}</pre>Inputs used by the examples on this page:
| Input | Type | Default | Description |
|---|---|---|---|
| [items] | Array<any> |
[] |
Items array |
| bindLabel | string |
label |
Object property to use for label. Default label |
| bindValue | string |
- |
Object property to use for selected model. By default binds to whole object. |
| [multiple] | boolean |
false |
Allows to select multiple items. |
| [clearable] | boolean |
true |
Allow to clear selected value. Default true |
| [readonly] | boolean |
false |
Set ng-select as readonly. |
| [searchable] | boolean |
true |
Allow to search for value. Default true |
| placeholder | string |
- |
Placeholder text. |
| labelForId | string |
- |
Id to associate control with label. |
| appearance | string |
underline |
Allows to select dropdown appearance. Set to outline or fill for Material form-field styles (applies only to Material theme) |
| dropdownPosition | bottom | top | left | right | auto |
auto |
Set the dropdown position on open |
| clearAllText | string |
Clear all |
Set custom text for clear all icon title |
| [selectOnTab] | boolean |
false |
Select marked dropdown item using tab. Default false |
| [virtualScroll] | boolean |
false | Enable virtual scroll for better performance when rendering a lot of data |
Outputs used by the examples on this page:
| Output | Description |
|---|---|
| (change) | Fired on selection change. Outputs the selected item (an array when [multiple]="true") as the whole item object, not the bindValue-projected model value |
See the NgSelectComponent API reference for the complete list of inputs and outputs.