From fdd8e564e2c47df164e21d63cd0a35c575c29b4d Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 19 Nov 2019 10:39:49 -0500 Subject: [PATCH] Added required input to radio group (#79) * Added input property for radio group * code style cleanup * tightening up the screws --- .../radio-group.component.fixture.html | 12 +++- .../fixtures/radio-group.component.fixture.ts | 21 ++++--- .../modules/radio/radio-group.component.html | 4 +- .../radio/radio-group.component.spec.ts | 41 +++++++++++- .../modules/radio/radio-group.component.ts | 63 +++++++++++++------ .../visual/radio/radio-visual.component.html | 15 ++++- .../visual/radio/radio-visual.component.ts | 11 +++- 7 files changed, 130 insertions(+), 37 deletions(-) diff --git a/src/app/public/modules/radio/fixtures/radio-group.component.fixture.html b/src/app/public/modules/radio/fixtures/radio-group.component.fixture.html index 447cf163..88b9cb33 100644 --- a/src/app/public/modules/radio/fixtures/radio-group.component.fixture.html +++ b/src/app/public/modules/radio/fixtures/radio-group.component.fixture.html @@ -1,17 +1,23 @@ -
+ + id="radio-group-label" + > Reactive Radio button options: -
    +
    • diff --git a/src/app/public/modules/radio/radio-group.component.spec.ts b/src/app/public/modules/radio/radio-group.component.spec.ts index af82a12b..2910ee34 100644 --- a/src/app/public/modules/radio/radio-group.component.spec.ts +++ b/src/app/public/modules/radio/radio-group.component.spec.ts @@ -24,16 +24,22 @@ describe('Radio group component', function () { let fixture: ComponentFixture; let componentInstance: SkyRadioGroupTestComponent; + //#region helpers function getRadios(radioFixture: ComponentFixture) { return radioFixture.nativeElement.querySelectorAll('.sky-radio-input'); } + function getRadioGroup(radioFixture: ComponentFixture): HTMLElement { + return radioFixture.nativeElement.querySelector('.sky-radio-group'); + } + function clickCheckbox(radioFixture: ComponentFixture, index: number) { const radios = getRadios(radioFixture); radios.item(index).click(); fixture.detectChanges(); tick(); } + //#endregion beforeEach(function () { TestBed.configureTestingModule({ @@ -138,6 +144,37 @@ describe('Radio group component', function () { expect(componentInstance.radioForm.value.option.name).toBe('Harima Kenji'); })); + it('should not show a required state when not required', fakeAsync(() => { + fixture.detectChanges(); + tick(); + + const radioGroupDiv = getRadioGroup(fixture); + expect(radioGroupDiv.getAttribute('required')).toBeNull(); + expect(radioGroupDiv.getAttribute('aria-required')).toBeNull(); + })); + + it('should show a required state when required input is set to true', fakeAsync(() => { + componentInstance.required = true; + + fixture.detectChanges(); + tick(); + + const radioGroupDiv = getRadioGroup(fixture); + expect(radioGroupDiv.getAttribute('required')).not.toBeNull(); + expect(radioGroupDiv.getAttribute('aria-required')).toBe('true'); + })); + + it('should update the ngModel properly when radio button is required and changed', fakeAsync(() => { + componentInstance.required = true; + fixture.detectChanges(); + + expect(componentInstance.radioForm.valid).toBe(false); + + clickCheckbox(fixture, 1); + + expect(componentInstance.radioForm.valid).toBe(true); + })); + it('should use tabIndex when specified', fakeAsync(function () { componentInstance.tabIndex = 2; fixture.detectChanges(); @@ -260,7 +297,7 @@ describe('Radio group component', function () { fixture.detectChanges(); tick(); - const radioGroupDiv = fixture.nativeElement.querySelector('.sky-radio-group'); + const radioGroupDiv = getRadioGroup(fixture); expect(radioGroupDiv.getAttribute('aria-labelledby')).toBe('radio-group-label'); })); @@ -271,7 +308,7 @@ describe('Radio group component', function () { fixture.detectChanges(); tick(); - const radioGroupDiv = fixture.nativeElement.querySelector('.sky-radio-group'); + const radioGroupDiv = getRadioGroup(fixture); expect(radioGroupDiv.getAttribute('aria-label')).toBe('radio-group-label-manual'); })); diff --git a/src/app/public/modules/radio/radio-group.component.ts b/src/app/public/modules/radio/radio-group.component.ts index af466234..d9c85edf 100644 --- a/src/app/public/modules/radio/radio-group.component.ts +++ b/src/app/public/modules/radio/radio-group.component.ts @@ -1,23 +1,28 @@ -// #region imports import { AfterContentInit, + AfterViewInit, + ChangeDetectorRef, Component, ContentChildren, - forwardRef, Input, OnDestroy, - QueryList + Optional, + QueryList, + Self } from '@angular/core'; import { - ControlValueAccessor, - NG_VALUE_ACCESSOR + NgControl } from '@angular/forms'; import { Subject } from 'rxjs/Subject'; +import { + SkyFormsUtility +} from '../shared/forms-utility'; + import { SkyRadioChange } from './types'; @@ -25,25 +30,15 @@ import { import { SkyRadioComponent } from './radio.component'; -// #endregion let nextUniqueId = 0; -const SKY_RADIO_GROUP_CONTROL_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, - // tslint:disable-next-line:no-forward-ref no-use-before-declare - useExisting: forwardRef(() => SkyRadioGroupComponent), - multi: true -}; - @Component({ selector: 'sky-radio-group', - templateUrl: './radio-group.component.html', - providers: [ - SKY_RADIO_GROUP_CONTROL_VALUE_ACCESSOR - ] + templateUrl: './radio-group.component.html' }) -export class SkyRadioGroupComponent implements AfterContentInit, ControlValueAccessor, OnDestroy { +export class SkyRadioGroupComponent implements AfterContentInit, AfterViewInit, OnDestroy { + @Input() public ariaLabelledBy: string; @@ -59,6 +54,15 @@ export class SkyRadioGroupComponent implements AfterContentInit, ControlValueAcc return this._name; } + /** + * Indicates whether the input is required for form validation. + * When you set this property to `true`, the component adds `aria-required` and `required` + * attributes to the input element so that forms display an invalid state until the input element + * is complete. This property accepts a `boolean` value. + */ + @Input() + public required: boolean = false; + @Input() public set value(value: any) { const isNewValue = value !== this._value; @@ -90,9 +94,20 @@ export class SkyRadioGroupComponent implements AfterContentInit, ControlValueAcc private ngUnsubscribe = new Subject(); private _name = `sky-radio-group-${nextUniqueId++}`; - private _value: any; + private _tabIndex: number; + private _value: any; + + constructor( + private changeDetector: ChangeDetectorRef, + @Self() @Optional() private ngControl: NgControl + ) { + if (this.ngControl) { + this.ngControl.valueAccessor = this; + } + } + public ngAfterContentInit(): void { this.resetRadioButtons(); @@ -109,6 +124,16 @@ export class SkyRadioGroupComponent implements AfterContentInit, ControlValueAcc }); } + public ngAfterViewInit(): void { + if (this.ngControl) { + // Backwards compatibility support for anyone still using Validators.Required. + this.required = this.required || SkyFormsUtility.hasRequiredValidation(this.ngControl); + + // Avoid an ExpressionChangedAfterItHasBeenCheckedError. + this.changeDetector.detectChanges(); + } + } + public watchForSelections() { this.radios.forEach((radio) => { radio.change diff --git a/src/app/visual/radio/radio-visual.component.html b/src/app/visual/radio/radio-visual.component.html index 23fb2cd7..a38229ad 100644 --- a/src/app/visual/radio/radio-visual.component.html +++ b/src/app/visual/radio/radio-visual.component.html @@ -1,3 +1,11 @@ + +

      Radio buttons (reactive)

      @@ -8,6 +16,7 @@

      >

      What is your favorite season?

      @@ -15,6 +24,7 @@

        Radio buttons (template-driven)

      -

      +

      What is your favorite season?

      diff --git a/src/app/visual/radio/radio-visual.component.ts b/src/app/visual/radio/radio-visual.component.ts index aa59141a..ae47bc65 100644 --- a/src/app/visual/radio/radio-visual.component.ts +++ b/src/app/visual/radio/radio-visual.component.ts @@ -13,12 +13,15 @@ import { templateUrl: './radio-visual.component.html' }) export class RadioVisualComponent implements OnInit { - public selectedValue = '3'; + public iconSelectedValue = '1'; - public valueGuy = '2'; + public radioForm: FormGroup; + public radioValue: any; + public required: boolean = false; + public seasons = [ { name: 'Spring', disabled: false }, { name: 'Summer', disabled: false }, @@ -26,6 +29,10 @@ export class RadioVisualComponent implements OnInit { { name: 'Winter', disabled: false } ]; + public selectedValue = '3'; + + public valueGuy = '2'; + constructor( private formBuilder: FormBuilder ) { }