Skip to content

Commit 39e0b49

Browse files
committed
feat(radio): support radio button sharing a control
1 parent 54c577c commit 39e0b49

File tree

6 files changed

+99
-103
lines changed

6 files changed

+99
-103
lines changed

modules/@angular/forms/src/directives.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export {NgForm} from './directives/ng_form';
2525
export {NgModel} from './directives/ng_model';
2626
export {NgModelGroup} from './directives/ng_model_group';
2727
export {NumberValueAccessor} from './directives/number_value_accessor';
28-
export {RadioButtonState, RadioControlValueAccessor} from './directives/radio_control_value_accessor';
28+
export {RadioControlValueAccessor} from './directives/radio_control_value_accessor';
2929
export {FormControlDirective} from './directives/reactive_directives/form_control_directive';
3030
export {FormControlName} from './directives/reactive_directives/form_control_name';
3131
export {FormGroupDirective} from './directives/reactive_directives/form_group_directive';

modules/@angular/forms/src/directives/ng_form.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
99
import {ControlContainer} from './control_container';
1010
import {Form} from './form_interface';
1111
import {NgControl} from './ng_control';
12+
import {NgModel} from './ng_model';
1213
import {NgModelGroup} from './ng_model_group';
1314
import {composeAsyncValidators, composeValidators, setUpControl, setUpFormGroup} from './shared';
1415

@@ -107,20 +108,21 @@ export class NgForm extends ControlContainer implements Form {
107108

108109
get controls(): {[key: string]: AbstractControl} { return this.form.controls; }
109110

110-
addControl(dir: NgControl): FormControl {
111+
addControl(dir: NgModel): FormControl {
111112
const ctrl = new FormControl();
112113
PromiseWrapper.scheduleMicrotask(() => {
113114
const container = this._findContainer(dir.path);
114-
setUpControl(ctrl, dir);
115-
container.registerControl(dir.name, ctrl);
116-
ctrl.updateValueAndValidity({emitEvent: false});
115+
dir._control = <FormControl>container.registerControl(dir.name, ctrl);
116+
setUpControl(dir.control, dir);
117+
dir.control.updateValueAndValidity({emitEvent: false});
117118
});
119+
118120
return ctrl;
119121
}
120122

121-
getControl(dir: NgControl): FormControl { return <FormControl>this.form.find(dir.path); }
123+
getControl(dir: NgModel): FormControl { return <FormControl>this.form.find(dir.path); }
122124

123-
removeControl(dir: NgControl): void {
125+
removeControl(dir: NgModel): void {
124126
PromiseWrapper.scheduleMicrotask(() => {
125127
var container = this._findContainer(dir.path);
126128
if (isPresent(container)) {

modules/@angular/forms/src/directives/radio_control_value_accessor.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class RadioControlRegistry {
3636
select(accessor: RadioControlValueAccessor) {
3737
this._accessors.forEach((c) => {
3838
if (this._isSameGroup(c, accessor) && c[1] !== accessor) {
39-
c[1].fireUncheck();
39+
c[1].fireUncheck(accessor.value);
4040
}
4141
});
4242
}
@@ -48,16 +48,6 @@ export class RadioControlRegistry {
4848
}
4949
}
5050

51-
/**
52-
* The value provided by the forms API for radio buttons.
53-
*
54-
* @experimental
55-
*/
56-
export class RadioButtonState {
57-
constructor(public checked: boolean, public value: string) {}
58-
}
59-
60-
6151
/**
6252
* The accessor for writing a radio control value and listening to changes that is used by the
6353
* {@link NgModel}, {@link FormControlDirective}, and {@link FormControlName} directives.
@@ -66,13 +56,12 @@ export class RadioButtonState {
6656
* ```
6757
* @Component({
6858
* template: `
69-
* <input type="radio" name="food" [(ngModel)]="foodChicken">
70-
* <input type="radio" name="food" [(ngModel)]="foodFish">
59+
* <input type="radio" name="food" [(ngModel)]="food" value="chicken">
60+
* <input type="radio" name="food" [(ngModel)]="food" value="fish">
7161
* `
7262
* })
7363
* class FoodCmp {
74-
* foodChicken = new RadioButtonState(true, "chicken");
75-
* foodFish = new RadioButtonState(false, "fish");
64+
* food = 'chicken';
7665
* }
7766
* ```
7867
*/
@@ -85,14 +74,16 @@ export class RadioButtonState {
8574
export class RadioControlValueAccessor implements ControlValueAccessor,
8675
OnDestroy, OnInit {
8776
/** @internal */
88-
_state: RadioButtonState;
77+
_state: boolean;
8978
/** @internal */
9079
_control: NgControl;
91-
@Input() name: string;
9280
/** @internal */
9381
_fn: Function;
9482
onChange = () => {};
95-
onTouched = () => {};
83+
onTouched = () => {}
84+
85+
@Input() name: string;
86+
@Input() value: any;
9687

9788
constructor(
9889
private _renderer: Renderer, private _elementRef: ElementRef,
@@ -106,21 +97,21 @@ export class RadioControlValueAccessor implements ControlValueAccessor,
10697
ngOnDestroy(): void { this._registry.remove(this); }
10798

10899
writeValue(value: any): void {
109-
this._state = value;
110-
if (isPresent(value) && value.checked) {
111-
this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', true);
100+
this._state = value === this.value;
101+
if (isPresent(value)) {
102+
this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', this._state);
112103
}
113104
}
114105

115106
registerOnChange(fn: (_: any) => {}): void {
116107
this._fn = fn;
117108
this.onChange = () => {
118-
fn(new RadioButtonState(true, this._state.value));
109+
fn(this.value);
119110
this._registry.select(this);
120111
};
121112
}
122113

123-
fireUncheck(): void { this._fn(new RadioButtonState(false, this._state.value)); }
114+
fireUncheck(value: any): void { this.writeValue(value); }
124115

125116
registerOnTouched(fn: () => {}): void { this.onTouched = fn; }
126117
}

modules/@angular/forms/src/forms.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*/
1414

1515

16-
export {FORM_DIRECTIVES, REACTIVE_FORM_DIRECTIVES, RadioButtonState} from './directives';
16+
export {FORM_DIRECTIVES, REACTIVE_FORM_DIRECTIVES} from './directives';
1717
export {AbstractControlDirective} from './directives/abstract_control_directive';
1818
export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
1919
export {ControlContainer} from './directives/control_container';

modules/@angular/forms/src/model.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ export abstract class AbstractControl {
282282
*/
283283
export class FormControl extends AbstractControl {
284284
/** @internal */
285-
_onChange: Function;
285+
_onChange: Function[] = [];
286286

287287
constructor(
288288
value: any = null, validator: ValidatorFn|ValidatorFn[] = null,
@@ -312,7 +312,9 @@ export class FormControl extends AbstractControl {
312312
} = {}): void {
313313
emitModelToViewChange = isPresent(emitModelToViewChange) ? emitModelToViewChange : true;
314314
this._value = value;
315-
if (isPresent(this._onChange) && emitModelToViewChange) this._onChange(this._value);
315+
if (this._onChange.length && emitModelToViewChange) {
316+
this._onChange.forEach((changeFn) => changeFn(this._value));
317+
}
316318
this.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
317319
}
318320

@@ -329,7 +331,7 @@ export class FormControl extends AbstractControl {
329331
/**
330332
* Register a listener for change events.
331333
*/
332-
registerOnChange(fn: Function): void { this._onChange = fn; }
334+
registerOnChange(fn: Function): void { this._onChange.push(fn); }
333335
}
334336

335337
/**
@@ -364,9 +366,11 @@ export class FormGroup extends AbstractControl {
364366
/**
365367
* Register a control with the group's list of controls.
366368
*/
367-
registerControl(name: string, control: AbstractControl): void {
369+
registerControl(name: string, control: AbstractControl): AbstractControl {
370+
if (this.controls[name]) return this.controls[name];
368371
this.controls[name] = control;
369372
control.setParent(this);
373+
return control;
370374
}
371375

372376
/**

modules/@angular/forms/test/integration_spec.ts

Lines changed: 67 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Input, Provider, forwardRef} from '@angular/core';
66
import {fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
77
import {afterEach, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
88
import {AsyncTestCompleter} from '@angular/core/testing/testing_internal';
9-
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';
9+
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';
1010
import {By} from '@angular/platform-browser/src/dom/debug/by';
1111
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
1212
import {dispatchEvent} from '@angular/platform-browser/testing';
@@ -503,29 +503,35 @@ export function main() {
503503
[TestComponentBuilder, AsyncTestCompleter],
504504
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
505505
const t = `<form [formGroup]="form">
506-
<input type="radio" formControlName="foodChicken" name="food">
507-
<input type="radio" formControlName="foodFish" name="food">
506+
<input type="radio" formControlName="food" value="chicken">
507+
<input type="radio" formControlName="food" value="fish">
508508
</form>`;
509509

510+
const ctrl = new FormControl('fish');
510511
tcb.overrideTemplate(MyComp8, t)
511512
.overrideProviders(MyComp8, providerArr)
512513
.createAsync(MyComp8)
513514
.then((fixture) => {
514-
fixture.debugElement.componentInstance.form = new FormGroup({
515-
'foodChicken': new FormControl(new RadioButtonState(false, 'chicken')),
516-
'foodFish': new FormControl(new RadioButtonState(true, 'fish'))
517-
});
515+
fixture.debugElement.componentInstance.form = new FormGroup({'food': ctrl});
518516
fixture.detectChanges();
519517

520-
var input = fixture.debugElement.query(By.css('input'));
521-
expect(input.nativeElement.checked).toEqual(false);
518+
var inputs = fixture.debugElement.queryAll(By.css('input'));
519+
expect(inputs[0].nativeElement.checked).toEqual(false);
520+
expect(inputs[1].nativeElement.checked).toEqual(true);
522521

523-
dispatchEvent(input.nativeElement, 'change');
522+
dispatchEvent(inputs[0].nativeElement, 'change');
524523
fixture.detectChanges();
525524

526525
let value = fixture.debugElement.componentInstance.form.value;
527-
expect(value['foodChicken'].checked).toEqual(true);
528-
expect(value['foodFish'].checked).toEqual(false);
526+
expect(value.food).toEqual('chicken');
527+
expect(inputs[1].nativeElement.checked).toEqual(false);
528+
529+
ctrl.updateValue('fish');
530+
fixture.detectChanges();
531+
532+
expect(inputs[0].nativeElement.checked).toEqual(false);
533+
expect(inputs[1].nativeElement.checked).toEqual(true);
534+
529535
async.done();
530536
});
531537
}));
@@ -1393,77 +1399,70 @@ export function main() {
13931399
expect(form.value).toEqual({two: 'some data'});
13941400
})));
13951401

1396-
// TODO(kara): Fix when re-doing radio buttons
1397-
xit('should support <type=radio>',
1398-
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
1399-
const t = `<form>
1400-
<input type="radio" name="food" [(ngModel)]="data['chicken']">
1401-
<input type="radio" name="food" [(ngModel)]="data['fish']">
1402-
<input type="radio" name="food" [(ngModel)]="data['beef']">
1403-
<input type="radio" name="food" [(ngModel)]="data['pork']">
1402+
it('should support <type=radio>',
1403+
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
1404+
const t = `<form>
1405+
<input type="radio" name="food" [(ngModel)]="data.food" value="chicken">
1406+
<input type="radio" name="food" [(ngModel)]="data.food" value="fish">
14041407
</form>`;
14051408

1406-
const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8);
1407-
tick();
1409+
const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8);
1410+
tick();
14081411

1409-
fixture.debugElement.componentInstance.data = {
1410-
'chicken': new RadioButtonState(false, 'chicken'),
1411-
'fish': new RadioButtonState(true, 'fish'),
1412-
'beef': new RadioButtonState(false, 'beef'),
1413-
'pork': new RadioButtonState(true, 'pork')
1414-
};
1415-
fixture.detectChanges();
1416-
tick();
1412+
fixture.debugElement.componentInstance.data = {food: 'fish'};
1413+
fixture.detectChanges();
1414+
tick();
14171415

1418-
const input = fixture.debugElement.query(By.css('input'));
1419-
expect(input.nativeElement.checked).toEqual(false);
1416+
const inputs = fixture.debugElement.queryAll(By.css('input'));
1417+
expect(inputs[0].nativeElement.checked).toEqual(false);
1418+
expect(inputs[1].nativeElement.checked).toEqual(true);
14201419

1421-
dispatchEvent(input.nativeElement, 'change');
1422-
tick();
1420+
dispatchEvent(inputs[0].nativeElement, 'change');
1421+
tick();
14231422

1424-
const data = fixture.debugElement.componentInstance.data;
1423+
const data = fixture.debugElement.componentInstance.data;
14251424

1426-
expect(data['chicken']).toEqual(new RadioButtonState(true, 'chicken'));
1427-
expect(data['fish']).toEqual(new RadioButtonState(false, 'fish'));
1428-
expect(data['beef']).toEqual(new RadioButtonState(false, 'beef'));
1429-
expect(data['pork']).toEqual(new RadioButtonState(false, 'pork'));
1430-
})));
1431-
});
1425+
expect(data.food).toEqual('chicken');
1426+
expect(inputs[1].nativeElement.checked).toEqual(false);
1427+
})));
14321428

1433-
xit('should support multiple named <type=radio> groups',
1434-
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
1435-
const t = `<form>
1436-
<input type="radio" name="food" [(ngModel)]="data['chicken']">
1437-
<input type="radio" name="food" [(ngModel)]="data['fish']">
1438-
<input type="radio" name="drink" [(ngModel)]="data['cola']">
1439-
<input type="radio" name="drink" [(ngModel)]="data['sprite']">
1429+
it('should support multiple named <type=radio> groups',
1430+
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
1431+
const t = `<form>
1432+
<input type="radio" name="food" [(ngModel)]="data.food" value="chicken">
1433+
<input type="radio" name="food" [(ngModel)]="data.food" value="fish">
1434+
<input type="radio" name="drink" [(ngModel)]="data.drink" value="cola">
1435+
<input type="radio" name="drink" [(ngModel)]="data.drink" value="sprite">
14401436
</form>`;
14411437

1442-
const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8);
1443-
tick();
1438+
const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8);
1439+
tick();
1440+
1441+
fixture.debugElement.componentInstance.data = {food: 'fish', drink: 'sprite'};
1442+
fixture.detectChanges();
1443+
tick();
14441444

1445-
fixture.debugElement.componentInstance.data = {
1446-
'chicken': new RadioButtonState(false, 'chicken'),
1447-
'fish': new RadioButtonState(true, 'fish'),
1448-
'cola': new RadioButtonState(false, 'cola'),
1449-
'sprite': new RadioButtonState(true, 'sprite')
1450-
};
1451-
fixture.detectChanges();
1452-
tick();
1445+
const inputs = fixture.debugElement.queryAll(By.css('input'));
1446+
expect(inputs[0].nativeElement.checked).toEqual(false);
1447+
expect(inputs[1].nativeElement.checked).toEqual(true);
1448+
expect(inputs[2].nativeElement.checked).toEqual(false);
1449+
expect(inputs[3].nativeElement.checked).toEqual(true);
1450+
1451+
dispatchEvent(inputs[0].nativeElement, 'change');
1452+
tick();
14531453

1454-
const input = fixture.debugElement.query(By.css('input'));
1455-
expect(input.nativeElement.checked).toEqual(false);
1454+
const data = fixture.debugElement.componentInstance.data;
14561455

1457-
dispatchEvent(input.nativeElement, 'change');
1458-
tick();
1456+
expect(data.food).toEqual('chicken');
1457+
expect(data.drink).toEqual('sprite');
1458+
expect(inputs[1].nativeElement.checked).toEqual(false);
1459+
expect(inputs[2].nativeElement.checked).toEqual(false);
1460+
expect(inputs[3].nativeElement.checked).toEqual(true);
14591461

1460-
const data = fixture.debugElement.componentInstance.data;
1462+
})));
1463+
1464+
});
14611465

1462-
expect(data['chicken']).toEqual(new RadioButtonState(true, 'chicken'));
1463-
expect(data['fish']).toEqual(new RadioButtonState(false, 'fish'));
1464-
expect(data['cola']).toEqual(new RadioButtonState(false, 'cola'));
1465-
expect(data['sprite']).toEqual(new RadioButtonState(true, 'sprite'));
1466-
})));
14671466

14681467
describe('setting status classes', () => {
14691468
it('should work with single fields',

0 commit comments

Comments
 (0)