From 4ca62c6a6542cdfbd8cfbf639a91c2173f192440 Mon Sep 17 00:00:00 2001 From: Nolan Arnold Date: Tue, 12 May 2020 11:35:07 -0500 Subject: [PATCH] feat(design): pushed CVA implementation to a directive --- .../radio/cva/radio-cva.directive.spec.ts | 93 ++++++++++++++++++ .../form/radio/cva/radio-cva.directive.ts | 94 +++++++++++++++++++ .../src/atoms/form/radio/radio.component.ts | 47 +++++++++- libs/design/src/atoms/form/radio/radio.html | 6 +- .../src/atoms/form/radio/radio.module.ts | 27 +++++- .../radio/registry/radio-registry.spec.ts | 17 ++++ .../form/radio/registry/radio-registry.ts | 59 ++++++++++++ .../form/radio/specs/radio-with-set.spec.ts | 40 +++----- .../radio/specs/radio.accessibility.spec.ts | 14 +-- .../atoms/form/radio/specs/radio.cva.spec.ts | 26 ++--- 10 files changed, 368 insertions(+), 55 deletions(-) create mode 100644 libs/design/src/atoms/form/radio/cva/radio-cva.directive.spec.ts create mode 100644 libs/design/src/atoms/form/radio/cva/radio-cva.directive.ts create mode 100644 libs/design/src/atoms/form/radio/registry/radio-registry.spec.ts create mode 100644 libs/design/src/atoms/form/radio/registry/radio-registry.ts diff --git a/libs/design/src/atoms/form/radio/cva/radio-cva.directive.spec.ts b/libs/design/src/atoms/form/radio/cva/radio-cva.directive.spec.ts new file mode 100644 index 0000000000..8135cab3bc --- /dev/null +++ b/libs/design/src/atoms/form/radio/cva/radio-cva.directive.spec.ts @@ -0,0 +1,93 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, async, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule, FormControl } from '@angular/forms'; +import { By } from '@angular/platform-browser'; + +import { DaffRadioComponent } from '../radio.component'; +import { DaffRadioModule } from '../radio.module'; + + + +@Component({ + template: ` + + ` +}) +class RadioWrapperComponent { + radio = new FormControl() +} + +describe('DaffRadioControlValueAccessorDirective', () => { + + describe('with the directive', () => { + let radioWrapper: RadioWrapperComponent; + + let component: DaffRadioComponent; + + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + RadioWrapperComponent, + ], + imports: [ + ReactiveFormsModule, + DaffRadioModule + ] + }) + .compileComponents(); + })); + describe('the DaffRadioComponent', () => { + + beforeEach(() => { + + fixture = TestBed.createComponent(RadioWrapperComponent); + radioWrapper = fixture.componentInstance; + component = fixture.debugElement.query(By.css('daff-radio')).componentInstance; + fixture.detectChanges(); + }); + it('has the writeValue function for formControls', async () => { + + expect(component.checked).toEqual(false); + + radioWrapper.radio.setValue('testValue'); + + expect(component.checked).toEqual(true); + }); + }); + + }); + describe('without the directive', () => { + let radioWrapper: RadioWrapperComponent; + + let component: DaffRadioComponent; + + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + RadioWrapperComponent, + DaffRadioComponent + ], + imports: [ + ReactiveFormsModule, + ] + }) + .compileComponents(); + })); + describe('the DaffRadioComponent', () => { + + it('throws an error without the directive to give the itself the CVA interface', async () => { + fixture = TestBed.createComponent(RadioWrapperComponent); + radioWrapper = fixture.componentInstance; + expect(() => { + component = fixture.debugElement.query(By.css('daff-radio')).componentInstance; + fixture.detectChanges(); + }).toThrowError() + }); + + }); + }); +}); \ No newline at end of file diff --git a/libs/design/src/atoms/form/radio/cva/radio-cva.directive.ts b/libs/design/src/atoms/form/radio/cva/radio-cva.directive.ts new file mode 100644 index 0000000000..4ee0510e65 --- /dev/null +++ b/libs/design/src/atoms/form/radio/cva/radio-cva.directive.ts @@ -0,0 +1,94 @@ +import { Directive, Input, OnInit, Self, Optional } from '@angular/core'; +import { NgControl, ControlValueAccessor } from '@angular/forms'; +import { DaffRadioComponent } from '../radio.component'; +import { DaffRadioRegistry } from '../registry/radio-registry'; + +/** + * ControlValueAccessor functionality for the DaffRadio + */ +@Directive({ + // tslint:disable-next-line: directive-selector + selector: 'daff-radio[ngModel], daff-radio[formControl], daff-radio[formControlName]' +}) +export class DaffRadioControlValueAccessorDirective implements OnInit, ControlValueAccessor { + _onChange: () => void; + _onTouched: () => void; + + /** + * The value of the ControlValueAccessor + */ + @Input() value: any; + + /** + * The name of the ControlValueAccessor + */ + @Input() name: string; + + constructor( + @Optional() @Self() public _control: NgControl, + private _registry: DaffRadioRegistry, + private _radio: DaffRadioComponent + ) { + if (this._control != null) { + this._control.valueAccessor = this; + } + } + + ngOnInit(): void { + this.writeValue(this._control.value); + this._registry.add(this._control, this); + + this._radio.selectionChange.subscribe( + value => value ? this._onChange() : null + ); + } + /** + * + * writeValue function from the CVA interface + */ + writeValue(value: any): void { + console.log(value); + if (this.value === value) { + this._onChange(); + this.fireSelect(); + } + } + + /** + * registerOnChange implemented from the CVA interface + */ + registerOnChange(fn: any): void { + this._onChange = () => { + fn(this.value); + this._registry.select(this); + }; + } + + /** + * registerOnTouch implemented from the CVA interface + */ + registerOnTouched(fn: any): void { + this._onTouched = fn; + } + + /** + * sets the disabled state. + */ + setDisabledState?(isDisabled: boolean): void { + this._radio.disabled = isDisabled; + } + + /** + calls select function for the radio + */ + fireSelect() { + this._radio.select(); + } + + /** + calls deselect function for the radio + */ + fireDeselect() { + this._radio.deselect(); + } +} diff --git a/libs/design/src/atoms/form/radio/radio.component.ts b/libs/design/src/atoms/form/radio/radio.component.ts index a443e1b04f..f1a0706b33 100644 --- a/libs/design/src/atoms/form/radio/radio.component.ts +++ b/libs/design/src/atoms/form/radio/radio.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, Input, ElementRef, Renderer2, HostBinding, ViewEncapsulation, ChangeDetectionStrategy, HostListener, forwardRef, ViewChild, Optional } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Component, OnInit, Input, HostBinding, ChangeDetectionStrategy, forwardRef, Optional, Output, EventEmitter } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { DaffRadioSetComponent } from '../radio-set/radio-set.component'; let radioUniqueId = 0; @@ -18,7 +18,7 @@ let radioUniqueId = 0; changeDetection: ChangeDetectionStrategy.OnPush }) -export class DaffRadioComponent implements ControlValueAccessor, OnInit { +export class DaffRadioComponent implements OnInit { onChange: () => {}; onTouched: () => {}; @@ -30,10 +30,30 @@ export class DaffRadioComponent implements ControlValueAccessor, OnInit { return this.disabled === true; }; + /** + * Output event of selection being changed + */ + @Output() selectionChange: EventEmitter = new EventEmitter(); + + @Input() checked = false; + /** + * The value of the radio + */ @Input() value: any; + /** + * The id of the radio. It is uniquely generated but can be overwritten by the user. Must be unique. + */ @Input() id: string = 'daff-radio-' + radioUniqueId; + /** + * Name of the Radio + */ @Input() name: string; + /** + * Used for aria-label. Default to name if user does not input a label. + */ + @Input() label = name; + disabled = false; focused = false; @@ -44,14 +64,31 @@ export class DaffRadioComponent implements ControlValueAccessor, OnInit { this.name = this.radioset ? this.radioset.name : this.name } + + + /** + * updates Focus styling + */ onFocus() { this.focused = true; } + /** + * updates Blur styling + */ onBlur() { this.focused = false; } - writeValue(value: any): void { - this.checked = value === this.value ? true : false; + /** + * toggeles checked attribute on + */ + select(): void { + this.checked = true; + } + /** + * toggeles checked attribute off + */ + deselect(): void { + this.checked = false; } registerOnChange(fn: any): void { this.onChange = fn; diff --git a/libs/design/src/atoms/form/radio/radio.html b/libs/design/src/atoms/form/radio/radio.html index 0c8ada594d..314d00e300 100644 --- a/libs/design/src/atoms/form/radio/radio.html +++ b/libs/design/src/atoms/form/radio/radio.html @@ -1,6 +1,8 @@ { + beforeEach(() => TestBed.configureTestingModule({ + imports: [ + DaffRadioModule + ] + })); + + it('should be created', () => { + const service: DaffRadioRegistry = TestBed.get(DaffRadioRegistry); + expect(service).toBeTruthy(); + }); +}); diff --git a/libs/design/src/atoms/form/radio/registry/radio-registry.ts b/libs/design/src/atoms/form/radio/registry/radio-registry.ts new file mode 100644 index 0000000000..f62fa3f211 --- /dev/null +++ b/libs/design/src/atoms/form/radio/registry/radio-registry.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { NgControl } from '@angular/forms'; +import { DaffRadioControlValueAccessorDirective } from '../cva/radio-cva.directive'; + +export interface ControlAccessorPair { + control: NgControl; + accessor: DaffRadioControlValueAccessorDirective; +} + +@Injectable() +export class DaffRadioRegistry { + private _accessors: ControlAccessorPair[] = []; + + /** + * @description + * Adds a control to the internal registry. + */ + add(control: NgControl, accessor: DaffRadioControlValueAccessorDirective) { + this._accessors.push({ + control: control, + accessor: accessor + }); + } + + /** + * @description + * Removes a control from the internal registry. + */ + remove(accessor: DaffRadioControlValueAccessorDirective) { + for (let i = this._accessors.length - 1; i >= 0; --i) { + if (this._accessors[i]['accessor'] === accessor) { + this._accessors.splice(i, 1); + return; + } + } + } + + /** + * @description + * Selects a radio button. + */ + select(accessor: DaffRadioControlValueAccessorDirective) { + this._accessors.forEach((c) => { + if (this._isSameGroup(c, accessor) && c['accessor'] !== accessor) { + c['accessor'].fireDeselect(); + } + }); + } + + private _isSameGroup ( + controlPair: ControlAccessorPair, + accessor: DaffRadioControlValueAccessorDirective): boolean { + if (!controlPair['control'].control) { + return false; + } + return controlPair['control'].control.parent === accessor._control.control.parent + && controlPair['accessor'].name === accessor.name; + } +} diff --git a/libs/design/src/atoms/form/radio/specs/radio-with-set.spec.ts b/libs/design/src/atoms/form/radio/specs/radio-with-set.spec.ts index 118ddb1449..5dc29dd20c 100644 --- a/libs/design/src/atoms/form/radio/specs/radio-with-set.spec.ts +++ b/libs/design/src/atoms/form/radio/specs/radio-with-set.spec.ts @@ -1,17 +1,19 @@ -import { DaffRadioComponent } from "../radio.component"; -import { ComponentFixture, async, TestBed } from "@angular/core/testing"; -import { FormGroup, FormControl, ReactiveFormsModule } from "@angular/forms"; -import { Component } from "@angular/core"; -import { DaffRadioModule } from "../radio.module"; -import { By } from "@angular/platform-browser"; +import { By } from '@angular/platform-browser'; + +import { ComponentFixture, async, TestBed } from '@angular/core/testing'; +import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; +import { Component } from '@angular/core'; +import { DaffRadioModule } from '../radio.module'; +import { DaffRadioComponent } from '../radio.component'; + @Component({ template: ` - - Apple - Grape - Peach + + Apple + Grape + Peach ` }) @@ -19,12 +21,7 @@ class RadioEmbeddedComponent { radioGroup = new FormGroup({ fruit: new FormControl() }); - disable() { - this.radioGroup.disable(); - } - setValue() { - this.radioGroup.setValue({ fruit: 'pear' }); - } + } describe('DaffRadioComponent within a daff-radio-set', () => { let radioEmbedded: RadioEmbeddedComponent; @@ -66,17 +63,6 @@ describe('DaffRadioComponent within a daff-radio-set', () => { it('should have a generated id', () => { expect(embeddedComponent.id).toMatch('daff-radio-[0-9]*') }); - describe('and the control value accessor implementation', () => { - it('should let the value be set from a form control', () => { - embeddedComponent.checked = true; - radioEmbedded.setValue(); - expect(embeddedComponent.checked).toEqual(false); - }); - it('should let the radio be disabled from a form control', () => { - radioEmbedded.disable(); - expect(embeddedComponent.disabled).toEqual(true); - }); - }); }) }); diff --git a/libs/design/src/atoms/form/radio/specs/radio.accessibility.spec.ts b/libs/design/src/atoms/form/radio/specs/radio.accessibility.spec.ts index a8a42ba147..45e55f626f 100644 --- a/libs/design/src/atoms/form/radio/specs/radio.accessibility.spec.ts +++ b/libs/design/src/atoms/form/radio/specs/radio.accessibility.spec.ts @@ -1,15 +1,15 @@ -import { Component } from "@angular/core"; -import { ComponentFixture, async, TestBed } from "@angular/core/testing"; +import { Component } from '@angular/core'; +import { ComponentFixture, async, TestBed } from '@angular/core/testing'; -import { DaffRadioComponent } from "../radio.component"; -import { ReactiveFormsModule } from "@angular/forms"; -import { DaffRadioModule } from "../radio.module"; -import { By } from "@angular/platform-browser"; +import { ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { DaffRadioModule } from '../radio.module'; +import { DaffRadioComponent } from '../radio.component'; @Component({ template: ` - + ` }) class RadioWrapperComponent { } diff --git a/libs/design/src/atoms/form/radio/specs/radio.cva.spec.ts b/libs/design/src/atoms/form/radio/specs/radio.cva.spec.ts index 476cf41f5d..110ff7c96f 100644 --- a/libs/design/src/atoms/form/radio/specs/radio.cva.spec.ts +++ b/libs/design/src/atoms/form/radio/specs/radio.cva.spec.ts @@ -1,23 +1,24 @@ -import { DaffRadioComponent } from "../radio.component"; -import { ComponentFixture, async, TestBed } from "@angular/core/testing"; -import { FormGroup, FormControl, ReactiveFormsModule } from "@angular/forms"; -import { Component } from "@angular/core"; -import { DaffRadioModule } from "../radio.module"; -import { By } from "@angular/platform-browser"; +import { ComponentFixture, async, TestBed } from '@angular/core/testing'; +import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; +import { Component } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +import { DaffRadioComponent } from '../radio.component'; +import { DaffRadioModule } from '../radio.module'; @Component({ template: ` - - Apple - Grape - Peach + + Apple + Grape + Peach ` }) class RadioEmbeddedComponent { radioGroup = new FormGroup({ - fruit: new FormControl() + fruit: new FormControl('apple') }); disable() { this.radioGroup.disable(); @@ -55,9 +56,8 @@ describe('DaffRadioComponent', () => { embeddedFixture.detectChanges(); }); it('should let the value be set from a form control', () => { - embeddedComponent.checked = true; radioEmbedded.setValue(); - expect(embeddedComponent.checked).toEqual(false); + expect(radioEmbedded.radioGroup.value).toEqual({'fruit': 'pear'}); }); it('should let the radio be disabled from a form control', () => { radioEmbedded.disable();