Skip to content

Commit 6aff4cc

Browse files
doctorrustynelsonjelbourn
authored andcommitted
feat(radio): support ngModel on md-radio-group
Closes #209
1 parent 0b31d25 commit 6aff4cc

File tree

5 files changed

+150
-2
lines changed

5 files changed

+150
-2
lines changed

src/components/radio/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ A dynamic example, populated from a `data` variable:
2121
</md-radio-group>
2222
```
2323

24+
A dynamic example for use inside a form showing support for `[(ngModel)]`:
25+
```html
26+
<md-radio-group [(ngModel)]="chosenOption">
27+
<md-radio-button *ngFor="#o of options" [value]="o.value">
28+
{{o.label}}
29+
</md-radio-button>
30+
</md-radio-group>
31+
```
32+
2433
## `<md-radio-group>`
2534
### Properties
2635

src/components/radio/radio.spec.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,68 @@ export function main() {
258258
});
259259
}).then(done);
260260
});
261+
262+
it('should bind value to model without initial value', (done: () => void) => {
263+
builder
264+
.overrideTemplate(TestApp, `
265+
<md-radio-group [(ngModel)]="choice">
266+
<md-radio-button [value]="0"></md-radio-button>
267+
<md-radio-button [value]="1"></md-radio-button>
268+
</md-radio-group>`)
269+
.createAsync(TestApp)
270+
.then((fixture) => {
271+
fakeAsync(function() {
272+
let buttons = fixture.debugElement.queryAll(By.css('md-radio-button'));
273+
let group = fixture.debugElement.query(By.css('md-radio-group'));
274+
275+
fixture.detectChanges();
276+
expect(buttons[0].componentInstance.checked).toBe(false);
277+
expect(buttons[1].componentInstance.checked).toBe(false);
278+
expect(fixture.componentInstance.choice).toBe(undefined);
279+
280+
group.componentInstance.selected = buttons[0].componentInstance;
281+
fixture.detectChanges();
282+
expect(isSinglySelected(buttons[0], buttons)).toBe(true);
283+
expect(fixture.componentInstance.choice).toBe(0);
284+
285+
group.componentInstance.selected = buttons[1].componentInstance;
286+
fixture.detectChanges();
287+
expect(isSinglySelected(buttons[1], buttons)).toBe(true);
288+
expect(fixture.componentInstance.choice).toBe(1);
289+
});
290+
}).then(done);
291+
});
292+
293+
it('should bind value to model with initial value', (done: () => void) => {
294+
builder
295+
.overrideTemplate(TestAppWithInitialValue, `
296+
<md-radio-group [(ngModel)]="choice">
297+
<md-radio-button [value]="0"></md-radio-button>
298+
<md-radio-button [value]="1"></md-radio-button>
299+
</md-radio-group>`)
300+
.createAsync(TestAppWithInitialValue)
301+
.then((fixture) => {
302+
fakeAsync(function() {
303+
let buttons = fixture.debugElement.queryAll(By.css('md-radio-button'));
304+
let group = fixture.debugElement.query(By.css('md-radio-group'));
305+
306+
fixture.detectChanges();
307+
expect(isSinglySelected(buttons[1], buttons)).toBe(true);
308+
expect(fixture.componentInstance.choice).toBe(1);
309+
310+
group.componentInstance.selected = buttons[0].componentInstance;
311+
fixture.detectChanges();
312+
expect(isSinglySelected(buttons[0], buttons)).toBe(true);
313+
expect(fixture.componentInstance.choice).toBe(0);
314+
315+
group.componentInstance.selected = buttons[1].componentInstance;
316+
fixture.detectChanges();
317+
expect(isSinglySelected(buttons[1], buttons)).toBe(true);
318+
expect(fixture.componentInstance.choice).toBe(1);
319+
});
320+
}).then(done);
321+
});
322+
261323
});
262324
}
263325

@@ -289,4 +351,16 @@ function createEvent(name: string): Event {
289351
providers: [MdRadioDispatcher],
290352
template: ''
291353
})
292-
class TestApp {}
354+
class TestApp {
355+
choice: number;
356+
}
357+
358+
/** Test component. */
359+
@Component({
360+
directives: [MdRadioButton, MdRadioGroup],
361+
providers: [MdRadioDispatcher],
362+
template: ''
363+
})
364+
class TestAppWithInitialValue {
365+
choice: number = 1;
366+
}

src/components/radio/radio.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,31 @@ import {
1010
OnInit,
1111
Optional,
1212
Output,
13+
Provider,
1314
QueryList,
1415
ViewEncapsulation,
1516
forwardRef
1617
} from 'angular2/core';
1718

19+
import {
20+
NG_VALUE_ACCESSOR,
21+
ControlValueAccessor
22+
} from 'angular2/src/common/forms/directives/control_value_accessor';
23+
import {CONST_EXPR} from 'angular2/src/facade/lang';
24+
1825
import {MdRadioDispatcher} from './radio_dispatcher';
1926
export {MdRadioDispatcher} from './radio_dispatcher';
2027

28+
/**
29+
* Provider Expression that allows md-radio-group to register as a ControlValueAccessor. This
30+
* allows it to support [(ngModel)] and ngControl.
31+
*/
32+
const MD_RADIO_GROUP_CONTROL_VALUE_ACCESSOR = CONST_EXPR(new Provider(
33+
NG_VALUE_ACCESSOR, {
34+
useExisting: forwardRef(() => MdRadioGroup),
35+
multi: true
36+
}));
37+
2138
// TODO(mtlin):
2239
// Ink ripple is currently placeholder.
2340
// Determine motion spec for button transitions.
@@ -36,11 +53,12 @@ export class MdRadioChange {
3653

3754
@Directive({
3855
selector: 'md-radio-group',
56+
providers: [MD_RADIO_GROUP_CONTROL_VALUE_ACCESSOR],
3957
host: {
4058
'role': 'radiogroup',
4159
},
4260
})
43-
export class MdRadioGroup implements AfterContentInit {
61+
export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
4462
/** The value for the radio group. Should match currently selected button. */
4563
private _value: any = null;
4664

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

74+
/** Change event subscription set up by registerOnChange (ControlValueAccessor). */
75+
private _changeSubscription: {unsubscribe: () => any} = null;
76+
77+
onTouched: () => any = () => {};
78+
5679
/** Event emitted when the group value changes. */
5780
@Output()
5881
change: EventEmitter<MdRadioChange> = new EventEmitter();
@@ -155,6 +178,25 @@ export class MdRadioGroup implements AfterContentInit {
155178

156179
selected.checked = true;
157180
}
181+
182+
/** Implemented as part of ControlValueAccessor. */
183+
writeValue(value: any) {
184+
this.value = value;
185+
}
186+
187+
/** Implemented as part of ControlValueAccessor. */
188+
registerOnChange(fn: any) {
189+
if (this._changeSubscription) {
190+
this._changeSubscription.unsubscribe();
191+
}
192+
this._changeSubscription = <{unsubscribe: () => any}>this.change.subscribe(
193+
(changeEvent: MdRadioChange) => { fn(changeEvent.value); });
194+
}
195+
196+
/** Implemented as part of ControlValueAccessor. */
197+
registerOnTouched(fn: any) {
198+
this.onTouched = fn;
199+
}
158200
}
159201

160202

@@ -210,6 +252,10 @@ export class MdRadioButton implements OnInit {
210252
if (this.id == null) {
211253
this.id = `md-radio-${_uniqueIdCounter++}`;
212254
}
255+
256+
if (this.radioGroup && this._value == this.radioGroup.value) {
257+
this._checked = true;
258+
}
213259
}
214260

215261
/*

src/demo-app/radio/radio-demo.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
<h1>Basic Example</h1>
12
<section class="demo-section">
23
<md-radio-button name="group1">Option 1</md-radio-button>
34
<md-radio-button name="group1">Option 2</md-radio-button>
45
<md-radio-button name="group1" disabled="true">Option 3 (disabled)</md-radio-button>
56
</section>
7+
<h1>Dynamic Example</h1>
68
<section class="demo-section">
79
<div>
810
<span>isDisabled: {{isDisabled}}</span>
@@ -16,3 +18,13 @@
1618
<md-radio-button value="option_3">Option 3</md-radio-button>
1719
</md-radio-group>
1820
</section>
21+
<h1>Favorite Season Example</h1>
22+
<h2>Dynamic Example with two-way data-binding</h2>
23+
<section class="demo-section">
24+
<md-radio-group name="more_options" [(ngModel)]="favoriteSeason">
25+
<md-radio-button *ngFor="#season of seasonOptions" name="more_options" [value]="season">
26+
{{season}}
27+
</md-radio-button>
28+
</md-radio-group>
29+
<p>Your favorite season is: {{favoriteSeason}}</p>
30+
</section>

src/demo-app/radio/radio-demo.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,11 @@ import {MdRadioDispatcher} from '../../components/radio/radio_dispatcher';
1111
})
1212
export class RadioDemo {
1313
isDisabled: boolean = false;
14+
favoriteSeason: string = 'Autumn';
15+
seasonOptions = [
16+
'Winter',
17+
'Spring',
18+
'Summer',
19+
'Autumn',
20+
];
1421
}

0 commit comments

Comments
 (0)