feat(radio): support radio button sharing a control

This commit is contained in:
Kara Erickson 2016-06-15 15:15:41 -07:00
parent 54c577cfe0
commit 39e0b4903c
6 changed files with 105 additions and 109 deletions

View File

@ -25,7 +25,7 @@ export {NgForm} from './directives/ng_form';
export {NgModel} from './directives/ng_model'; export {NgModel} from './directives/ng_model';
export {NgModelGroup} from './directives/ng_model_group'; export {NgModelGroup} from './directives/ng_model_group';
export {NumberValueAccessor} from './directives/number_value_accessor'; 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 {FormControlDirective} from './directives/reactive_directives/form_control_directive';
export {FormControlName} from './directives/reactive_directives/form_control_name'; export {FormControlName} from './directives/reactive_directives/form_control_name';
export {FormGroupDirective} from './directives/reactive_directives/form_group_directive'; export {FormGroupDirective} from './directives/reactive_directives/form_group_directive';

View File

@ -9,6 +9,7 @@ import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {ControlContainer} from './control_container'; import {ControlContainer} from './control_container';
import {Form} from './form_interface'; import {Form} from './form_interface';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
import {NgModel} from './ng_model';
import {NgModelGroup} from './ng_model_group'; import {NgModelGroup} from './ng_model_group';
import {composeAsyncValidators, composeValidators, setUpControl, setUpFormGroup} from './shared'; 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; } get controls(): {[key: string]: AbstractControl} { return this.form.controls; }
addControl(dir: NgControl): FormControl { addControl(dir: NgModel): FormControl {
const ctrl = new FormControl(); const ctrl = new FormControl();
PromiseWrapper.scheduleMicrotask(() => { PromiseWrapper.scheduleMicrotask(() => {
const container = this._findContainer(dir.path); const container = this._findContainer(dir.path);
setUpControl(ctrl, dir); dir._control = <FormControl>container.registerControl(dir.name, ctrl);
container.registerControl(dir.name, ctrl); setUpControl(dir.control, dir);
ctrl.updateValueAndValidity({emitEvent: false}); dir.control.updateValueAndValidity({emitEvent: false});
}); });
return ctrl; return ctrl;
} }
getControl(dir: NgControl): FormControl { return <FormControl>this.form.find(dir.path); } getControl(dir: NgModel): FormControl { return <FormControl>this.form.find(dir.path); }
removeControl(dir: NgControl): void { removeControl(dir: NgModel): void {
PromiseWrapper.scheduleMicrotask(() => { PromiseWrapper.scheduleMicrotask(() => {
var container = this._findContainer(dir.path); var container = this._findContainer(dir.path);
if (isPresent(container)) { if (isPresent(container)) {

View File

@ -36,7 +36,7 @@ export class RadioControlRegistry {
select(accessor: RadioControlValueAccessor) { select(accessor: RadioControlValueAccessor) {
this._accessors.forEach((c) => { this._accessors.forEach((c) => {
if (this._isSameGroup(c, accessor) && c[1] !== accessor) { 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 * 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. * {@link NgModel}, {@link FormControlDirective}, and {@link FormControlName} directives.
@ -66,13 +56,12 @@ export class RadioButtonState {
* ``` * ```
* @Component({ * @Component({
* template: ` * template: `
* <input type="radio" name="food" [(ngModel)]="foodChicken"> * <input type="radio" name="food" [(ngModel)]="food" value="chicken">
* <input type="radio" name="food" [(ngModel)]="foodFish"> * <input type="radio" name="food" [(ngModel)]="food" value="fish">
* ` * `
* }) * })
* class FoodCmp { * class FoodCmp {
* foodChicken = new RadioButtonState(true, "chicken"); * food = 'chicken';
* foodFish = new RadioButtonState(false, "fish");
* } * }
* ``` * ```
*/ */
@ -85,14 +74,16 @@ export class RadioButtonState {
export class RadioControlValueAccessor implements ControlValueAccessor, export class RadioControlValueAccessor implements ControlValueAccessor,
OnDestroy, OnInit { OnDestroy, OnInit {
/** @internal */ /** @internal */
_state: RadioButtonState; _state: boolean;
/** @internal */ /** @internal */
_control: NgControl; _control: NgControl;
@Input() name: string;
/** @internal */ /** @internal */
_fn: Function; _fn: Function;
onChange = () => {}; onChange = () => {};
onTouched = () => {}; onTouched = () => {}
@Input() name: string;
@Input() value: any;
constructor( constructor(
private _renderer: Renderer, private _elementRef: ElementRef, private _renderer: Renderer, private _elementRef: ElementRef,
@ -106,21 +97,21 @@ export class RadioControlValueAccessor implements ControlValueAccessor,
ngOnDestroy(): void { this._registry.remove(this); } ngOnDestroy(): void { this._registry.remove(this); }
writeValue(value: any): void { writeValue(value: any): void {
this._state = value; this._state = value === this.value;
if (isPresent(value) && value.checked) { if (isPresent(value)) {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', true); this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', this._state);
} }
} }
registerOnChange(fn: (_: any) => {}): void { registerOnChange(fn: (_: any) => {}): void {
this._fn = fn; this._fn = fn;
this.onChange = () => { this.onChange = () => {
fn(new RadioButtonState(true, this._state.value)); fn(this.value);
this._registry.select(this); 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; } registerOnTouched(fn: () => {}): void { this.onTouched = fn; }
} }

View File

@ -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 {AbstractControlDirective} from './directives/abstract_control_directive';
export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor'; export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
export {ControlContainer} from './directives/control_container'; export {ControlContainer} from './directives/control_container';

View File

@ -282,7 +282,7 @@ export abstract class AbstractControl {
*/ */
export class FormControl extends AbstractControl { export class FormControl extends AbstractControl {
/** @internal */ /** @internal */
_onChange: Function; _onChange: Function[] = [];
constructor( constructor(
value: any = null, validator: ValidatorFn|ValidatorFn[] = null, value: any = null, validator: ValidatorFn|ValidatorFn[] = null,
@ -312,7 +312,9 @@ export class FormControl extends AbstractControl {
} = {}): void { } = {}): void {
emitModelToViewChange = isPresent(emitModelToViewChange) ? emitModelToViewChange : true; emitModelToViewChange = isPresent(emitModelToViewChange) ? emitModelToViewChange : true;
this._value = value; 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}); this.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
} }
@ -329,7 +331,7 @@ export class FormControl extends AbstractControl {
/** /**
* Register a listener for change events. * 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. * 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; this.controls[name] = control;
control.setParent(this); control.setParent(this);
return control;
} }
/** /**

View File

@ -6,7 +6,7 @@ import {Input, Provider, forwardRef} from '@angular/core';
import {fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; 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 {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 {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 {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {dispatchEvent} from '@angular/platform-browser/testing'; import {dispatchEvent} from '@angular/platform-browser/testing';
@ -503,29 +503,35 @@ export function main() {
[TestComponentBuilder, AsyncTestCompleter], [TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => { (tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form [formGroup]="form"> const t = `<form [formGroup]="form">
<input type="radio" formControlName="foodChicken" name="food"> <input type="radio" formControlName="food" value="chicken">
<input type="radio" formControlName="foodFish" name="food"> <input type="radio" formControlName="food" value="fish">
</form>`; </form>`;
const ctrl = new FormControl('fish');
tcb.overrideTemplate(MyComp8, t) tcb.overrideTemplate(MyComp8, t)
.overrideProviders(MyComp8, providerArr) .overrideProviders(MyComp8, providerArr)
.createAsync(MyComp8) .createAsync(MyComp8)
.then((fixture) => { .then((fixture) => {
fixture.debugElement.componentInstance.form = new FormGroup({ fixture.debugElement.componentInstance.form = new FormGroup({'food': ctrl});
'foodChicken': new FormControl(new RadioButtonState(false, 'chicken')),
'foodFish': new FormControl(new RadioButtonState(true, 'fish'))
});
fixture.detectChanges(); fixture.detectChanges();
var input = fixture.debugElement.query(By.css('input')); var inputs = fixture.debugElement.queryAll(By.css('input'));
expect(input.nativeElement.checked).toEqual(false); 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(); fixture.detectChanges();
let value = fixture.debugElement.componentInstance.form.value; let value = fixture.debugElement.componentInstance.form.value;
expect(value['foodChicken'].checked).toEqual(true); expect(value.food).toEqual('chicken');
expect(value['foodFish'].checked).toEqual(false); 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(); async.done();
}); });
})); }));
@ -1393,77 +1399,70 @@ export function main() {
expect(form.value).toEqual({two: 'some data'}); expect(form.value).toEqual({two: 'some data'});
}))); })));
// TODO(kara): Fix when re-doing radio buttons it('should support <type=radio>',
xit('should support <type=radio>',
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
const t = `<form> const t = `<form>
<input type="radio" name="food" [(ngModel)]="data['chicken']"> <input type="radio" name="food" [(ngModel)]="data.food" value="chicken">
<input type="radio" name="food" [(ngModel)]="data['fish']"> <input type="radio" name="food" [(ngModel)]="data.food" value="fish">
<input type="radio" name="food" [(ngModel)]="data['beef']">
<input type="radio" name="food" [(ngModel)]="data['pork']">
</form>`; </form>`;
const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8); const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8);
tick(); tick();
fixture.debugElement.componentInstance.data = { fixture.debugElement.componentInstance.data = {food: 'fish'};
'chicken': new RadioButtonState(false, 'chicken'),
'fish': new RadioButtonState(true, 'fish'),
'beef': new RadioButtonState(false, 'beef'),
'pork': new RadioButtonState(true, 'pork')
};
fixture.detectChanges(); fixture.detectChanges();
tick(); tick();
const input = fixture.debugElement.query(By.css('input')); const inputs = fixture.debugElement.queryAll(By.css('input'));
expect(input.nativeElement.checked).toEqual(false); expect(inputs[0].nativeElement.checked).toEqual(false);
expect(inputs[1].nativeElement.checked).toEqual(true);
dispatchEvent(input.nativeElement, 'change'); dispatchEvent(inputs[0].nativeElement, 'change');
tick(); tick();
const data = fixture.debugElement.componentInstance.data; const data = fixture.debugElement.componentInstance.data;
expect(data['chicken']).toEqual(new RadioButtonState(true, 'chicken')); expect(data.food).toEqual('chicken');
expect(data['fish']).toEqual(new RadioButtonState(false, 'fish')); expect(inputs[1].nativeElement.checked).toEqual(false);
expect(data['beef']).toEqual(new RadioButtonState(false, 'beef'));
expect(data['pork']).toEqual(new RadioButtonState(false, 'pork'));
}))); })));
it('should support multiple named <type=radio> groups',
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
const t = `<form>
<input type="radio" name="food" [(ngModel)]="data.food" value="chicken">
<input type="radio" name="food" [(ngModel)]="data.food" value="fish">
<input type="radio" name="drink" [(ngModel)]="data.drink" value="cola">
<input type="radio" name="drink" [(ngModel)]="data.drink" value="sprite">
</form>`;
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);
})));
}); });
xit('should support multiple named <type=radio> groups',
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
const t = `<form>
<input type="radio" name="food" [(ngModel)]="data['chicken']">
<input type="radio" name="food" [(ngModel)]="data['fish']">
<input type="radio" name="drink" [(ngModel)]="data['cola']">
<input type="radio" name="drink" [(ngModel)]="data['sprite']">
</form>`;
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', () => { describe('setting status classes', () => {
it('should work with single fields', it('should work with single fields',