From 7a30cb5593a6afdcdc2327c557553e373288bbae Mon Sep 17 00:00:00 2001 From: Ed Morales Date: Wed, 13 Dec 2017 14:52:45 -0800 Subject: [PATCH] feat(mixin): add ngModel mixin for reuse (control value accessor) (#1024) * feat(mixin): add ngModel mixin for reuse (control value accessor) * feat(): use ngModel mixin where possible --- src/platform/core/chips/chips.component.ts | 73 ++++---------- .../control-value-accesor.mixin.spec.ts | 44 +++++++++ .../behaviors/control-value-accesor.mixin.ts | 66 +++++++++++++ src/platform/core/common/common.module.ts | 1 + .../core/data-table/data-table.component.ts | 97 +++++++------------ .../file/file-input/file-input.component.ts | 65 +++---------- .../file/file-upload/file-upload.component.ts | 66 +++---------- 7 files changed, 194 insertions(+), 218 deletions(-) create mode 100644 src/platform/core/common/behaviors/control-value-accesor.mixin.spec.ts create mode 100644 src/platform/core/common/behaviors/control-value-accesor.mixin.ts diff --git a/src/platform/core/chips/chips.component.ts b/src/platform/core/chips/chips.component.ts index 790556be07..57256ffa6f 100644 --- a/src/platform/core/chips/chips.component.ts +++ b/src/platform/core/chips/chips.component.ts @@ -22,11 +22,7 @@ import { fromEvent } from 'rxjs/observable/fromEvent'; import { filter } from 'rxjs/operators/filter'; import { debounceTime } from 'rxjs/operators/debounceTime'; -import { ICanDisable, mixinDisabled } from '../common/common.module'; - -const noop: any = () => { - // empty method -}; +import { ICanDisable, mixinDisabled, IControlValueAccessor, mixinControlValueAccessor } from '../common/common.module'; @Directive({ selector: '[td-chip]ng-template', @@ -46,10 +42,12 @@ export class TdAutocompleteOptionDirective extends TemplatePortalDirective { } } -export class TdChipsBase {} +export class TdChipsBase { + constructor(public _changeDetectorRef: ChangeDetectorRef) {} +} /* tslint:disable-next-line */ -export const _TdChipsMixinBase = mixinDisabled(TdChipsBase); +export const _TdChipsMixinBase = mixinControlValueAccessor(mixinDisabled(TdChipsBase), []); @Component({ providers: [{ @@ -58,22 +56,17 @@ export const _TdChipsMixinBase = mixinDisabled(TdChipsBase); multi: true, }], selector: 'td-chips', - inputs: ['disabled'], + inputs: ['disabled', 'value'], styleUrls: ['./chips.component.scss' ], templateUrl: './chips.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TdChipsComponent extends _TdChipsMixinBase implements ControlValueAccessor, DoCheck, OnInit, AfterViewInit, OnDestroy, ICanDisable { +export class TdChipsComponent extends _TdChipsMixinBase implements IControlValueAccessor, DoCheck, OnInit, AfterViewInit, OnDestroy, ICanDisable { private _outsideClickSubs: Subscription; private _isMousedown: boolean = false; - /** - * Implemented as part of ControlValueAccessor. - */ - private _value: any[] = []; - private _items: any[]; private _length: number = 0; private _stacked: boolean = false; @@ -254,18 +247,6 @@ export class TdChipsComponent extends _TdChipsMixinBase implements ControlValueA */ @Output('chipBlur') onChipBlur: EventEmitter = new EventEmitter(); - /** - * Implemented as part of ControlValueAccessor. - */ - @Input() set value(v: any) { - if (v !== this._value) { - this._value = v; - this._length = this._value ? this._value.length : 0; - this._changeDetectorRef.markForCheck(); - } - } - get value(): any { return this._value; } - /** * Hostbinding to set the a11y of the TdChipsComponent depending on its state */ @@ -276,9 +257,9 @@ export class TdChipsComponent extends _TdChipsMixinBase implements ControlValueA constructor(private _elementRef: ElementRef, private _renderer: Renderer2, - private _changeDetectorRef: ChangeDetectorRef, - @Optional() @Inject(DOCUMENT) private _document: any) { - super(); + @Optional() @Inject(DOCUMENT) private _document: any, + _changeDetectorRef: ChangeDetectorRef) { + super(_changeDetectorRef); this._renderer.addClass(this._elementRef.nativeElement, 'mat-' + this._color); } @@ -363,9 +344,9 @@ export class TdChipsComponent extends _TdChipsMixinBase implements ControlValueA ngDoCheck(): void { // Throw onChange event only if array changes size. - if (this._value && this._value.length !== this._length) { - this._length = this._value.length; - this.onChange(this._value); + if (this.value && this.value.length !== this._length) { + this._length = this.value.length; + this.onChange(this.value); } } @@ -438,13 +419,13 @@ export class TdChipsComponent extends _TdChipsMixinBase implements ControlValueA this.inputControl.setValue(''); // check if value is already part of the model - if (this._value.indexOf(value) > -1) { + if (this.value.indexOf(value) > -1) { return false; } - this._value.push(value); + this.value.push(value); this.onAdd.emit(value); - this.onChange(this._value); + this.onChange(this.value); this._changeDetectorRef.markForCheck(); return true; } @@ -454,7 +435,7 @@ export class TdChipsComponent extends _TdChipsMixinBase implements ControlValueA * returns 'true' if successful, 'false' if it fails. */ removeChip(index: number): boolean { - let removedValues: any[] = this._value.splice(index, 1); + let removedValues: any[] = this.value.splice(index, 1); if (removedValues.length === 0) { return false; } @@ -472,7 +453,7 @@ export class TdChipsComponent extends _TdChipsMixinBase implements ControlValueA } this.onRemove.emit(removedValues[0]); - this.onChange(this._value); + this.onChange(this.value); this.inputControl.setValue(''); this._changeDetectorRef.markForCheck(); return true; @@ -657,24 +638,6 @@ export class TdChipsComponent extends _TdChipsMixinBase implements ControlValueA } } - /** - * Implemented as part of ControlValueAccessor. - */ - writeValue(value: any): void { - this.value = value; - } - - registerOnChange(fn: any): void { - this.onChange = fn; - } - - registerOnTouched(fn: any): void { - this.onTouched = fn; - } - - onChange = (_: any) => noop; - onTouched = () => noop; - /** * Get total of chips */ diff --git a/src/platform/core/common/behaviors/control-value-accesor.mixin.spec.ts b/src/platform/core/common/behaviors/control-value-accesor.mixin.spec.ts new file mode 100644 index 0000000000..6d0ed19fa4 --- /dev/null +++ b/src/platform/core/common/behaviors/control-value-accesor.mixin.spec.ts @@ -0,0 +1,44 @@ +import { mixinControlValueAccessor } from './control-value-accesor.mixin'; +import { ChangeDetectorRef } from '@angular/core'; + +describe('ControlValueAccessorMixin', () => { + + it('should augment an existing class with a writeValue property', () => { + const classWithControlValueAccess: any = mixinControlValueAccessor(TestClass); + const instance: any = new classWithControlValueAccess(); + + expect(instance.value) + .toBeUndefined(); + expect(instance.writeValue) + .toBeTruthy(); + }); + + it('should agument and set an initial empty array', () => { + const classWithControlValueAccess: any = mixinControlValueAccessor(TestClass, []); + const instance: any = new classWithControlValueAccess(); + + expect(instance.value instanceof Array).toBeTruthy(); + expect(instance.value.length === 0).toBeTruthy(); + }); +}); + +class TestClass { + /** Fake instance of an ChangeDetectorRef. */ + _changeDetectorRef: ChangeDetectorRef = { + markForCheck: function(): void { + /* empty */ + }, + detach: function(): void { + /* empty */ + }, + detectChanges: function(): void { + /* empty */ + }, + checkNoChanges: function (): void { + /* empty */ + }, + reattach: function (): void { + /* empty */ + }, + }; +} diff --git a/src/platform/core/common/behaviors/control-value-accesor.mixin.ts b/src/platform/core/common/behaviors/control-value-accesor.mixin.ts new file mode 100644 index 0000000000..3f21bf9c30 --- /dev/null +++ b/src/platform/core/common/behaviors/control-value-accesor.mixin.ts @@ -0,0 +1,66 @@ +import { Constructor } from './constructor'; +import { ChangeDetectorRef } from '@angular/core'; +import { ControlValueAccessor } from '@angular/forms'; + +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + +const noop: any = () => { + // empty method +}; + +export interface IControlValueAccessor extends ControlValueAccessor { + value: any; + valueChanges: Observable; + onChange: (_: any) => any; + onTouched: () => any; +} + +export interface IHasChangeDetectorRef { + _changeDetectorRef: ChangeDetectorRef; +} + +/** Mixin to augment a component with ngModel support. */ +export function mixinControlValueAccessor> + (base: T, initialValue?: any): Constructor & T { + return class extends base { + private _value: any = initialValue; + private _subjectValueChanges: Subject; + valueChanges: Observable; + + constructor(...args: any[]) { + super(...args); + this._subjectValueChanges = new Subject(); + this.valueChanges = this._subjectValueChanges.asObservable(); + } + + set value(v: any) { + if (v !== this._value) { + this._value = v; + this.onChange(v); + this._changeDetectorRef.markForCheck(); + this._subjectValueChanges.next(v); + } + } + get value(): any { + return this._value; + } + + writeValue(value: any): void { + this.value = value; + this._changeDetectorRef.markForCheck(); + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + onChange = (_: any) => noop; + onTouched = () => noop; + + }; +} diff --git a/src/platform/core/common/common.module.ts b/src/platform/core/common/common.module.ts index 2dbe1fecab..e95965b8b8 100644 --- a/src/platform/core/common/common.module.ts +++ b/src/platform/core/common/common.module.ts @@ -32,6 +32,7 @@ export { TdPulseAnimation } from './animations/pulse/pulse.animation'; * BEHAVIORS */ +export { IControlValueAccessor, mixinControlValueAccessor } from './behaviors/control-value-accesor.mixin'; export { ICanDisable, mixinDisabled } from './behaviors/disabled.mixin'; export { ICanDisableRipple, mixinDisableRipple } from './behaviors/disable-ripple.mixin'; diff --git a/src/platform/core/data-table/data-table.component.ts b/src/platform/core/data-table/data-table.component.ts index 82141cbce2..819882357d 100644 --- a/src/platform/core/data-table/data-table.component.ts +++ b/src/platform/core/data-table/data-table.component.ts @@ -18,15 +18,7 @@ import { TdDataTableRowComponent } from './data-table-row/data-table-row.compone import { ITdDataTableSortChangeEvent, TdDataTableColumnComponent } from './data-table-column/data-table-column.component'; import { TdDataTableTemplateDirective } from './directives/data-table-template.directive'; -const noop: any = () => { - // empty method -}; - -export const TD_DATA_TABLE_CONTROL_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TdDataTableComponent), - multi: true, -}; +import { IControlValueAccessor, mixinControlValueAccessor } from '../common/common.module'; export enum TdDataTableSortingOrder { Ascending = 'ASC', @@ -85,14 +77,27 @@ const TD_VIRTUAL_OFFSET: number = 2; */ const TD_VIRTUAL_DEFAULT_ROW_HEIGHT: number = 48; +export class TdDataTableBase { + constructor(public _changeDetectorRef: ChangeDetectorRef) {} +} + +/* tslint:disable-next-line */ +export const _TdDataTableMixinBase = mixinControlValueAccessor(TdDataTableBase, []); + @Component({ - providers: [ TD_DATA_TABLE_CONTROL_VALUE_ACCESSOR ], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TdDataTableComponent), + multi: true, + }], selector: 'td-data-table', styleUrls: ['./data-table.component.scss' ], templateUrl: './data-table.component.html', + inputs: ['value'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TdDataTableComponent implements ControlValueAccessor, OnInit, AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy { +export class TdDataTableComponent extends _TdDataTableMixinBase implements IControlValueAccessor, OnInit, + AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy { /** responsive width calculations */ private _resizeSubs: Subscription; @@ -163,13 +168,7 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After return this._toRow; } - /** - * Implemented as part of ControlValueAccessor. - */ - private _value: any[] = []; - /** Callback registered via registerOnChange (ControlValueAccessor) */ - private _onChangeCallback: (_: any) => void = noop; - + private _valueChangesSubs: Subscription; /** internal attributes */ private _data: any[]; // data virtually iterated by component @@ -224,18 +223,6 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After return this._indeterminate; } - /** - * Implemented as part of ControlValueAccessor. - */ - @Input() set value(v: any) { - if (v !== this._value) { - this._value = v; - this._onChangeCallback(v); - this.refresh(); - } - } - get value(): any { return this._value; } - /** * data?: {[key: string]: any}[] * Sets the data to be rendered as rows. @@ -415,7 +402,9 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After constructor(@Optional() @Inject(DOCUMENT) private _document: any, private _elementRef: ElementRef, private _domSanitizer: DomSanitizer, - private _changeDetectorRef: ChangeDetectorRef) {} + _changeDetectorRef: ChangeDetectorRef) { + super(_changeDetectorRef); + } /** * compareWith?: function(row, model): boolean @@ -453,6 +442,9 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After this._calculateVirtualRows(); this._changeDetectorRef.markForCheck(); }); + this._valueChangesSubs = this.valueChanges.subscribe((value: any) => { + this.refresh(); + }); } /** @@ -519,6 +511,9 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After if (this._rowsChangedSubs) { this._rowsChangedSubs.unsubscribe(); } + if (this._valueChangesSubs) { + this._valueChangesSubs.unsubscribe(); + } } /** @@ -567,7 +562,7 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After * Clears model (ngModel) of component by removing all values in array. */ clearModel(): void { - this._value.splice(0, this._value.length); + this.value.splice(0, this.value.length); } /** @@ -589,7 +584,7 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After this._data.forEach((row: any) => { // skiping already selected rows if (!this.isRowSelected(row)) { - this._value.push(row); + this.value.push(row); // checking which ones are being toggled toggledRows.push(row); } @@ -601,12 +596,12 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After // checking which ones are being toggled if (this.isRowSelected(row)) { toggledRows.push(row); - let modelRow: any = this._value.filter((val: any) => { + let modelRow: any = this.value.filter((val: any) => { return this.compareWith(row, val); })[0]; - let index: number = this._value.indexOf(modelRow); + let index: number = this.value.indexOf(modelRow); if (index > -1) { - this._value.splice(index, 1); + this.value.splice(index, 1); } } }); @@ -621,7 +616,7 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After */ isRowSelected(row: any): boolean { // compare items by [compareWith] function - return this._value ? this._value.filter((val: any) => { + return this.value ? this.value.filter((val: any) => { return this.compareWith(row, val); }).length > 0 : false; } @@ -792,24 +787,6 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After event.preventDefault(); } - /** - * Implemented as part of ControlValueAccessor. - */ - writeValue(value: any): void { - this.value = value; - } - - registerOnChange(fn: any): void { - this.onChange = fn; - } - - registerOnTouched(fn: any): void { - this.onTouched = fn; - } - - onChange = (_: any) => noop; - onTouched = () => noop; - private _getNestedValue(name: string, value: any): string { if (!(value instanceof Object) || !name) { return value; @@ -831,20 +808,20 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After if (!this._multiple) { this.clearModel(); } - this._value.push(row); + this.value.push(row); } else { // compare items by [compareWith] function - row = this._value.filter((val: any) => { + row = this.value.filter((val: any) => { return this.compareWith(row, val); })[0]; - let index: number = this._value.indexOf(row); + let index: number = this.value.indexOf(row); if (index > -1) { - this._value.splice(index, 1); + this.value.splice(index, 1); } } this._calculateCheckboxState(); this.onRowSelect.emit({row: row, index: rowIndex, selected: !wasSelected}); - this.onChange(this._value); + this.onChange(this.value); return !wasSelected; } diff --git a/src/platform/core/file/file-input/file-input.component.ts b/src/platform/core/file/file-input/file-input.component.ts index 3309c5c64b..0a4e05251a 100644 --- a/src/platform/core/file/file-input/file-input.component.ts +++ b/src/platform/core/file/file-input/file-input.component.ts @@ -4,17 +4,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { TemplatePortalDirective } from '@angular/cdk/portal'; import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; -import { ICanDisable, mixinDisabled } from '../../common/common.module'; - -const noop: any = () => { - // empty method -}; - -export const FILE_INPUT_CONTROL_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TdFileInputComponent), - multi: true, -}; +import { ICanDisable, mixinDisabled, IControlValueAccessor, mixinControlValueAccessor } from '../../common/common.module'; @Directive({ selector: '[td-file-input-label]ng-template', @@ -25,34 +15,26 @@ export class TdFileInputLabelDirective extends TemplatePortalDirective { } } -export class TdFileInputBase {} +export class TdFileInputBase { + constructor(public _changeDetectorRef: ChangeDetectorRef) {} +} /* tslint:disable-next-line */ -export const _TdFileInputMixinBase = mixinDisabled(TdFileInputBase); +export const _TdFileInputMixinBase = mixinControlValueAccessor(mixinDisabled(TdFileInputBase)); @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - providers: [ FILE_INPUT_CONTROL_VALUE_ACCESSOR ], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TdFileInputComponent), + multi: true, + }], selector: 'td-file-input', - inputs: ['disabled'], + inputs: ['disabled', 'value'], styleUrls: ['./file-input.component.scss'], templateUrl: './file-input.component.html', }) -export class TdFileInputComponent extends _TdFileInputMixinBase implements ControlValueAccessor, ICanDisable { - - /** - * Implemented as part of ControlValueAccessor. - */ - private _value: FileList | File = undefined; - - // get/set accessor (needed for ControlValueAccessor) - get value(): FileList | File { return this._value; } - set value(v: FileList | File) { - if (v !== this._value) { - this._value = v; - this.onChange(v); - } - } +export class TdFileInputComponent extends _TdFileInputMixinBase implements IControlValueAccessor, ICanDisable { private _multiple: boolean = false; @@ -94,8 +76,8 @@ export class TdFileInputComponent extends _TdFileInputMixinBase implements Contr */ @Output('select') onSelect: EventEmitter = new EventEmitter(); - constructor(private _renderer: Renderer2, private _changeDetectorRef: ChangeDetectorRef) { - super(); + constructor(private _renderer: Renderer2, _changeDetectorRef: ChangeDetectorRef) { + super(_changeDetectorRef); } /** @@ -121,23 +103,4 @@ export class TdFileInputComponent extends _TdFileInputMixinBase implements Contr } } - /** - * Implemented as part of ControlValueAccessor. - */ - writeValue(value: any): void { - this.value = value; - this._changeDetectorRef.markForCheck(); - } - - registerOnChange(fn: any): void { - this.onChange = fn; - } - - registerOnTouched(fn: any): void { - this.onTouched = fn; - } - - onChange = (_: any) => noop; - onTouched = () => noop; - } diff --git a/src/platform/core/file/file-upload/file-upload.component.ts b/src/platform/core/file/file-upload/file-upload.component.ts index a26bd6e1d5..10abaada8a 100644 --- a/src/platform/core/file/file-upload/file-upload.component.ts +++ b/src/platform/core/file/file-upload/file-upload.component.ts @@ -1,49 +1,30 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewChild, ContentChild, ChangeDetectorRef, forwardRef } from '@angular/core'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { ICanDisable, mixinDisabled } from '../../common/common.module'; +import { ICanDisable, mixinDisabled, IControlValueAccessor, mixinControlValueAccessor } from '../../common/common.module'; import { TdFileInputComponent, TdFileInputLabelDirective } from '../file-input/file-input.component'; import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; -const noop: any = () => { - // empty method -}; - -export class TdFileUploadBase {} +export class TdFileUploadBase { + constructor(public _changeDetectorRef: ChangeDetectorRef) {} +} /* tslint:disable-next-line */ -export const _TdFileUploadMixinBase = mixinDisabled(TdFileUploadBase); - -export const FILE_UPLOAD_CONTROL_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TdFileUploadComponent), - multi: true, -}; +export const _TdFileUploadMixinBase = mixinControlValueAccessor(mixinDisabled(TdFileUploadBase)); @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - providers: [ FILE_UPLOAD_CONTROL_VALUE_ACCESSOR ], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TdFileUploadComponent), + multi: true, + }], selector: 'td-file-upload', - inputs: ['disabled'], + inputs: ['disabled', 'value'], styleUrls: ['./file-upload.component.scss'], templateUrl: './file-upload.component.html', }) -export class TdFileUploadComponent extends _TdFileUploadMixinBase implements ControlValueAccessor, ICanDisable { - - /** - * Implemented as part of ControlValueAccessor. - */ - private _value: FileList | File = undefined; - - // get/set accessor (needed for ControlValueAccessor) - get value(): FileList | File { return this._value; } - set value(v: FileList | File) { - if (v !== this._value) { - this._value = v; - this.onChange(v); - this._changeDetectorRef.markForCheck(); - } - } +export class TdFileUploadComponent extends _TdFileUploadMixinBase implements IControlValueAccessor, ICanDisable { private _multiple: boolean = false; private _required: boolean = false; @@ -129,8 +110,8 @@ export class TdFileUploadComponent extends _TdFileUploadMixinBase implements Con */ @Output('cancel') onCancel: EventEmitter = new EventEmitter(); - constructor(private _changeDetectorRef: ChangeDetectorRef) { - super(); + constructor(_changeDetectorRef: ChangeDetectorRef) { + super(_changeDetectorRef); } /** @@ -169,23 +150,4 @@ export class TdFileUploadComponent extends _TdFileUploadMixinBase implements Con this.cancel(); } } - - /** - * Implemented as part of ControlValueAccessor. - */ - writeValue(value: any): void { - this.value = value; - this._changeDetectorRef.markForCheck(); - } - - registerOnChange(fn: any): void { - this.onChange = fn; - } - - registerOnTouched(fn: any): void { - this.onTouched = fn; - } - - onChange = (_: any) => noop; - onTouched = () => noop; }