Angular2 Select2 Wrapper

Angular2 is an exciting technology that gives us a “WOW” effect on developing a web application. We used PHP, JSP and GWT before. They are good, but at some point, developers blend dynamic code, html and javascript into one html file. Yes, we can separate the javascript and html file, but that separation feels so disconnected. Beside that, Single Page Application (SPA) is far more efficient since there are information that don’t need to be reloaded for every page like header, footer and the menu.

Angular2 promotes better reusability of component. Here we are going to demonstrate how to transform html select component into Select2.

You can see the complete code in that plunker. Focus on CmpTypeahead.ts, this is the angular component that wrap Select2. It supports ngModel syntax. In this example, it only supports placeholder and allowClear options of Select2. But it is very easy to add more options. Here is CmpTypeahead.ts


import {Component, Input, AfterViewInit, OnChanges, SimpleChange, forwardRef, ViewChild, ElementRef} from '@angular/core';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';

export const TYPEAHEAD_VALUE_ACCESSOR: any = {
	provide: NG_VALUE_ACCESSOR,
	useExisting: forwardRef(() => CmpTypeahead),
	multi: true
};

declare var jQuery: any;

@Component({
	moduleId: module.id,
	selector: 'p-typeahead',
	template: ``,
	providers: [TYPEAHEAD_VALUE_ACCESSOR],
	styleUrls: ['typeahead.css']
})

export class CmpTypeahead implements AfterViewInit, ControlValueAccessor {
	private objRef: any;
	@Input() items: any;
	value: string;
	onModelChange: Function = () => {};
	onModelTouched: Function = () => {};
	@Input() disabled: any;
	@Input() placeholder = '';
	@Input() allowClear = true;
	selectElm: any;
	initItemsCount = 0;
	
	constructor(private elRef: ElementRef) {
	}

	ngAfterViewInit() {
		this.selectElm = this.elRef.nativeElement.getElementsByTagName('select')[0];
		this.initItemsCount = this.selectElm.options.length;
		this.objRef = jQuery(this.selectElm);
		this.objRef.select2({
			placeholder: this.placeholder,
			allowClear: this.allowClear
		});

		this.objRef.on("select2:select", (e) => {
			this.value = this.objRef.val();
			this.onModelChange(this.value);
			this.selectElm.focus();
		});
		
		this.objRef.on("select2:unselect", (e) => {
			this.value = null;
			this.onModelChange(this.value);
			this.selectElm.focus();
		});
		
	}

	writeValue(value: any): void {
		if (value === undefined) {
			value = null;
		}
		this.value = value;
		if (this.objRef) {
			this.objRef.val(value).trigger("change");
		}
	}
	
	ngAfterViewChecked() {
		if (this.selectElm && this.selectElm.options.length != this.initItemsCount) {
			/**
			 * trigger change in writeValue() doesn't work if the items (the option tags) in 
			 *<select> haven't been rendered. This trigger is to fix this problem
			 */
			this.initItemsCount = this.selectElm.options.length;
			this.objRef.val(this.value).trigger("change");
		}
	}

	registerOnChange(fn: Function): void {
		this.onModelChange = fn;
	}

	registerOnTouched(fn: Function): void {
		this.onModelTouched = fn;
	}

	setDisabledState(val: boolean): void {
		this.disabled = val;
		this.objRef.prop("disabled", val);
	}

}
</select>

That component depends on typeahead.css below

:host {
	display: block;
	border-radius: 4px;
	/*padding-left: -5px;*/
}

:host[required] /deep/ .select2-selection--single {
	border-radius: 0 4px 4px 0;
}

Of course the component should be declared in the one of the module. Once it is declared, we can use it to wrap existing select tags with p-typeahead to transform it into Select2 component.

<p-typeahead>
  <select style="width: 200px;">
    <option value=""></option>
    <option value="1">One</option>
    <option value="2">Two</option>
    <option value="3">Three</option>
    <option value="4">Four</option>
    <option value="5" selected>Five</option>
  </select>
</p-typeahead>