Skip to content

Commit

Permalink
feat(radio): add required attribute to radio-group (#5751)
Browse files Browse the repository at this point in the history
* feat(radio): add required attribute to radio-group

* chore(radio): add sample using required attribute in demo-app

* feat(radio): add required attribute to radio-button

* refactor(radio): change let to const when possible

* style(radio): remove trailing white-spaces

* docs: revert radio.md
  • Loading branch information
rafaelss95 authored and andrewseguin committed Jul 25, 2017
1 parent 36f708c commit f06fe11
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 25 deletions.
12 changes: 11 additions & 1 deletion src/demo-app/radio/radio-demo.html
Expand Up @@ -21,10 +21,20 @@ <h1>Dynamic Example</h1>
Disable buttons
</button>
</div>
<div>
<span>isRequired: {{isRequired}}</span>
<button md-raised-button (click)="isRequired=!isRequired" class="demo-button">
Require buttons
</button>
</div>
<div>
<span><md-checkbox [(ngModel)]="isAlignEnd">Align end</md-checkbox></span>
</div>
<md-radio-group name="my_options" [disabled]="isDisabled" [align]="isAlignEnd ? 'end' : 'start'">
<md-radio-group
name="my_options"
[disabled]="isDisabled"
[required]="isRequired"
[align]="isAlignEnd ? 'end' : 'start'">
<md-radio-button value="option_1">Option 1</md-radio-button>
<md-radio-button value="option_2">Option 2</md-radio-button>
<md-radio-button value="option_3">Option 3</md-radio-button>
Expand Down
3 changes: 2 additions & 1 deletion src/demo-app/radio/radio-demo.ts
Expand Up @@ -8,8 +8,9 @@ import {Component} from '@angular/core';
styleUrls: ['radio-demo.css'],
})
export class RadioDemo {
isDisabled: boolean = false;
isAlignEnd: boolean = false;
isDisabled: boolean = false;
isRequired: boolean = false;
favoriteSeason: string = 'Autumn';
seasonOptions = [
'Winter',
Expand Down
1 change: 1 addition & 0 deletions src/lib/radio/radio.html
Expand Up @@ -16,6 +16,7 @@
[checked]="checked"
[disabled]="disabled"
[name]="name"
[required]="required"
[attr.aria-label]="ariaLabel"
[attr.aria-labelledby]="ariaLabelledby"
(change)="_onInputChange($event)"
Expand Down
55 changes: 37 additions & 18 deletions src/lib/radio/radio.spec.ts
Expand Up @@ -62,7 +62,7 @@ describe('MdRadio', () => {

it('should set individual radio names based on the group name', () => {
expect(groupInstance.name).toBeTruthy();
for (let radio of radioInstances) {
for (const radio of radioInstances) {
expect(radio.name).toBe(groupInstance.name);
}
});
Expand Down Expand Up @@ -92,14 +92,14 @@ describe('MdRadio', () => {
testComponent.labelPos = 'before';
fixture.detectChanges();

for (let radio of radioInstances) {
for (const radio of radioInstances) {
expect(radio.labelPosition).toBe('before');
}

testComponent.labelPos = 'after';
fixture.detectChanges();

for (let radio of radioInstances) {
for (const radio of radioInstances) {
expect(radio.labelPosition).toBe('after');
}
});
Expand All @@ -108,11 +108,20 @@ describe('MdRadio', () => {
testComponent.isGroupDisabled = true;
fixture.detectChanges();

for (let radio of radioInstances) {
for (const radio of radioInstances) {
expect(radio.disabled).toBe(true);
}
});

it('should set required to each radio button when the group is required', () => {
testComponent.isGroupRequired = true;
fixture.detectChanges();

for (const radio of radioInstances) {
expect(radio.required).toBe(true);
}
});

it('should update the group value when one of the radios changes', () => {
expect(groupInstance.value).toBeFalsy();

Expand Down Expand Up @@ -155,7 +164,7 @@ describe('MdRadio', () => {
it('should emit a change event from radio buttons', () => {
expect(radioInstances[0].checked).toBe(false);

let spies = radioInstances
const spies = radioInstances
.map((radio, index) => jasmine.createSpy(`onChangeSpy ${index} for ${radio.name}`));

spies.forEach((spy, index) => radioInstances[index].change.subscribe(spy));
Expand All @@ -178,7 +187,7 @@ describe('MdRadio', () => {
programmatically`, () => {
expect(groupInstance.value).toBeFalsy();

let changeSpy = jasmine.createSpy('radio-group change listener');
const changeSpy = jasmine.createSpy('radio-group change listener');
groupInstance.change.subscribe(changeSpy);

radioLabelElements[0].click();
Expand Down Expand Up @@ -265,7 +274,7 @@ describe('MdRadio', () => {
testComponent.disableRipple = true;
fixture.detectChanges();

for (let radioLabel of radioLabelElements) {
for (const radioLabel of radioLabelElements) {
dispatchFakeEvent(radioLabel, 'mousedown');
dispatchFakeEvent(radioLabel, 'mouseup');

Expand All @@ -275,7 +284,7 @@ describe('MdRadio', () => {
testComponent.disableRipple = false;
fixture.detectChanges();

for (let radioLabel of radioLabelElements) {
for (const radioLabel of radioLabelElements) {
dispatchFakeEvent(radioLabel, 'mousedown');
dispatchFakeEvent(radioLabel, 'mouseup');

Expand All @@ -285,7 +294,7 @@ describe('MdRadio', () => {

it(`should update the group's selected radio to null when unchecking that radio
programmatically`, () => {
let changeSpy = jasmine.createSpy('radio-group change listener');
const changeSpy = jasmine.createSpy('radio-group change listener');
groupInstance.change.subscribe(changeSpy);
radioInstances[0].checked = true;

Expand All @@ -305,7 +314,7 @@ describe('MdRadio', () => {
});

it('should not fire a change event from the group when a radio checked state changes', () => {
let changeSpy = jasmine.createSpy('radio-group change listener');
const changeSpy = jasmine.createSpy('radio-group change listener');
groupInstance.change.subscribe(changeSpy);
radioInstances[0].checked = true;

Expand All @@ -324,7 +333,7 @@ describe('MdRadio', () => {
});

it(`should update checked status if changed value to radio group's value`, () => {
let changeSpy = jasmine.createSpy('radio-group change listener');
const changeSpy = jasmine.createSpy('radio-group change listener');
groupInstance.change.subscribe(changeSpy);
groupInstance.value = 'apple';

Expand Down Expand Up @@ -403,25 +412,25 @@ describe('MdRadio', () => {

it('should set individual radio names based on the group name', () => {
expect(groupInstance.name).toBeTruthy();
for (let radio of radioInstances) {
for (const radio of radioInstances) {
expect(radio.name).toBe(groupInstance.name);
}

groupInstance.name = 'new name';

for (let radio of radioInstances) {
for (const radio of radioInstances) {
expect(radio.name).toBe(groupInstance.name);
}
});

it('should check the corresponding radio button on group value change', () => {
expect(groupInstance.value).toBeFalsy();
for (let radio of radioInstances) {
for (const radio of radioInstances) {
expect(radio.checked).toBeFalsy();
}

groupInstance.value = 'vanilla';
for (let radio of radioInstances) {
for (const radio of radioInstances) {
expect(radio.checked).toBe(groupInstance.value === radio.value);
}
expect(groupInstance.selected!.value).toBe(groupInstance.value);
Expand Down Expand Up @@ -539,12 +548,12 @@ describe('MdRadio', () => {
.filter(debugEl => debugEl.componentInstance.name == 'fruit')
.map(debugEl => debugEl.componentInstance);

let fruitRadioNativeElements = radioDebugElements
const fruitRadioNativeElements = radioDebugElements
.filter(debugEl => debugEl.componentInstance.name == 'fruit')
.map(debugEl => debugEl.nativeElement);

fruitRadioNativeInputs = [];
for (let element of fruitRadioNativeElements) {
for (const element of fruitRadioNativeElements) {
fruitRadioNativeInputs.push(<HTMLElement> element.querySelector('input'));
}
});
Expand Down Expand Up @@ -579,6 +588,14 @@ describe('MdRadio', () => {
expect(weatherRadioInstances[2].checked).toBe(true);
});

it('should add required attribute to the underlying input element if defined', () => {
const radioInstance = seasonRadioInstances[0];
radioInstance.required = true;
fixture.detectChanges();

expect(radioInstance.required).toBe(true);
});

it('should add aria-label attribute to the underlying input element if defined', () => {
expect(fruitRadioNativeInputs[0].getAttribute('aria-label')).toBe('Banana');
});
Expand Down Expand Up @@ -630,6 +647,7 @@ describe('MdRadio', () => {
template: `
<md-radio-group [disabled]="isGroupDisabled"
[labelPosition]="labelPos"
[required]="isGroupRequired"
[value]="groupValue"
name="test-name">
<md-radio-button value="fire" [disableRipple]="disableRipple" [disabled]="isFirstDisabled"
Expand All @@ -647,8 +665,9 @@ describe('MdRadio', () => {
})
class RadiosInsideRadioGroup {
labelPos: 'before' | 'after';
isGroupDisabled: boolean = false;
isFirstDisabled: boolean = false;
isGroupDisabled: boolean = false;
isGroupRequired: boolean = false;
groupValue: string | null = null;
disableRipple: boolean = false;
color: string | null;
Expand Down
33 changes: 28 additions & 5 deletions src/lib/radio/radio.ts
Expand Up @@ -105,6 +105,9 @@ export class MdRadioGroup extends _MdRadioGroupMixinBase
/** Whether the radio group is disabled. */
private _disabled: boolean = false;

/** Whether the radio group is required. */
private _required: boolean = false;

/** The method to be called in order to update ngModel */
_controlValueAccessorChangeFn: (value: any) => void = () => {};

Expand Down Expand Up @@ -189,12 +192,20 @@ export class MdRadioGroup extends _MdRadioGroupMixinBase

/** Whether the radio group is disabled */
@Input()
get disabled() { return this._disabled; }
get disabled(): boolean { return this._disabled; }
set disabled(value) {
this._disabled = coerceBooleanProperty(value);
this._markRadiosForCheck();
}

/** Whether the radio group is required */
@Input()
get required(): boolean { return this._required; }
set required(value: boolean) {
this._required = coerceBooleanProperty(value);
this._markRadiosForCheck();
}

constructor(private _changeDetector: ChangeDetectorRef) {
super();
}
Expand Down Expand Up @@ -231,7 +242,7 @@ export class MdRadioGroup extends _MdRadioGroupMixinBase
/** Updates the `selected` radio button from the internal _value state. */
private _updateSelectedRadioFromValue(): void {
// If the value already matches the selected radio, do nothing.
let isAlreadySelected = this._selected != null && this._selected.value == this._value;
const isAlreadySelected = this._selected != null && this._selected.value == this._value;

if (this._radios != null && !isAlreadySelected) {
this._selected = null;
Expand All @@ -247,7 +258,7 @@ export class MdRadioGroup extends _MdRadioGroupMixinBase
/** Dispatch change event with current selection and group value. */
_emitChangeEvent(): void {
if (this._isInitialized) {
let event = new MdRadioChange();
const event = new MdRadioChange();
event.source = this._selected;
event.value = this._value;
this.change.emit(event);
Expand Down Expand Up @@ -424,6 +435,15 @@ export class MdRadioButton extends _MdRadioButtonMixinBase
this._disabled = coerceBooleanProperty(value);
}

/** Whether the radio button is required. */
@Input()
get required(): boolean {
return this._required || (this.radioGroup && this.radioGroup.required);
}
set required(value: boolean) {
this._required = coerceBooleanProperty(value);
}

/**
* Event emitted when the checked state of this radio button changes.
* Change events are only emitted when the value changes due to user interaction with
Expand All @@ -443,6 +463,9 @@ export class MdRadioButton extends _MdRadioButtonMixinBase
/** Whether this radio is disabled. */
private _disabled: boolean;

/** Whether this radio is required. */
private _required: boolean;

/** Value assigned to this radio.*/
private _value: any = null;

Expand Down Expand Up @@ -516,7 +539,7 @@ export class MdRadioButton extends _MdRadioButtonMixinBase

/** Dispatch change event with current value. */
private _emitChangeEvent(): void {
let event = new MdRadioChange();
const event = new MdRadioChange();
event.source = this;
event.value = this._value;
this.change.emit(event);
Expand Down Expand Up @@ -547,7 +570,7 @@ export class MdRadioButton extends _MdRadioButtonMixinBase
// emit its event object to the `change` output.
event.stopPropagation();

let groupValueChanged = this.radioGroup && this.value != this.radioGroup.value;
const groupValueChanged = this.radioGroup && this.value != this.radioGroup.value;
this.checked = true;
this._emitChangeEvent();

Expand Down

0 comments on commit f06fe11

Please sign in to comment.