From bff5d3fd4f738c4c23e84b714f90240ec37663bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20B=C3=BCrk?= Date: Fri, 21 Dec 2018 02:06:33 +0100 Subject: [PATCH] feat(core): Implemented MultiSelectControlValueAccessorDirective relates #14 relates #5 --- .../ngqp/core/src/lib/accessors/accessors.ts | 3 +- ...select-control-value-accessor.directive.ts | 142 ++++++++++++++++++ ...select-control-value-accessor.directive.ts | 13 +- .../ngqp/core/src/lib/query-param.module.ts | 10 +- 4 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 projects/ngqp/core/src/lib/accessors/multi-select-control-value-accessor.directive.ts diff --git a/projects/ngqp/core/src/lib/accessors/accessors.ts b/projects/ngqp/core/src/lib/accessors/accessors.ts index 07d7774..57d8a3a 100644 --- a/projects/ngqp/core/src/lib/accessors/accessors.ts +++ b/projects/ngqp/core/src/lib/accessors/accessors.ts @@ -2,4 +2,5 @@ export { DefaultControlValueAccessorDirective } from './default-control-value-ac export { NumberControlValueAccessorDirective } from './number-control-value-accessor.directive'; export { RangeControlValueAccessorDirective } from './range-control-value-accessor.directive'; export { CheckboxControlValueAccessorDirective } from './checkbox-control-value-accessor.directive'; -export { SelectControlValueAccessorDirective, NgqpSelectOptionDirective } from './select-control-value-accessor.directive'; \ No newline at end of file +export { SelectControlValueAccessorDirective, SelectOptionDirective } from './select-control-value-accessor.directive'; +export { MultiSelectControlValueAccessorDirective, MultiSelectOptionDirective } from './multi-select-control-value-accessor.directive'; \ No newline at end of file diff --git a/projects/ngqp/core/src/lib/accessors/multi-select-control-value-accessor.directive.ts b/projects/ngqp/core/src/lib/accessors/multi-select-control-value-accessor.directive.ts new file mode 100644 index 0000000..90dfb41 --- /dev/null +++ b/projects/ngqp/core/src/lib/accessors/multi-select-control-value-accessor.directive.ts @@ -0,0 +1,142 @@ +import { + Directive, + ElementRef, + forwardRef, + Host, + HostListener, + Input, + OnDestroy, + OnInit, + Optional, + Renderer2, + StaticProvider +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +export const NGQP_MULTI_SELECT_VALUE_ACCESSOR: StaticProvider = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MultiSelectControlValueAccessorDirective), + multi: true +}; + +@Directive({ + selector: 'select[multiple][queryParamName]', + providers: [NGQP_MULTI_SELECT_VALUE_ACCESSOR], +}) +export class MultiSelectControlValueAccessorDirective implements ControlValueAccessor { + + private selectedIds: string[] = []; + private options = new Map>(); + private optionMap = new Map(); + + private idCounter = 0; + private fnChange = (_: T[]) => {}; + private fnTouched = () => {}; + + @HostListener('change') + public onChange() { + this.selectedIds = Array.from(this.options.entries()) + .filter(([id, option]) => option.selected) + .map(([id]) => id); + const values = this.selectedIds.map(id => this.optionMap.get(id)); + this.fnChange(values); + } + + @HostListener('blur') + public onTouched() { + this.fnTouched(); + } + + constructor(private renderer: Renderer2, private elementRef: ElementRef) { + } + + public writeValue(values: T[]) { + values = values === null ? [] : values; + if (!Array.isArray(values)) { + throw new Error(`Provided a non-array value to select[multiple]: ${values}`); + } + + this.selectedIds = values + .map(value => this.getOptionId(value)) + .filter(id => id !== null); + this.options.forEach((option, id) => option.selected = this.selectedIds.includes(id)); + } + + public registerOnChange(fn: any) { + this.fnChange = fn; + } + + public registerOnTouched(fn: any) { + this.fnTouched = fn; + } + + public setDisabledState(isDisabled: boolean) { + this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', isDisabled); + } + + public registerOption(option: MultiSelectOptionDirective): string { + const newId = (this.idCounter++).toString(); + this.options.set(newId, option); + return newId; + } + + public deregisterOption(id: string): void { + this.optionMap.delete(id); + } + + public updateOptionValue(id: string, value: T): void { + this.optionMap.set(id, value); + if (this.selectedIds.includes(id)) { + this.onChange(); + } + } + + private getOptionId(value: T): string | null { + for (const id of Array.from(this.optionMap.keys())) { + if (this.optionMap.get(id) === value) { + return id; + } + } + + return null; + } + +} + +@Directive({ + selector: 'option', +}) +export class MultiSelectOptionDirective implements OnInit, OnDestroy { + + private readonly id: string | null = null; + + constructor( + @Optional() @Host() private parent: MultiSelectControlValueAccessorDirective, + private renderer: Renderer2, + private elementRef: ElementRef, + ) { + this.id = this.parent.registerOption(this); + } + + public ngOnInit() { + this.renderer.setProperty(this.elementRef.nativeElement, 'value', this.id); + } + + public ngOnDestroy() { + this.parent.deregisterOption(this.id); + } + + @Input('value') + public set value(value: T) { + this.parent.updateOptionValue(this.id, value); + } + + public get selected(): boolean { + return (this.elementRef.nativeElement as HTMLOptionElement).selected; + } + + public set selected(selected: boolean) { + this.renderer.setProperty(this.elementRef.nativeElement, 'selected', this.selected); + } + +} \ No newline at end of file diff --git a/projects/ngqp/core/src/lib/accessors/select-control-value-accessor.directive.ts b/projects/ngqp/core/src/lib/accessors/select-control-value-accessor.directive.ts index dd7cac3..c04c403 100644 --- a/projects/ngqp/core/src/lib/accessors/select-control-value-accessor.directive.ts +++ b/projects/ngqp/core/src/lib/accessors/select-control-value-accessor.directive.ts @@ -5,7 +5,7 @@ import { Host, HostListener, Input, - OnDestroy, + OnDestroy, OnInit, Optional, Renderer2, StaticProvider @@ -103,7 +103,7 @@ export class SelectControlValueAccessorDirective implements ControlValueAcces @Directive({ selector: 'option', }) -export class NgqpSelectOptionDirective implements OnDestroy { +export class SelectOptionDirective implements OnInit, OnDestroy { private readonly id: string | null = null; @@ -112,9 +112,11 @@ export class NgqpSelectOptionDirective implements OnDestroy { private renderer: Renderer2, private elementRef: ElementRef, ) { - if (this.parent) { - this.id = this.parent.registerOption(); - } + this.id = this.parent.registerOption(); + } + + public ngOnInit() { + this.renderer.setProperty(this.elementRef.nativeElement, 'value', this.id); } public ngOnDestroy() { @@ -124,7 +126,6 @@ export class NgqpSelectOptionDirective implements OnDestroy { @Input('value') public set value(value: T) { - this.renderer.setProperty(this.elementRef.nativeElement, 'value', this.id); this.parent.updateOptionValue(this.id, value); } diff --git a/projects/ngqp/core/src/lib/query-param.module.ts b/projects/ngqp/core/src/lib/query-param.module.ts index 381cedc..e7290cc 100644 --- a/projects/ngqp/core/src/lib/query-param.module.ts +++ b/projects/ngqp/core/src/lib/query-param.module.ts @@ -4,10 +4,12 @@ import { QueryParamGroupDirective } from './query-param-group.directive'; import { CheckboxControlValueAccessorDirective, DefaultControlValueAccessorDirective, - NgqpSelectOptionDirective, + MultiSelectControlValueAccessorDirective, + MultiSelectOptionDirective, NumberControlValueAccessorDirective, RangeControlValueAccessorDirective, - SelectControlValueAccessorDirective + SelectControlValueAccessorDirective, + SelectOptionDirective } from './accessors/accessors'; import { DefaultRouterAdapter, NGQP_ROUTER_ADAPTER } from './router-adapter/router-adapter'; @@ -21,7 +23,9 @@ const DIRECTIVES: Type[] = [ RangeControlValueAccessorDirective, CheckboxControlValueAccessorDirective, SelectControlValueAccessorDirective, - NgqpSelectOptionDirective, + SelectOptionDirective, + MultiSelectControlValueAccessorDirective, + MultiSelectOptionDirective, ]; @NgModule({