Skip to content

Commit

Permalink
feat(radio): support ngModel on md-radio-group
Browse files Browse the repository at this point in the history
Closes #209
  • Loading branch information
doctorrustynelson authored and jelbourn committed Mar 22, 2016
1 parent 0b31d25 commit 6aff4cc
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 2 deletions.
9 changes: 9 additions & 0 deletions src/components/radio/README.md
Expand Up @@ -21,6 +21,15 @@ A dynamic example, populated from a `data` variable:
</md-radio-group>
```

A dynamic example for use inside a form showing support for `[(ngModel)]`:
```html
<md-radio-group [(ngModel)]="chosenOption">
<md-radio-button *ngFor="#o of options" [value]="o.value">
{{o.label}}
</md-radio-button>
</md-radio-group>
```

## `<md-radio-group>`
### Properties

Expand Down
76 changes: 75 additions & 1 deletion src/components/radio/radio.spec.ts
Expand Up @@ -258,6 +258,68 @@ export function main() {
});
}).then(done);
});

it('should bind value to model without initial value', (done: () => void) => {
builder
.overrideTemplate(TestApp, `
<md-radio-group [(ngModel)]="choice">
<md-radio-button [value]="0"></md-radio-button>
<md-radio-button [value]="1"></md-radio-button>
</md-radio-group>`)
.createAsync(TestApp)
.then((fixture) => {
fakeAsync(function() {
let buttons = fixture.debugElement.queryAll(By.css('md-radio-button'));
let group = fixture.debugElement.query(By.css('md-radio-group'));

fixture.detectChanges();
expect(buttons[0].componentInstance.checked).toBe(false);
expect(buttons[1].componentInstance.checked).toBe(false);
expect(fixture.componentInstance.choice).toBe(undefined);

group.componentInstance.selected = buttons[0].componentInstance;
fixture.detectChanges();
expect(isSinglySelected(buttons[0], buttons)).toBe(true);
expect(fixture.componentInstance.choice).toBe(0);

group.componentInstance.selected = buttons[1].componentInstance;
fixture.detectChanges();
expect(isSinglySelected(buttons[1], buttons)).toBe(true);
expect(fixture.componentInstance.choice).toBe(1);
});
}).then(done);
});

it('should bind value to model with initial value', (done: () => void) => {
builder
.overrideTemplate(TestAppWithInitialValue, `
<md-radio-group [(ngModel)]="choice">
<md-radio-button [value]="0"></md-radio-button>
<md-radio-button [value]="1"></md-radio-button>
</md-radio-group>`)
.createAsync(TestAppWithInitialValue)
.then((fixture) => {
fakeAsync(function() {
let buttons = fixture.debugElement.queryAll(By.css('md-radio-button'));
let group = fixture.debugElement.query(By.css('md-radio-group'));

fixture.detectChanges();
expect(isSinglySelected(buttons[1], buttons)).toBe(true);
expect(fixture.componentInstance.choice).toBe(1);

group.componentInstance.selected = buttons[0].componentInstance;
fixture.detectChanges();
expect(isSinglySelected(buttons[0], buttons)).toBe(true);
expect(fixture.componentInstance.choice).toBe(0);

group.componentInstance.selected = buttons[1].componentInstance;
fixture.detectChanges();
expect(isSinglySelected(buttons[1], buttons)).toBe(true);
expect(fixture.componentInstance.choice).toBe(1);
});
}).then(done);
});

});
}

Expand Down Expand Up @@ -289,4 +351,16 @@ function createEvent(name: string): Event {
providers: [MdRadioDispatcher],
template: ''
})
class TestApp {}
class TestApp {
choice: number;
}

/** Test component. */
@Component({
directives: [MdRadioButton, MdRadioGroup],
providers: [MdRadioDispatcher],
template: ''
})
class TestAppWithInitialValue {
choice: number = 1;
}
48 changes: 47 additions & 1 deletion src/components/radio/radio.ts
Expand Up @@ -10,14 +10,31 @@ import {
OnInit,
Optional,
Output,
Provider,
QueryList,
ViewEncapsulation,
forwardRef
} from 'angular2/core';

import {
NG_VALUE_ACCESSOR,
ControlValueAccessor
} from 'angular2/src/common/forms/directives/control_value_accessor';
import {CONST_EXPR} from 'angular2/src/facade/lang';

import {MdRadioDispatcher} from './radio_dispatcher';
export {MdRadioDispatcher} from './radio_dispatcher';

/**
* Provider Expression that allows md-radio-group to register as a ControlValueAccessor. This
* allows it to support [(ngModel)] and ngControl.
*/
const MD_RADIO_GROUP_CONTROL_VALUE_ACCESSOR = CONST_EXPR(new Provider(
NG_VALUE_ACCESSOR, {
useExisting: forwardRef(() => MdRadioGroup),
multi: true
}));

// TODO(mtlin):
// Ink ripple is currently placeholder.
// Determine motion spec for button transitions.
Expand All @@ -36,11 +53,12 @@ export class MdRadioChange {

@Directive({
selector: 'md-radio-group',
providers: [MD_RADIO_GROUP_CONTROL_VALUE_ACCESSOR],
host: {
'role': 'radiogroup',
},
})
export class MdRadioGroup implements AfterContentInit {
export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
/** The value for the radio group. Should match currently selected button. */
private _value: any = null;

Expand All @@ -53,6 +71,11 @@ export class MdRadioGroup implements AfterContentInit {
/** The currently selected radio button. Should match value. */
private _selected: MdRadioButton = null;

/** Change event subscription set up by registerOnChange (ControlValueAccessor). */
private _changeSubscription: {unsubscribe: () => any} = null;

onTouched: () => any = () => {};

/** Event emitted when the group value changes. */
@Output()
change: EventEmitter<MdRadioChange> = new EventEmitter();
Expand Down Expand Up @@ -155,6 +178,25 @@ export class MdRadioGroup implements AfterContentInit {

selected.checked = true;
}

/** Implemented as part of ControlValueAccessor. */
writeValue(value: any) {
this.value = value;
}

/** Implemented as part of ControlValueAccessor. */
registerOnChange(fn: any) {
if (this._changeSubscription) {
this._changeSubscription.unsubscribe();
}
this._changeSubscription = <{unsubscribe: () => any}>this.change.subscribe(
(changeEvent: MdRadioChange) => { fn(changeEvent.value); });
}

/** Implemented as part of ControlValueAccessor. */
registerOnTouched(fn: any) {
this.onTouched = fn;
}
}


Expand Down Expand Up @@ -210,6 +252,10 @@ export class MdRadioButton implements OnInit {
if (this.id == null) {
this.id = `md-radio-${_uniqueIdCounter++}`;
}

if (this.radioGroup && this._value == this.radioGroup.value) {
this._checked = true;
}
}

/*
Expand Down
12 changes: 12 additions & 0 deletions src/demo-app/radio/radio-demo.html
@@ -1,8 +1,10 @@
<h1>Basic Example</h1>
<section class="demo-section">
<md-radio-button name="group1">Option 1</md-radio-button>
<md-radio-button name="group1">Option 2</md-radio-button>
<md-radio-button name="group1" disabled="true">Option 3 (disabled)</md-radio-button>
</section>
<h1>Dynamic Example</h1>
<section class="demo-section">
<div>
<span>isDisabled: {{isDisabled}}</span>
Expand All @@ -16,3 +18,13 @@
<md-radio-button value="option_3">Option 3</md-radio-button>
</md-radio-group>
</section>
<h1>Favorite Season Example</h1>
<h2>Dynamic Example with two-way data-binding</h2>
<section class="demo-section">
<md-radio-group name="more_options" [(ngModel)]="favoriteSeason">
<md-radio-button *ngFor="#season of seasonOptions" name="more_options" [value]="season">
{{season}}
</md-radio-button>
</md-radio-group>
<p>Your favorite season is: {{favoriteSeason}}</p>
</section>
7 changes: 7 additions & 0 deletions src/demo-app/radio/radio-demo.ts
Expand Up @@ -11,4 +11,11 @@ import {MdRadioDispatcher} from '../../components/radio/radio_dispatcher';
})
export class RadioDemo {
isDisabled: boolean = false;
favoriteSeason: string = 'Autumn';
seasonOptions = [
'Winter',
'Spring',
'Summer',
'Autumn',
];
}

0 comments on commit 6aff4cc

Please sign in to comment.