All Components

Custom Values

To configure the MultiSelect to accept custom values, set the allowCustom property to true.

Primitive Values

If the component is bound to primitive values, set the allowCustom property to true.

This approach is valid when the component is populated with either a dataset of objects or primitive values, and the valuePrimitive property is set to true.

The following example demonstrates how to allow custom primitive values.

  • If the typed custom value matches an item from the list, the component will select it. If the typed value matches an already selected item, the component will deselect that value and remove the corresponding tag. This behavior can be customized by creating a custom valueNormalizer(/kendo-angular-ui/components/dropdowns/api/MultiSelectComponent/#toc-valuenormalizer).
  • The built-in matching logic uses case-insensitive comparison.
@Component({
  selector: 'my-app',
  template: `
    <p>Custom values are <strong>enabled</strong>. Type a custom value.</p>

    <p>primitive data</p>
    <div class="example-wrapper">
        <kendo-multiselect
            [data]="sizes"
            [value]="selectedSizes"
            [allowCustom]="true"
            (valueChange)="onSizeChange($event)"
        >
        </kendo-multiselect>
    </div>
  `
})
class AppComponent {
    public sizes: Array<string> = [ "Small", "Medium", "Large" ];
    public selectedSizes: Array<string> = [];

    public onSizeChange(value) {
        this.selectedSizes = value;
    }
}

Object Values

If the component is bound to objects, set the allowCustom property to true and provide a valueNormalizer function. The purpose of the valueNormalizer function is to convert the user text input into an object that can be recognized as a valid MultiSelect value.

The valueNormalizer function receives Observable<string> as an argument and is expected to return a single normalized value that is wrapped as Observable, which will be further processed by the component.

The following example demonstrates how to allow custom object values.

import { map } from 'rxjs/operators/map';

@Component({
  selector: 'my-app',
  template: `
    <p>Custom values are <strong>enabled</strong>. Type a custom value.</p>
    <p>MultiSelect value: {{ sizes|json }}</p>

    <kendo-multiselect
        [allowCustom]="true"
        [data]="listItems"
        [textField]="'text'"
        [valueField]="'value'"
        [valueNormalizer]="valueNormalizer"
        [(ngModel)]="sizes"
    >
    </kendo-multiselect>
  `
})

class AppComponent {
    public listItems: Array<{ text: string, value: number }> = [
        { text: "Small", value: 1 },
        { text: "Medium", value: 2 },
        { text: "Large", value: 3 }
    ];

    public sizes: Array<{ text: string, value: number }> = [{ text: "Medium", value: 2 }];

    /*
        The component will emit custom text, which is typed by the user, and pass it to the `valueNormalizer` method.
        You can process the custom text, create a single custom object that represents the normalized value,
        and pass it back wrapped as an Observable.

        This example uses the `map` operator to transform text into a normalized value object.

        For more information on the `map` operator, refer to
        http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-map
    */
    public valueNormalizer = (text$: Observable<string>) => text$.pipe(map((text: string) => {
        //search for matching item to avoid duplicates

        //search in values
        const matchingValue: any = this.sizes.find((item: any) => {
            return item["text"].toLowerCase() === text.toLowerCase();
        });

        if (matchingValue) {
            return matchingValue; //return the already selected matching value and the component will remove it
        }

        //search in data
        const matchingItem: any = this.listItems.find((item: any) => {
            return item["text"].toLowerCase() === text.toLowerCase();
        });

        if (matchingItem) {
            return matchingItem;
        } else {
            return {
                value: Math.random(), //generate unique value for the custom item
                text: text
            };
        }
    }));
}

Remote Service

The following example demonstrates how to handle custom object values through a remote service.

import { Component, Inject, Injectable } from '@angular/core';
import {
    HttpClient,
    HttpEvent,
    HttpEventType,
    HttpHandler,
    HttpInterceptor,
    HttpProgressEvent,
    HttpRequest,
    HttpResponse
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { concat } from 'rxjs/observable/concat';
import { of } from 'rxjs/observable/of';
import { delay } from 'rxjs/operators/delay';
import { switchMap } from 'rxjs/operators/switchMap';

import { DataService } from './data.service';
import { Product } from '../common/product.model';

@Component({
  selector: 'my-app',
  template: `
            <p>Type a custom value and press <strong>Enter</strong>.</p>
            <p>Value: {{items | json }}</p>
            <kendo-multiselect
                [data]="listItems"
                [textField]="'ProductName'"
                [valueField]="'ProductID'"
                [(ngModel)]="items"
                [valueNormalizer]="valueNormalizer"
                [allowCustom]="true"
            >
            </kendo-multiselect>`
})
export class AppComponent implements OnInit {
    public listItems: Product[] = [];
    public items: Product[] = [{ ProductID: 1, ProductName: 'Chai' }];

    constructor(
        public http: HttpClient,
        @Inject(DataService) private dataService: DataService
    ) {}

    ngOnInit() {
        // Fetch MultiSelect data from remote service.
        this.dataService.fetchData().subscribe(data => this.listItems = data);
    }

    /*
        Maps each custom text that the user types to the result of a function that performs http requests to the remove service.
        For further details about the `switchMap` operator, check
        http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-switchMap.
    */
    public valueNormalizer =  (text$: Observable<string>): any => text$.pipe(
        switchMap((text: string) => {
            // Search in values
            const matchingValue: any = this.items.find((item: any) => {
                // Search for matching item to avoid duplicates
                return item['ProductName'].toLowerCase() === text.toLowerCase();
            });

            if (matchingValue) {
                // Return the already selected matching value and the component will remove it
                return of(matchingValue);
            }

            // Search in data
            const matchingItem: any = this.listItems.find((item: any) => {
                return item['ProductName'].toLowerCase() === text.toLowerCase();
            });

            if (matchingItem) {
                return of(matchingItem);
            } else {
                return of(text).pipe(switchMap(this.service$));
            }
        })
    )

    /*
        Sends the custom text to the remote service that will process it and sends back a generated data item with
        `ProductID` and `ProductName` fields.

        Note that the response should contain a *single item* that represents the normalized value.
        The response value will be returned to the MultiSelect by the `valueNormalizer` function.
        If you want to modify the server response before it is returned to the MultiSelect by the `valueNormalizer`,
        use the `map` operator:
            http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-map.

        For further details on how to perform an http request, check
        https://angular.io/docs/ts/latest/guide/server-communication.html.

        *IMPORTANT* If the request fails due to some reason, the component will clear the custom text and will reset its value.
        If you want to notify the user for the error, use a `catch` operator.

        For further details on how to handle http errors, check
        https://angular.io/docs/ts/latest/guide/server-communication.html#!#always-handle-errors.
    */
    public service$ = (text: string): any => this.http.post('normalize/url', { text });
}

@Injectable()
export class MultiSelectInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

        /*
          Mocked backend service.

          ** Do NOT use in production environment **
        */
        if (req.url === 'normalize/url') {
            const download = of(<HttpProgressEvent>{
                type: HttpEventType.DownloadProgress,
                loaded: 50,
                total: 100
            }).pipe(delay(1000));

            const success = of(new HttpResponse({
                body: {
                    ProductID: Math.floor(Math.random() * (1000 - 100) + 1000),
                    ProductName: req.body.text
                },
                status: 200
            }));

            return concat(download, success);
        }

        return next.handle(req);
    }
}

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule, HTTP_INTERCEPTORS, HttpClientJsonpModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { AppComponent, MultiSelectInterceptor } from './app.component';
import { DataService } from './data.service';

@NgModule({
  imports:      [ BrowserModule, HttpClientModule, HttpClientJsonpModule, DropDownsModule, FormsModule, BrowserAnimationsModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ],
  providers: [
    DataService,
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MultiSelectInterceptor,
      multi: true
    }
  ]
})

export class AppModule { }
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Product } from './product.model';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class DataService {
  constructor(private http: HttpClient) { }

  fetchData(action: string = '', data?: Product): Observable<Product[]> {
    return this.http.jsonp<Product[]>(
      `https://demos.telerik.com/kendo-ui/service/Products/${action}?${this.serializeModels(data)}`,
      'callback'
    );
  }

  private serializeModels(data?: Product): string {
    return data ? `&models=${JSON.stringify([data])}` : '';
  }
}
export class Product {
    constructor(
        public ProductID?: number,
        public ProductName?: string,
        public UnitPrice?: number,
        public UnitsInStock?: number,
        public Discontinued?: boolean
    ) { }
}

Handling Errors

The following example demonstrates how to handle errors that occur while normalizing custom object values through a remote service.

import { Component, Inject, Injectable, OnInit } from '@angular/core';
import {
    HttpClient,
    HttpErrorResponse,
    HttpEvent,
    HttpEventType,
    HttpHandler,
    HttpInterceptor,
    HttpProgressEvent,
    HttpRequest,
    HttpResponse
} from '@angular/common/http';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { Observer } from 'rxjs/Observer';
import { Observable } from 'rxjs/Observable';
import { concat } from 'rxjs/observable/concat';
import { of } from 'rxjs/observable/of';
import { catchError } from 'rxjs/operators/catchError';
import { delay } from 'rxjs/operators/delay';
import { switchMap } from 'rxjs/operators/switchMap';

import { DataService } from './data.service';
import { Product } from '../common/product.model';

@Component({
  selector: 'my-app',
  template: `
            <p>This example demonstrates how to handle server errors that occur in the normalization service.</p>
            <p>Type a custom value and press <strong>Enter</strong>.</p>
            <p>Value: {{items | json }}</p>
            <kendo-multiselect
                [data]="listItems"
                [textField]="'ProductName'"
                [valueField]="'ProductID'"
                [(ngModel)]="items"
                [valueNormalizer]="valueNormalizer"
                [allowCustom]="true"
            >
            </kendo-multiselect>
            <span *ngIf="hasError" style="color: red;">{{ errorMessage }}</span>
  `
})
export class AppComponent implements OnInit {
    public listItems: Product[] = [];
    public items: Product[] = [{ ProductID: 1, ProductName: 'Chai' }];
    public hasError = false;
    public errorMessage = '';

    constructor(
        public http: HttpClient,
        @Inject(DataService) private dataService: DataService
    ) {
    }

    ngOnInit() {
        // Fetches the MultiSelect data from the remote service.
        this.dataService.fetchData().subscribe(data => this.listItems = data);
    }

    /*
        Maps each custom text that the user types to the result of a function that performs an http request to remove a service.
        For further details on the `switchMap` operator, refer to
        http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-switchMap.
    */
    public valueNormalizer =  (text$: Observable<string>): any => text$.pipe(
        switchMap((text: string) => {
            // Search for matching item to avoid duplicates
            // Search in values
            const matchingValue: any = this.items.find((item: any) => {
                return item['ProductName'].toLowerCase() === text.toLowerCase();
            });

            if (matchingValue) {
                // Return the already selected matching value and the component will remove it
                return of(matchingValue);
            }

            // Search in data
            const matchingItem: any = this.listItems.find((item: any) => {
                return item['ProductName'].toLowerCase() === text.toLowerCase();
            });

            if (matchingItem) {
                return of(matchingItem);
            } else {
                return of(text).pipe(switchMap(this.service$));
            }
        })
    )

    /*
        Sends the custom text to the remote service that will process it and sends back a generated data item with
        the `ProductID` and `ProductName` fields.

        Note that the response should contain a *single item* that represents the normalized value.
        The response value will be returned by the `valueNormalizer` function to the MultiSelect.
        If you want to modify the server response before it is returned by the `valueNormalizer` to the MultiSelect component,
        use the `map` operator:
            http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-map.

        For further details on how to perform an http request, refer to
        https://angular.io/docs/ts/latest/guide/server-communication.html.

        **IMPORTANT** If the request fails due to some reason, the component will clear the custom text and reset its value.
        If you want to notify the user about the error, use the `catchError` operator.

        For further details on how to handle http errors, refer to
        https://angular.io/docs/ts/latest/guide/server-communication.html#!#always-handle-errors.
    */
    public service$ = (text: string): any => this.http
        .post('normalize/url', { text })
        .pipe(
            catchError((response: any, caught: Observable<object>) => {
                this.hasError = true;
                this.errorMessage = response.error;

                return new ErrorObservable(response.error);
            })
        )
}

@Injectable()
export class MultiSelectInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        /*
          Mocked backend service.
          **IMPORTANT** Do NOT use in production environment.
        */
        if (req.url === 'normalize/url') {
            const download = of(<HttpProgressEvent>{
                type: HttpEventType.DownloadProgress,
                loaded: 50,
                total: 100
            }).pipe(delay(1000));

            const error = new Observable<HttpEvent<any>>((observer: Observer<HttpEvent<any>>) => {
                observer.error(new HttpErrorResponse({ error: 'Error occured', status: 500 }));
            });

            return concat(download, error);
        }

        return next.handle(req);
    }
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule, HTTP_INTERCEPTORS, HttpClientJsonpModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { AppComponent, MultiSelectInterceptor } from './app.component';
import { DataService } from './data.service';

@NgModule({
  imports:      [ BrowserModule, HttpClientModule, HttpClientJsonpModule, DropDownsModule, FormsModule, BrowserAnimationsModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ],
  providers: [
    DataService,
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MultiSelectInterceptor,
      multi: true
    }
  ]
})

export class AppModule { }
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Product } from './product.model';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class DataService {
  constructor(private http: HttpClient) { }

  fetchData(action: string = '', data?: Product): Observable<Product[]> {
    return this.http.jsonp<Product[]>(
      `https://demos.telerik.com/kendo-ui/service/Products/${action}?${this.serializeModels(data)}`,
      'callback'
    );
  }

  private serializeModels(data?: Product): string {
    return data ? `&models=${JSON.stringify([data])}` : '';
  }
}
export class Product {
    constructor(
        public ProductID?: number,
        public ProductName?: string,
        public UnitPrice?: number,
        public UnitsInStock?: number,
        public Discontinued?: boolean
    ) { }
}
In this article