From 6aff4cce7153c166783ea070b51ed220ed7f444e Mon Sep 17 00:00:00 2001 From: Rusty Nelson Date: Sat, 19 Mar 2016 18:51:47 -0400 Subject: [PATCH] feat(radio): support ngModel on md-radio-group Closes #209 --- src/components/radio/README.md | 9 ++++ src/components/radio/radio.spec.ts | 76 +++++++++++++++++++++++++++++- src/components/radio/radio.ts | 48 ++++++++++++++++++- src/demo-app/radio/radio-demo.html | 12 +++++ src/demo-app/radio/radio-demo.ts | 7 +++ 5 files changed, 150 insertions(+), 2 deletions(-) diff --git a/src/components/radio/README.md b/src/components/radio/README.md index 8a598bc8fb95..4268b61fec09 100644 --- a/src/components/radio/README.md +++ b/src/components/radio/README.md @@ -21,6 +21,15 @@ A dynamic example, populated from a `data` variable: ``` +A dynamic example for use inside a form showing support for `[(ngModel)]`: +```html + + + {{o.label}} + + +``` + ## `` ### Properties diff --git a/src/components/radio/radio.spec.ts b/src/components/radio/radio.spec.ts index b26f3954f884..3296c5ea5fbb 100644 --- a/src/components/radio/radio.spec.ts +++ b/src/components/radio/radio.spec.ts @@ -258,6 +258,68 @@ export function main() { }); }).then(done); }); + + it('should bind value to model without initial value', (done: () => void) => { + builder + .overrideTemplate(TestApp, ` + + + + `) + .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, ` + + + + `) + .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); + }); + }); } @@ -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; +} diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts index c04f2b98a713..1cbf1b43d7a7 100644 --- a/src/components/radio/radio.ts +++ b/src/components/radio/radio.ts @@ -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. @@ -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; @@ -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 = new EventEmitter(); @@ -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; + } } @@ -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; + } } /* diff --git a/src/demo-app/radio/radio-demo.html b/src/demo-app/radio/radio-demo.html index 8c203a405dc7..c3d0cb20dc83 100644 --- a/src/demo-app/radio/radio-demo.html +++ b/src/demo-app/radio/radio-demo.html @@ -1,8 +1,10 @@ +

Basic Example

Option 1 Option 2 Option 3 (disabled)
+

Dynamic Example

isDisabled: {{isDisabled}} @@ -16,3 +18,13 @@ Option 3
+

Favorite Season Example

+

Dynamic Example with two-way data-binding

+
+ + + {{season}} + + +

Your favorite season is: {{favoriteSeason}}

+
diff --git a/src/demo-app/radio/radio-demo.ts b/src/demo-app/radio/radio-demo.ts index ecccd4471de4..3eaa7ee911ce 100644 --- a/src/demo-app/radio/radio-demo.ts +++ b/src/demo-app/radio/radio-demo.ts @@ -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', + ]; }