From e725542703d82d1d3e47d9898022452456115747 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Fri, 5 Feb 2016 16:08:53 -0800 Subject: [PATCH] fix(forms): add support for radio buttons Closes #6877 --- modules/angular2/src/common/forms.ts | 25 +++- .../angular2/src/common/forms/directives.ts | 6 + .../directives/checkbox_value_accessor.ts | 2 +- .../radio_control_value_accessor.ts | 126 ++++++++++++++++++ .../angular2/src/common/forms/form_builder.ts | 20 +-- modules/angular2/src/common/forms/model.ts | 10 ++ .../test/common/forms/integration_spec.ts | 74 +++++++++- modules/angular2/test/public_api_spec.ts | 11 +- tools/public_api_guard/public_api_spec.ts | 3 + 9 files changed, 252 insertions(+), 25 deletions(-) create mode 100644 modules/angular2/src/common/forms/directives/radio_control_value_accessor.ts diff --git a/modules/angular2/src/common/forms.ts b/modules/angular2/src/common/forms.ts index e18ef145e8..037ba6b721 100644 --- a/modules/angular2/src/common/forms.ts +++ b/modules/angular2/src/common/forms.ts @@ -31,7 +31,7 @@ export { NgSelectOption, SelectControlValueAccessor } from './forms/directives/select_control_value_accessor'; -export {FORM_DIRECTIVES} from './forms/directives'; +export {FORM_DIRECTIVES, RadioButtonState} from './forms/directives'; export {NG_VALIDATORS, NG_ASYNC_VALIDATORS, Validators} from './forms/validators'; export { RequiredValidator, @@ -39,4 +39,25 @@ export { MaxLengthValidator, Validator } from './forms/directives/validators'; -export {FormBuilder, FORM_PROVIDERS, FORM_BINDINGS} from './forms/form_builder'; \ No newline at end of file +export {FormBuilder} from './forms/form_builder'; +import {FormBuilder} from './forms/form_builder'; +import {RadioControlRegistry} from './forms/directives/radio_control_value_accessor'; +import {Type, CONST_EXPR} from 'angular2/src/facade/lang'; + +/** + * Shorthand set of providers used for building Angular forms. + * + * ### Example + * + * ```typescript + * bootstrap(MyApp, [FORM_PROVIDERS]); + * ``` + */ +export const FORM_PROVIDERS: Type[] = CONST_EXPR([FormBuilder, RadioControlRegistry]); + +/** + * See {@link FORM_PROVIDERS} instead. + * + * @deprecated + */ +export const FORM_BINDINGS = FORM_PROVIDERS; diff --git a/modules/angular2/src/common/forms/directives.ts b/modules/angular2/src/common/forms/directives.ts index 0a3bcbbd5a..6bc968025b 100644 --- a/modules/angular2/src/common/forms/directives.ts +++ b/modules/angular2/src/common/forms/directives.ts @@ -8,6 +8,7 @@ import {NgForm} from './directives/ng_form'; import {DefaultValueAccessor} from './directives/default_value_accessor'; import {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor'; import {NumberValueAccessor} from './directives/number_value_accessor'; +import {RadioControlValueAccessor} from './directives/radio_control_value_accessor'; import {NgControlStatus} from './directives/ng_control_status'; import { SelectControlValueAccessor, @@ -23,6 +24,10 @@ export {NgFormModel} from './directives/ng_form_model'; export {NgForm} from './directives/ng_form'; export {DefaultValueAccessor} from './directives/default_value_accessor'; export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor'; +export { + RadioControlValueAccessor, + RadioButtonState +} from './directives/radio_control_value_accessor'; export {NumberValueAccessor} from './directives/number_value_accessor'; export {NgControlStatus} from './directives/ng_control_status'; export { @@ -63,6 +68,7 @@ export const FORM_DIRECTIVES: Type[] = CONST_EXPR([ NumberValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, + RadioControlValueAccessor, NgControlStatus, RequiredValidator, diff --git a/modules/angular2/src/common/forms/directives/checkbox_value_accessor.ts b/modules/angular2/src/common/forms/directives/checkbox_value_accessor.ts index 0943268392..9a8fdccb6e 100644 --- a/modules/angular2/src/common/forms/directives/checkbox_value_accessor.ts +++ b/modules/angular2/src/common/forms/directives/checkbox_value_accessor.ts @@ -18,7 +18,7 @@ const CHECKBOX_VALUE_ACCESSOR = CONST_EXPR(new Provider( selector: 'input[type=checkbox][ngControl],input[type=checkbox][ngFormControl],input[type=checkbox][ngModel]', host: {'(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()'}, - bindings: [CHECKBOX_VALUE_ACCESSOR] + providers: [CHECKBOX_VALUE_ACCESSOR] }) export class CheckboxControlValueAccessor implements ControlValueAccessor { onChange = (_) => {}; diff --git a/modules/angular2/src/common/forms/directives/radio_control_value_accessor.ts b/modules/angular2/src/common/forms/directives/radio_control_value_accessor.ts new file mode 100644 index 0000000000..b7ab63d546 --- /dev/null +++ b/modules/angular2/src/common/forms/directives/radio_control_value_accessor.ts @@ -0,0 +1,126 @@ +import { + Directive, + ElementRef, + Renderer, + Self, + forwardRef, + Provider, + Attribute, + Input, + OnInit, + OnDestroy, + Injector, + Injectable +} from 'angular2/core'; +import { + NG_VALUE_ACCESSOR, + ControlValueAccessor +} from 'angular2/src/common/forms/directives/control_value_accessor'; +import {NgControl} from 'angular2/src/common/forms/directives/ng_control'; +import {CONST_EXPR, looseIdentical, isPresent} from 'angular2/src/facade/lang'; +import {ListWrapper} from 'angular2/src/facade/collection'; + +const RADIO_VALUE_ACCESSOR = CONST_EXPR(new Provider( + NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => RadioControlValueAccessor), multi: true})); + + +/** + * Internal class used by Angular to uncheck radio buttons with the matching name. + */ +@Injectable() +export class RadioControlRegistry { + private _accessors: any[] = []; + + add(control: NgControl, accessor: RadioControlValueAccessor) { + this._accessors.push([control, accessor]); + } + + remove(accessor: RadioControlValueAccessor) { + var indexToRemove = -1; + for (var i = 0; i < this._accessors.length; ++i) { + if (this._accessors[i][1] === accessor) { + indexToRemove = i; + } + } + ListWrapper.removeAt(this._accessors, indexToRemove); + } + + select(accessor: RadioControlValueAccessor) { + this._accessors.forEach((c) => { + if (c[0].control.root === accessor._control.control.root && c[1] !== accessor) { + c[1].fireUncheck(); + } + }); + } +} + +/** + * The value provided by the forms API for radio buttons. + */ +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 NgFormControl}, and {@link NgControlName} directives. + * + * ### Example + * ``` + * @Component({ + * template: ` + * + * + * ` + * }) + * class FoodCmp { + * foodChicken = new RadioButtonState(true, "chicken"); + * foodFish = new RadioButtonState(false, "fish"); + * } + * ``` + */ +@Directive({ + selector: + 'input[type=radio][ngControl],input[type=radio][ngFormControl],input[type=radio][ngModel]', + host: {'(change)': 'onChange()', '(blur)': 'onTouched()'}, + providers: [RADIO_VALUE_ACCESSOR] +}) +export class RadioControlValueAccessor implements ControlValueAccessor, + OnDestroy, OnInit { + _state: RadioButtonState; + _control: NgControl; + @Input() name: string; + _fn: Function; + onChange = () => {}; + onTouched = () => {}; + + constructor(private _renderer: Renderer, private _elementRef: ElementRef, + private _registry: RadioControlRegistry, private _injector: Injector) {} + + ngOnInit(): void { + this._control = this._injector.get(NgControl); + this._registry.add(this._control, this); + } + + 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); + } + } + + registerOnChange(fn: (_: any) => {}): void { + this._fn = fn; + this.onChange = () => { + fn(new RadioButtonState(true, this._state.value)); + this._registry.select(this); + }; + } + + fireUncheck(): void { this._fn(new RadioButtonState(false, this._state.value)); } + + registerOnTouched(fn: () => {}): void { this.onTouched = fn; } +} diff --git a/modules/angular2/src/common/forms/form_builder.ts b/modules/angular2/src/common/forms/form_builder.ts index b6ef3fdc7c..5c87b04841 100644 --- a/modules/angular2/src/common/forms/form_builder.ts +++ b/modules/angular2/src/common/forms/form_builder.ts @@ -105,22 +105,4 @@ export class FormBuilder { return this.control(controlConfig); } } -} - -/** - * Shorthand set of providers used for building Angular forms. - * - * ### Example - * - * ```typescript - * bootstrap(MyApp, [FORM_PROVIDERS]); - * ``` - */ -export const FORM_PROVIDERS: Type[] = CONST_EXPR([FormBuilder]); - -/** - * See {@link FORM_PROVIDERS} instead. - * - * @deprecated - */ -export const FORM_BINDINGS = FORM_PROVIDERS; +} \ No newline at end of file diff --git a/modules/angular2/src/common/forms/model.ts b/modules/angular2/src/common/forms/model.ts index 59b735bed7..1d5c6506b8 100644 --- a/modules/angular2/src/common/forms/model.ts +++ b/modules/angular2/src/common/forms/model.ts @@ -208,6 +208,16 @@ export abstract class AbstractControl { return isPresent(this.getError(errorCode, path)); } + get root(): AbstractControl { + let x: AbstractControl = this; + + while (isPresent(x._parent)) { + x = x._parent; + } + + return x; + } + /** @internal */ _updateControlsErrors(): void { this._status = this._calculateStatus(); diff --git a/modules/angular2/test/common/forms/integration_spec.ts b/modules/angular2/test/common/forms/integration_spec.ts index 252bb93296..6907e63391 100644 --- a/modules/angular2/test/common/forms/integration_spec.ts +++ b/modules/angular2/test/common/forms/integration_spec.ts @@ -10,6 +10,7 @@ import { dispatchEvent, fakeAsync, tick, + flushMicrotasks, expect, it, inject, @@ -31,7 +32,8 @@ import { NgFor, NgForm, Validators, - Validator + Validator, + RadioButtonState } from 'angular2/common'; import {Provider, forwardRef, Input} from 'angular2/core'; import {By} from 'angular2/platform/browser'; @@ -328,6 +330,33 @@ export function main() { }); })); + it("should support ", + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + var t = `
+ + +
`; + + tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((fixture) => { + fixture.debugElement.componentInstance.form = new ControlGroup({ + "foodChicken": new Control(new RadioButtonState(false, 'chicken')), + "foodFish": new Control(new RadioButtonState(true, 'fish')) + }); + fixture.detectChanges(); + + var input = fixture.debugElement.query(By.css("input")); + expect(input.nativeElement.checked).toEqual(false); + + dispatchEvent(input.nativeElement, "change"); + fixture.detectChanges(); + + let value = fixture.debugElement.componentInstance.form.value; + expect(value['foodChicken'].checked).toEqual(true); + expect(value['foodFish'].checked).toEqual(false); + async.done(); + }); + })); + it("should support + + + +
+ + +
`; + + var fixture: ComponentFixture; + tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((f) => { fixture = f; }); + tick(); + + fixture.debugElement.componentInstance.data = { + 'chicken1': new RadioButtonState(false, 'chicken'), + 'fish1': new RadioButtonState(true, 'fish'), + + 'chicken2': new RadioButtonState(false, 'chicken'), + 'fish2': new RadioButtonState(true, 'fish') + }; + fixture.detectChanges(); + tick(); + + var input = fixture.debugElement.query(By.css("input")); + expect(input.nativeElement.checked).toEqual(false); + + dispatchEvent(input.nativeElement, "change"); + tick(); + + let data = fixture.debugElement.componentInstance.data; + + expect(data['chicken1']).toEqual(new RadioButtonState(true, 'chicken')); + expect(data['fish1']).toEqual(new RadioButtonState(false, 'fish')); + + expect(data['chicken2']).toEqual(new RadioButtonState(false, 'chicken')); + expect(data['fish2']).toEqual(new RadioButtonState(true, 'fish')); + }))); + }); + describe("setting status classes", () => { it("should work with single fields", inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 38c4bdc5ad..9f082bb2b7 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -51,6 +51,7 @@ var NG_COMMON = [ 'AbstractControl.validator', 'AbstractControl.validator=', 'AbstractControl.value', + 'AbstractControl.root', 'AbstractControl.valueChanges', 'AbstractControlDirective', 'AbstractControlDirective.control', @@ -102,6 +103,7 @@ var NG_COMMON = [ 'Control.validator', 'Control.validator=', 'Control.value', + 'Control.root', 'Control.valueChanges', 'ControlArray', 'ControlArray.asyncValidator', @@ -134,6 +136,7 @@ var NG_COMMON = [ 'ControlArray.validator', 'ControlArray.validator=', 'ControlArray.value', + 'ControlArray.root', 'ControlArray.valueChanges', 'ControlContainer', 'ControlContainer.control', @@ -179,6 +182,7 @@ var NG_COMMON = [ 'ControlGroup.validator', 'ControlGroup.validator=', 'ControlGroup.value', + 'ControlGroup.root', 'ControlGroup.valueChanges', 'ControlValueAccessor:dart', 'CurrencyPipe', @@ -447,7 +451,12 @@ var NG_COMMON = [ 'Validators#maxLength()', 'Validators#minLength()', 'Validators#nullValidator()', - 'Validators#required()' + 'Validators#required()', + 'RadioButtonState', + 'RadioButtonState.checked', + 'RadioButtonState.checked=', + 'RadioButtonState.value', + 'RadioButtonState.value=' ]; var NG_COMPILER = [ diff --git a/tools/public_api_guard/public_api_spec.ts b/tools/public_api_guard/public_api_spec.ts index bea8d61740..fbd6a20ac3 100644 --- a/tools/public_api_guard/public_api_spec.ts +++ b/tools/public_api_guard/public_api_spec.ts @@ -566,6 +566,7 @@ const COMMON = [ 'AbstractControl.updateValueAndValidity({onlySelf,emitEvent}:{onlySelf?:boolean, emitEvent?:boolean}):void', 'AbstractControl.valid:boolean', 'AbstractControl.value:any', + 'AbstractControl.root:AbstractControl', 'AbstractControl.valueChanges:Observable', 'AbstractControlDirective', 'AbstractControlDirective.control:AbstractControl', @@ -794,6 +795,8 @@ const COMMON = [ 'Validators.minLength(minLength:number):Function', 'Validators.nullValidator(c:any):{[key:string]:boolean}', 'Validators.required(control:Control):{[key:string]:boolean}', + 'RadioButtonState', + 'RadioButtonState.constructor(checked:boolean, value:string)', 'const COMMON_DIRECTIVES:Type[][]', 'const COMMON_PIPES:any', 'const CORE_DIRECTIVES:Type[]',