Skip to content

Instantly share code, notes, and snippets.

@klinki
Created December 9, 2016 10:07
Show Gist options
  • Save klinki/36a761f48e40968a1da3312c3419144f to your computer and use it in GitHub Desktop.
Save klinki/36a761f48e40968a1da3312c3419144f to your computer and use it in GitHub Desktop.
Angular 2 select with custom comparator
import {Directive, Renderer, ElementRef, Input, OnDestroy, Optional, Host} from "@angular/core";
import {SelectMultipleControlValueAccessor} from "@angular/forms";
import {looseIdentical} from "@angular/core/src/facade/lang";
import {Comparator, ComparatorCallback, CUSTOM_SELECT_MULTIPLE_VALUE_ACCESSOR} from "./custom-select.directive";
// [formControlName],select[multiple][wu-select][formControl],select[multiple][wu-select][ngModel]
@Directive({
selector: 'select[multiple][wu-select]',
host: {'(change)': 'onChange($event.target)', '(blur)': 'onTouched()'},
providers: [CUSTOM_SELECT_MULTIPLE_VALUE_ACCESSOR]
})
export class ExtendedCustomSelectMultipleControlValueAccessor extends SelectMultipleControlValueAccessor {
@Input()
comparator: Comparator<any>;
@Input()
comparatorCallback: ComparatorCallback<any>;
_getOptionId(value: any): string {
for (const id of Array.from(this._optionMap.keys())) {
if (this._compare(this._optionMap.get(id)._value, value))
return id;
}
return null;
}
_compare(a: any, b: any): boolean {
if (this.comparator) {
return this.comparator.equals(a, b);
} else if (this.comparatorCallback) {
return this.comparatorCallback(a, b);
} else {
return looseIdentical(a, b);
}
}
}
/**
* Marks `<option>` as dynamic, so Angular can be notified when options change.
*
* ### Example
*
* ```
* <select multiple name="city" ngModel>
* <option *ngFor="let c of cities" [value]="c"></option>
* </select>
* ```
*/
@Directive({
selector: 'option'
})
export class CustomNgSelectMultipleOption implements OnDestroy {
id: string;
/** @internal */
_value: any;
constructor(private _element: ElementRef, private _renderer: Renderer,
@Optional() @Host() private _select: ExtendedCustomSelectMultipleControlValueAccessor) {
if (this._select) {
this.id = this._select._registerOption(this);
}
}
}
import {Directive, Renderer, ElementRef, Provider, forwardRef, Input, OnDestroy, Optional, Host} from "@angular/core";
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from "@angular/forms";
import {isPrimitive, looseIdentical} from "@angular/core/src/facade/lang";
export const CUSTOM_SELECT_MULTIPLE_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomSelectMultipleControlValueAccessor),
multi: true
};
function _buildValueString(id: string, value: any): string {
if (id == null) return `${value}`;
if (typeof value === 'string') value = `'${value}'`;
if (!isPrimitive(value)) value = 'Object';
return `${id}: ${value}`.slice(0, 50);
}
function _extractId(valueString: string): string {
return valueString.split(':')[0];
}
/** Mock interface for HTML Options */
interface HTMLOption {
value: string;
selected: boolean;
}
/** Mock interface for HTMLCollection */
abstract class HTMLCollection {
length: number;
abstract item(_: number): HTMLOption;
}
export interface ComparatorCallback<T> {
(a: T, b: T): boolean;
}
export interface Comparator<T> {
equals(a: T, b: T): boolean;
}
export class LooseIdenticalComparator implements Comparator<any> {
equals(a: any, b: any): boolean {
return a === b || typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b);
}
}
// [formControlName],select[multiple][wu-select][formControl],select[multiple][wu-select][ngModel]
@Directive({
selector: 'select[multiple][wu-select]',
host: {'(change)': 'onChange($event.target)', '(blur)': 'onTouched()'},
providers: [CUSTOM_SELECT_MULTIPLE_VALUE_ACCESSOR]
})
export class CustomSelectMultipleControlValueAccessor implements ControlValueAccessor {
@Input()
comparator: Comparator<any>;
@Input()
comparatorCallback: ComparatorCallback<any>;
value: any;
/** @internal */
_optionMap: Map<string, CustomNgSelectMultipleOption> = new Map<string, CustomNgSelectMultipleOption>();
/** @internal */
_idCounter: number = 0;
onChange = (_: any) => {
};
onTouched = () => {
};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {
console.log('Inside custom select multiprovider');
}
writeValue(value: any): void {
this.value = value;
if (value == null) return;
const values: Array<any> = <Array<any>>value;
// convert values to ids
const ids = values.map((v) => this._getOptionId(v));
this._optionMap.forEach((opt, o) => {
opt._setSelected(ids.indexOf(o.toString()) > -1);
});
}
registerOnChange(fn: (value: any) => any): void {
this.onChange = (_: any) => {
const selected: Array<any> = [];
if (_.hasOwnProperty('selectedOptions')) {
const options: HTMLCollection = _.selectedOptions;
for (let i = 0; i < options.length; i++) {
const opt: any = options.item(i);
const val: any = this._getOptionValue(opt.value);
selected.push(val);
}
}
// Degrade on IE
else {
const options: HTMLCollection = <HTMLCollection>_.options;
for (let i = 0; i < options.length; i++) {
const opt: HTMLOption = options.item(i);
if (opt.selected) {
const val: any = this._getOptionValue(opt.value);
selected.push(val);
}
}
}
fn(selected);
};
}
registerOnTouched(fn: () => any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
/** @internal */
_registerOption(value: CustomNgSelectMultipleOption): string {
const id: string = (this._idCounter++).toString();
this._optionMap.set(id, value);
return id;
}
/** @internal */
_getOptionId(value: any): string {
for (const id of Array.from(this._optionMap.keys())) {
if (this._compare(this._optionMap.get(id)._value, value))
return id;
}
return null;
}
_compare(a: any, b: any): boolean {
if (this.comparator) {
return this.comparator.equals(a, b);
} else if (this.comparatorCallback) {
return this.comparatorCallback(a, b);
} else {
return looseIdentical(a, b);
}
}
/** @internal */
_getOptionValue(valueString: string): any {
const id: string = _extractId(valueString);
return this._optionMap.has(id) ? this._optionMap.get(id)._value : valueString;
}
}
/**
* Marks `<option>` as dynamic, so Angular can be notified when options change.
*
* ### Example
*
* ```
* <select multiple name="city" ngModel>
* <option *ngFor="let c of cities" [value]="c"></option>
* </select>
* ```
*/
@Directive({
selector: 'option'
})
export class CustomNgSelectMultipleOption implements OnDestroy {
id: string;
/** @internal */
_value: any;
constructor(
private _element: ElementRef, private _renderer: Renderer,
@Optional() @Host() private _select: CustomSelectMultipleControlValueAccessor) {
if (this._select) {
this.id = this._select._registerOption(this);
}
}
@Input('ngValue')
set ngValue(value: any) {
if (this._select == null) return;
this._value = value;
this._setElementValue(_buildValueString(this.id, value));
this._select.writeValue(this._select.value);
}
@Input('value')
set value(value: any) {
if (this._select) {
this._value = value;
this._setElementValue(_buildValueString(this.id, value));
this._select.writeValue(this._select.value);
} else {
this._setElementValue(value);
}
}
/** @internal */
_setElementValue(value: string): void {
this._renderer.setElementProperty(this._element.nativeElement, 'value', value);
}
/** @internal */
_setSelected(selected: boolean) {
this._renderer.setElementProperty(this._element.nativeElement, 'selected', selected);
}
ngOnDestroy(): void {
if (this._select) {
this._select._optionMap.delete(this.id);
this._select.writeValue(this._select.value);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment