Skip to content

Commit

Permalink
feat(mixin): add ngModel mixin for reuse (control value accessor) (#…
Browse files Browse the repository at this point in the history
…1024)

* feat(mixin): add ngModel mixin for reuse (control value accessor)

* feat(): use ngModel mixin where possible
  • Loading branch information
emoralesb05 authored Dec 13, 2017
1 parent ba2b5ed commit 7a30cb5
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 218 deletions.
73 changes: 18 additions & 55 deletions src/platform/core/chips/chips.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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: [{
Expand All @@ -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;
Expand Down Expand Up @@ -254,18 +247,6 @@ export class TdChipsComponent extends _TdChipsMixinBase implements ControlValueA
*/
@Output('chipBlur') onChipBlur: EventEmitter<any> = new EventEmitter<any>();

/**
* 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
*/
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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 */
},
};
}
66 changes: 66 additions & 0 deletions src/platform/core/common/behaviors/control-value-accesor.mixin.ts
Original file line number Diff line number Diff line change
@@ -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<any>;
onChange: (_: any) => any;
onTouched: () => any;
}

export interface IHasChangeDetectorRef {
_changeDetectorRef: ChangeDetectorRef;
}

/** Mixin to augment a component with ngModel support. */
export function mixinControlValueAccessor<T extends Constructor<IHasChangeDetectorRef>>
(base: T, initialValue?: any): Constructor<IControlValueAccessor> & T {
return class extends base {
private _value: any = initialValue;
private _subjectValueChanges: Subject<any>;
valueChanges: Observable<any>;

constructor(...args: any[]) {
super(...args);
this._subjectValueChanges = new Subject<any>();
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;

};
}
1 change: 1 addition & 0 deletions src/platform/core/common/common.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading

0 comments on commit 7a30cb5

Please sign in to comment.