From 39e0b4903cd9a22237ea2b15c1f1396730f48b2f Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Wed, 15 Jun 2016 15:15:41 -0700 Subject: [PATCH] feat(radio): support radio button sharing a control --- modules/@angular/forms/src/directives.ts | 2 +- .../@angular/forms/src/directives/ng_form.ts | 14 +- .../radio_control_value_accessor.ts | 37 ++--- modules/@angular/forms/src/forms.ts | 2 +- modules/@angular/forms/src/model.ts | 12 +- .../@angular/forms/test/integration_spec.ts | 147 +++++++++--------- 6 files changed, 105 insertions(+), 109 deletions(-) diff --git a/modules/@angular/forms/src/directives.ts b/modules/@angular/forms/src/directives.ts index b8b4550817..3e5907d4e9 100644 --- a/modules/@angular/forms/src/directives.ts +++ b/modules/@angular/forms/src/directives.ts @@ -25,7 +25,7 @@ export {NgForm} from './directives/ng_form'; export {NgModel} from './directives/ng_model'; export {NgModelGroup} from './directives/ng_model_group'; export {NumberValueAccessor} from './directives/number_value_accessor'; -export {RadioButtonState, RadioControlValueAccessor} from './directives/radio_control_value_accessor'; +export {RadioControlValueAccessor} from './directives/radio_control_value_accessor'; export {FormControlDirective} from './directives/reactive_directives/form_control_directive'; export {FormControlName} from './directives/reactive_directives/form_control_name'; export {FormGroupDirective} from './directives/reactive_directives/form_group_directive'; diff --git a/modules/@angular/forms/src/directives/ng_form.ts b/modules/@angular/forms/src/directives/ng_form.ts index 37c40d3b22..84bd2f664d 100644 --- a/modules/@angular/forms/src/directives/ng_form.ts +++ b/modules/@angular/forms/src/directives/ng_form.ts @@ -9,6 +9,7 @@ import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators'; import {ControlContainer} from './control_container'; import {Form} from './form_interface'; import {NgControl} from './ng_control'; +import {NgModel} from './ng_model'; import {NgModelGroup} from './ng_model_group'; import {composeAsyncValidators, composeValidators, setUpControl, setUpFormGroup} from './shared'; @@ -107,20 +108,21 @@ export class NgForm extends ControlContainer implements Form { get controls(): {[key: string]: AbstractControl} { return this.form.controls; } - addControl(dir: NgControl): FormControl { + addControl(dir: NgModel): FormControl { const ctrl = new FormControl(); PromiseWrapper.scheduleMicrotask(() => { const container = this._findContainer(dir.path); - setUpControl(ctrl, dir); - container.registerControl(dir.name, ctrl); - ctrl.updateValueAndValidity({emitEvent: false}); + dir._control = container.registerControl(dir.name, ctrl); + setUpControl(dir.control, dir); + dir.control.updateValueAndValidity({emitEvent: false}); }); + return ctrl; } - getControl(dir: NgControl): FormControl { return this.form.find(dir.path); } + getControl(dir: NgModel): FormControl { return this.form.find(dir.path); } - removeControl(dir: NgControl): void { + removeControl(dir: NgModel): void { PromiseWrapper.scheduleMicrotask(() => { var container = this._findContainer(dir.path); if (isPresent(container)) { diff --git a/modules/@angular/forms/src/directives/radio_control_value_accessor.ts b/modules/@angular/forms/src/directives/radio_control_value_accessor.ts index a62396789d..571068bfbd 100644 --- a/modules/@angular/forms/src/directives/radio_control_value_accessor.ts +++ b/modules/@angular/forms/src/directives/radio_control_value_accessor.ts @@ -36,7 +36,7 @@ export class RadioControlRegistry { select(accessor: RadioControlValueAccessor) { this._accessors.forEach((c) => { if (this._isSameGroup(c, accessor) && c[1] !== accessor) { - c[1].fireUncheck(); + c[1].fireUncheck(accessor.value); } }); } @@ -48,16 +48,6 @@ export class RadioControlRegistry { } } -/** - * The value provided by the forms API for radio buttons. - * - * @experimental - */ -export class RadioButtonState { - constructor(public checked: boolean, public value: string) {} -} - - /** * The accessor for writing a radio control value and listening to changes that is used by the * {@link NgModel}, {@link FormControlDirective}, and {@link FormControlName} directives. @@ -66,13 +56,12 @@ export class RadioButtonState { * ``` * @Component({ * template: ` - * - * + * + * * ` * }) * class FoodCmp { - * foodChicken = new RadioButtonState(true, "chicken"); - * foodFish = new RadioButtonState(false, "fish"); + * food = 'chicken'; * } * ``` */ @@ -85,14 +74,16 @@ export class RadioButtonState { export class RadioControlValueAccessor implements ControlValueAccessor, OnDestroy, OnInit { /** @internal */ - _state: RadioButtonState; + _state: boolean; /** @internal */ _control: NgControl; - @Input() name: string; /** @internal */ _fn: Function; onChange = () => {}; - onTouched = () => {}; + onTouched = () => {} + + @Input() name: string; + @Input() value: any; constructor( private _renderer: Renderer, private _elementRef: ElementRef, @@ -106,21 +97,21 @@ export class RadioControlValueAccessor implements ControlValueAccessor, ngOnDestroy(): void { this._registry.remove(this); } writeValue(value: any): void { - this._state = value; - if (isPresent(value) && value.checked) { - this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', true); + this._state = value === this.value; + if (isPresent(value)) { + this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', this._state); } } registerOnChange(fn: (_: any) => {}): void { this._fn = fn; this.onChange = () => { - fn(new RadioButtonState(true, this._state.value)); + fn(this.value); this._registry.select(this); }; } - fireUncheck(): void { this._fn(new RadioButtonState(false, this._state.value)); } + fireUncheck(value: any): void { this.writeValue(value); } registerOnTouched(fn: () => {}): void { this.onTouched = fn; } } diff --git a/modules/@angular/forms/src/forms.ts b/modules/@angular/forms/src/forms.ts index 8ba67ef723..5d22405871 100644 --- a/modules/@angular/forms/src/forms.ts +++ b/modules/@angular/forms/src/forms.ts @@ -13,7 +13,7 @@ */ -export {FORM_DIRECTIVES, REACTIVE_FORM_DIRECTIVES, RadioButtonState} from './directives'; +export {FORM_DIRECTIVES, REACTIVE_FORM_DIRECTIVES} from './directives'; export {AbstractControlDirective} from './directives/abstract_control_directive'; export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor'; export {ControlContainer} from './directives/control_container'; diff --git a/modules/@angular/forms/src/model.ts b/modules/@angular/forms/src/model.ts index df183d6fdb..79118adacd 100644 --- a/modules/@angular/forms/src/model.ts +++ b/modules/@angular/forms/src/model.ts @@ -282,7 +282,7 @@ export abstract class AbstractControl { */ export class FormControl extends AbstractControl { /** @internal */ - _onChange: Function; + _onChange: Function[] = []; constructor( value: any = null, validator: ValidatorFn|ValidatorFn[] = null, @@ -312,7 +312,9 @@ export class FormControl extends AbstractControl { } = {}): void { emitModelToViewChange = isPresent(emitModelToViewChange) ? emitModelToViewChange : true; this._value = value; - if (isPresent(this._onChange) && emitModelToViewChange) this._onChange(this._value); + if (this._onChange.length && emitModelToViewChange) { + this._onChange.forEach((changeFn) => changeFn(this._value)); + } this.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent}); } @@ -329,7 +331,7 @@ export class FormControl extends AbstractControl { /** * Register a listener for change events. */ - registerOnChange(fn: Function): void { this._onChange = fn; } + registerOnChange(fn: Function): void { this._onChange.push(fn); } } /** @@ -364,9 +366,11 @@ export class FormGroup extends AbstractControl { /** * Register a control with the group's list of controls. */ - registerControl(name: string, control: AbstractControl): void { + registerControl(name: string, control: AbstractControl): AbstractControl { + if (this.controls[name]) return this.controls[name]; this.controls[name] = control; control.setParent(this); + return control; } /** diff --git a/modules/@angular/forms/test/integration_spec.ts b/modules/@angular/forms/test/integration_spec.ts index 48912d3dc2..2c0a233097 100644 --- a/modules/@angular/forms/test/integration_spec.ts +++ b/modules/@angular/forms/test/integration_spec.ts @@ -6,7 +6,7 @@ import {Input, Provider, forwardRef} from '@angular/core'; import {fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {afterEach, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; import {AsyncTestCompleter} from '@angular/core/testing/testing_internal'; -import {ControlValueAccessor, FORM_DIRECTIVES, FORM_PROVIDERS, FormControl, FormGroup, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NgControl, NgForm, NgModel, REACTIVE_FORM_DIRECTIVES, RadioButtonState, Validator, Validators, disableDeprecatedForms, provideForms} from '@angular/forms'; +import {ControlValueAccessor, FORM_DIRECTIVES, FORM_PROVIDERS, FormControl, FormGroup, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NgControl, NgForm, NgModel, REACTIVE_FORM_DIRECTIVES, Validator, Validators, disableDeprecatedForms, provideForms} from '@angular/forms'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {dispatchEvent} from '@angular/platform-browser/testing'; @@ -503,29 +503,35 @@ export function main() { [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { const t = `
- - + +
`; + const ctrl = new FormControl('fish'); tcb.overrideTemplate(MyComp8, t) .overrideProviders(MyComp8, providerArr) .createAsync(MyComp8) .then((fixture) => { - fixture.debugElement.componentInstance.form = new FormGroup({ - 'foodChicken': new FormControl(new RadioButtonState(false, 'chicken')), - 'foodFish': new FormControl(new RadioButtonState(true, 'fish')) - }); + fixture.debugElement.componentInstance.form = new FormGroup({'food': ctrl}); fixture.detectChanges(); - var input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.checked).toEqual(false); + var inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); - dispatchEvent(input.nativeElement, 'change'); + dispatchEvent(inputs[0].nativeElement, 'change'); fixture.detectChanges(); let value = fixture.debugElement.componentInstance.form.value; - expect(value['foodChicken'].checked).toEqual(true); - expect(value['foodFish'].checked).toEqual(false); + expect(value.food).toEqual('chicken'); + expect(inputs[1].nativeElement.checked).toEqual(false); + + ctrl.updateValue('fish'); + fixture.detectChanges(); + + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); + async.done(); }); })); @@ -1393,77 +1399,70 @@ export function main() { expect(form.value).toEqual({two: 'some data'}); }))); - // TODO(kara): Fix when re-doing radio buttons - xit('should support ', - fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - const t = `
- - - - + it('should support ', + fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { + const t = ` + + `; - const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8); - tick(); + const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8); + tick(); - fixture.debugElement.componentInstance.data = { - 'chicken': new RadioButtonState(false, 'chicken'), - 'fish': new RadioButtonState(true, 'fish'), - 'beef': new RadioButtonState(false, 'beef'), - 'pork': new RadioButtonState(true, 'pork') - }; - fixture.detectChanges(); - tick(); + fixture.debugElement.componentInstance.data = {food: 'fish'}; + fixture.detectChanges(); + tick(); - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.checked).toEqual(false); + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); - dispatchEvent(input.nativeElement, 'change'); - tick(); + dispatchEvent(inputs[0].nativeElement, 'change'); + tick(); - const data = fixture.debugElement.componentInstance.data; + const data = fixture.debugElement.componentInstance.data; + + expect(data.food).toEqual('chicken'); + expect(inputs[1].nativeElement.checked).toEqual(false); + }))); + + it('should support multiple named groups', + fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { + const t = `
+ + + + +
`; + + const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8); + tick(); + + fixture.debugElement.componentInstance.data = {food: 'fish', drink: 'sprite'}; + fixture.detectChanges(); + tick(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(true); + + dispatchEvent(inputs[0].nativeElement, 'change'); + tick(); + + const data = fixture.debugElement.componentInstance.data; + + expect(data.food).toEqual('chicken'); + expect(data.drink).toEqual('sprite'); + expect(inputs[1].nativeElement.checked).toEqual(false); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(true); + + }))); - expect(data['chicken']).toEqual(new RadioButtonState(true, 'chicken')); - expect(data['fish']).toEqual(new RadioButtonState(false, 'fish')); - expect(data['beef']).toEqual(new RadioButtonState(false, 'beef')); - expect(data['pork']).toEqual(new RadioButtonState(false, 'pork')); - }))); }); - xit('should support multiple named groups', - fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - const t = `
- - - - -
`; - - const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8); - tick(); - - fixture.debugElement.componentInstance.data = { - 'chicken': new RadioButtonState(false, 'chicken'), - 'fish': new RadioButtonState(true, 'fish'), - 'cola': new RadioButtonState(false, 'cola'), - 'sprite': new RadioButtonState(true, 'sprite') - }; - fixture.detectChanges(); - tick(); - - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.checked).toEqual(false); - - dispatchEvent(input.nativeElement, 'change'); - tick(); - - const data = fixture.debugElement.componentInstance.data; - - expect(data['chicken']).toEqual(new RadioButtonState(true, 'chicken')); - expect(data['fish']).toEqual(new RadioButtonState(false, 'fish')); - expect(data['cola']).toEqual(new RadioButtonState(false, 'cola')); - expect(data['sprite']).toEqual(new RadioButtonState(true, 'sprite')); - }))); describe('setting status classes', () => { it('should work with single fields',