Implement a custom comparison function for select in Angular 4 using [compareWith]

The Problem

There are times when you want to bind a select to a custom object. You use some property of the object for display and you store the entire thing in a field. But, when you receive data from the server, you stumble across a problem. The select does not seem to display correctly.

Select with standard comparison function does not work.

Here is a little snippet of code that demonstrates this issue.

<div>
  <p>Compare by reff (object not bound) using standard select</p>
  <select name="options" 
          [(ngModel)]="selectedOptionStandardComparisson">
    <option *ngFor="let opt of options" [ngValue]="opt">
      {{opt.name}}
    </option>
  </select>
  <p *ngIf="selectedOptionStandardComparisson">
    {{selectedOptionStandardComparisson | json}}
  </p>
</div>
//our root app component
import {Component, NgModule, VERSION} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.html'
})
export class App {
  angularVersion:string;
  
  // list of options to display ion the select
  options: Person[] = [
    {id: "1", name: "Anna", age: "23"}, 
    {id: "2", name: "Dan", age: "16"}, 
    {id: "3", name: "John", age: "45"}, 
    {id: "4", name: "Jamie", age: "19"}, 
    {id: "5", name: "Samantha", age: "32"}, 
  ];

  selectedOptionStandardComparisson: Person;
  
  constructor() {
    // will not work, comparing by object
    this.selectedOptionStandardComparisson = {id: "3", name: "John", age: "45"};
  }
  
}

export interface Person{
  id: string;
  name: string;
  age:number;
}

@NgModule({
  imports: [ BrowserModule, FormsModule ],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule {}

This is happening because the select statement compares the object that you instantiate in the constructor with one of the objects in the list of options. The comparison is done by reference, so it will never be true even though the data is the same.

The Solution

To fix this, we need to implement a custom comparison function for select in Angular 4 and use the data itself to determine how the binding should happen. This can be achieved using the [compareWith] directive. The code below illustrates how we can compare the items by the id field.

<div>
  <p>Compare by reff (object not bound) using standard select</p>
  <select name="options" [(ngModel)]="selectedOptionCompareWith" 
                         [compareWith]="compareById">
    <option *ngFor="let opt of options" [ngValue]="opt">
      {{opt.name}}
    </option>
  </select>
  <p *ngIf="selectedOptionCompareWith">
    {{selectedOptionCompareWith | json}}
  </p>
</div>
//our root app component
import {Component, NgModule, VERSION} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.html'
})
export class App {
  angularVersion:string;
  
  // list of options to display ion the select
  options: Person[] = [
    {id: "1", name: "Anna", age: "23"}, 
    {id: "2", name: "Dan", age: "16"}, 
    {id: "3", name: "John", age: "45"}, 
    {id: "4", name: "Jamie", age: "19"}, 
    {id: "5", name: "Samantha", age: "32"}, 
  ];

  selectedOptionCompareWith: Person;
  
  constructor() {
    // the selected option as a new object (comparrison will be done by id)
    // [compareWith] used here. Will compare by id field and display
    this.selectedOptionCompareWith = {id:"1",name:"Anna", age:"23"};
  }
  
  // custom comparrison function that we use in select
  compareById(o1,o2){
    if(o1 == null || o2 == null){
      return false;
    }
    return o1.id === o2.id;
  }
}

export interface Person{
  id: string;
  name: string;
  age:number;
}

@NgModule({
  imports: [ BrowserModule, FormsModule ],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule {}

The output of the above code will work as expected 😀

With custom comparison function the select works even when the objects are different

There are 3 things to notice in this snippet:

  • The value of the select is bound with [ngValue]=”opt”. If you just use value=”opt”, then you will not save the object, but a String representation of it
  • The select contains the [compareWith]=”compareById directive. This tells the select to use the compareById function when comparing the value with the list of options
  • The compareById function is pretty easy. It receives 2 objects (o1,o2) and compares them with the id field.