Created
December 9, 2016 10:07
-
-
Save klinki/36a761f48e40968a1da3312c3419144f to your computer and use it in GitHub Desktop.
Angular 2 select with custom comparator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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