X Tutup
Skip to content

Commit e725542

Browse files
committed
fix(forms): add support for radio buttons
Closes #6877
1 parent 2337469 commit e725542

File tree

9 files changed

+252
-25
lines changed

9 files changed

+252
-25
lines changed

modules/angular2/src/common/forms.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,33 @@ export {
3131
NgSelectOption,
3232
SelectControlValueAccessor
3333
} from './forms/directives/select_control_value_accessor';
34-
export {FORM_DIRECTIVES} from './forms/directives';
34+
export {FORM_DIRECTIVES, RadioButtonState} from './forms/directives';
3535
export {NG_VALIDATORS, NG_ASYNC_VALIDATORS, Validators} from './forms/validators';
3636
export {
3737
RequiredValidator,
3838
MinLengthValidator,
3939
MaxLengthValidator,
4040
Validator
4141
} from './forms/directives/validators';
42-
export {FormBuilder, FORM_PROVIDERS, FORM_BINDINGS} from './forms/form_builder';
42+
export {FormBuilder} from './forms/form_builder';
43+
import {FormBuilder} from './forms/form_builder';
44+
import {RadioControlRegistry} from './forms/directives/radio_control_value_accessor';
45+
import {Type, CONST_EXPR} from 'angular2/src/facade/lang';
46+
47+
/**
48+
* Shorthand set of providers used for building Angular forms.
49+
*
50+
* ### Example
51+
*
52+
* ```typescript
53+
* bootstrap(MyApp, [FORM_PROVIDERS]);
54+
* ```
55+
*/
56+
export const FORM_PROVIDERS: Type[] = CONST_EXPR([FormBuilder, RadioControlRegistry]);
57+
58+
/**
59+
* See {@link FORM_PROVIDERS} instead.
60+
*
61+
* @deprecated
62+
*/
63+
export const FORM_BINDINGS = FORM_PROVIDERS;

modules/angular2/src/common/forms/directives.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {NgForm} from './directives/ng_form';
88
import {DefaultValueAccessor} from './directives/default_value_accessor';
99
import {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
1010
import {NumberValueAccessor} from './directives/number_value_accessor';
11+
import {RadioControlValueAccessor} from './directives/radio_control_value_accessor';
1112
import {NgControlStatus} from './directives/ng_control_status';
1213
import {
1314
SelectControlValueAccessor,
@@ -23,6 +24,10 @@ export {NgFormModel} from './directives/ng_form_model';
2324
export {NgForm} from './directives/ng_form';
2425
export {DefaultValueAccessor} from './directives/default_value_accessor';
2526
export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
27+
export {
28+
RadioControlValueAccessor,
29+
RadioButtonState
30+
} from './directives/radio_control_value_accessor';
2631
export {NumberValueAccessor} from './directives/number_value_accessor';
2732
export {NgControlStatus} from './directives/ng_control_status';
2833
export {
@@ -63,6 +68,7 @@ export const FORM_DIRECTIVES: Type[] = CONST_EXPR([
6368
NumberValueAccessor,
6469
CheckboxControlValueAccessor,
6570
SelectControlValueAccessor,
71+
RadioControlValueAccessor,
6672
NgControlStatus,
6773

6874
RequiredValidator,

modules/angular2/src/common/forms/directives/checkbox_value_accessor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const CHECKBOX_VALUE_ACCESSOR = CONST_EXPR(new Provider(
1818
selector:
1919
'input[type=checkbox][ngControl],input[type=checkbox][ngFormControl],input[type=checkbox][ngModel]',
2020
host: {'(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()'},
21-
bindings: [CHECKBOX_VALUE_ACCESSOR]
21+
providers: [CHECKBOX_VALUE_ACCESSOR]
2222
})
2323
export class CheckboxControlValueAccessor implements ControlValueAccessor {
2424
onChange = (_) => {};
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {
2+
Directive,
3+
ElementRef,
4+
Renderer,
5+
Self,
6+
forwardRef,
7+
Provider,
8+
Attribute,
9+
Input,
10+
OnInit,
11+
OnDestroy,
12+
Injector,
13+
Injectable
14+
} from 'angular2/core';
15+
import {
16+
NG_VALUE_ACCESSOR,
17+
ControlValueAccessor
18+
} from 'angular2/src/common/forms/directives/control_value_accessor';
19+
import {NgControl} from 'angular2/src/common/forms/directives/ng_control';
20+
import {CONST_EXPR, looseIdentical, isPresent} from 'angular2/src/facade/lang';
21+
import {ListWrapper} from 'angular2/src/facade/collection';
22+
23+
const RADIO_VALUE_ACCESSOR = CONST_EXPR(new Provider(
24+
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => RadioControlValueAccessor), multi: true}));
25+
26+
27+
/**
28+
* Internal class used by Angular to uncheck radio buttons with the matching name.
29+
*/
30+
@Injectable()
31+
export class RadioControlRegistry {
32+
private _accessors: any[] = [];
33+
34+
add(control: NgControl, accessor: RadioControlValueAccessor) {
35+
this._accessors.push([control, accessor]);
36+
}
37+
38+
remove(accessor: RadioControlValueAccessor) {
39+
var indexToRemove = -1;
40+
for (var i = 0; i < this._accessors.length; ++i) {
41+
if (this._accessors[i][1] === accessor) {
42+
indexToRemove = i;
43+
}
44+
}
45+
ListWrapper.removeAt(this._accessors, indexToRemove);
46+
}
47+
48+
select(accessor: RadioControlValueAccessor) {
49+
this._accessors.forEach((c) => {
50+
if (c[0].control.root === accessor._control.control.root && c[1] !== accessor) {
51+
c[1].fireUncheck();
52+
}
53+
});
54+
}
55+
}
56+
57+
/**
58+
* The value provided by the forms API for radio buttons.
59+
*/
60+
export class RadioButtonState {
61+
constructor(public checked: boolean, public value: string) {}
62+
}
63+
64+
65+
/**
66+
* The accessor for writing a radio control value and listening to changes that is used by the
67+
* {@link NgModel}, {@link NgFormControl}, and {@link NgControlName} directives.
68+
*
69+
* ### Example
70+
* ```
71+
* @Component({
72+
* template: `
73+
* <input type="radio" name="food" [(ngModel)]="foodChicken">
74+
* <input type="radio" name="food" [(ngModel)]="foodFish">
75+
* `
76+
* })
77+
* class FoodCmp {
78+
* foodChicken = new RadioButtonState(true, "chicken");
79+
* foodFish = new RadioButtonState(false, "fish");
80+
* }
81+
* ```
82+
*/
83+
@Directive({
84+
selector:
85+
'input[type=radio][ngControl],input[type=radio][ngFormControl],input[type=radio][ngModel]',
86+
host: {'(change)': 'onChange()', '(blur)': 'onTouched()'},
87+
providers: [RADIO_VALUE_ACCESSOR]
88+
})
89+
export class RadioControlValueAccessor implements ControlValueAccessor,
90+
OnDestroy, OnInit {
91+
_state: RadioButtonState;
92+
_control: NgControl;
93+
@Input() name: string;
94+
_fn: Function;
95+
onChange = () => {};
96+
onTouched = () => {};
97+
98+
constructor(private _renderer: Renderer, private _elementRef: ElementRef,
99+
private _registry: RadioControlRegistry, private _injector: Injector) {}
100+
101+
ngOnInit(): void {
102+
this._control = this._injector.get(NgControl);
103+
this._registry.add(this._control, this);
104+
}
105+
106+
ngOnDestroy(): void { this._registry.remove(this); }
107+
108+
writeValue(value: any): void {
109+
this._state = value;
110+
if (isPresent(value) && value.checked) {
111+
this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', true);
112+
}
113+
}
114+
115+
registerOnChange(fn: (_: any) => {}): void {
116+
this._fn = fn;
117+
this.onChange = () => {
118+
fn(new RadioButtonState(true, this._state.value));
119+
this._registry.select(this);
120+
};
121+
}
122+
123+
fireUncheck(): void { this._fn(new RadioButtonState(false, this._state.value)); }
124+
125+
registerOnTouched(fn: () => {}): void { this.onTouched = fn; }
126+
}

modules/angular2/src/common/forms/form_builder.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -105,22 +105,4 @@ export class FormBuilder {
105105
return this.control(controlConfig);
106106
}
107107
}
108-
}
109-
110-
/**
111-
* Shorthand set of providers used for building Angular forms.
112-
*
113-
* ### Example
114-
*
115-
* ```typescript
116-
* bootstrap(MyApp, [FORM_PROVIDERS]);
117-
* ```
118-
*/
119-
export const FORM_PROVIDERS: Type[] = CONST_EXPR([FormBuilder]);
120-
121-
/**
122-
* See {@link FORM_PROVIDERS} instead.
123-
*
124-
* @deprecated
125-
*/
126-
export const FORM_BINDINGS = FORM_PROVIDERS;
108+
}

modules/angular2/src/common/forms/model.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,16 @@ export abstract class AbstractControl {
208208
return isPresent(this.getError(errorCode, path));
209209
}
210210

211+
get root(): AbstractControl {
212+
let x: AbstractControl = this;
213+
214+
while (isPresent(x._parent)) {
215+
x = x._parent;
216+
}
217+
218+
return x;
219+
}
220+
211221
/** @internal */
212222
_updateControlsErrors(): void {
213223
this._status = this._calculateStatus();

modules/angular2/test/common/forms/integration_spec.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
dispatchEvent,
1111
fakeAsync,
1212
tick,
13+
flushMicrotasks,
1314
expect,
1415
it,
1516
inject,
@@ -31,7 +32,8 @@ import {
3132
NgFor,
3233
NgForm,
3334
Validators,
34-
Validator
35+
Validator,
36+
RadioButtonState
3537
} from 'angular2/common';
3638
import {Provider, forwardRef, Input} from 'angular2/core';
3739
import {By} from 'angular2/platform/browser';
@@ -328,6 +330,33 @@ export function main() {
328330
});
329331
}));
330332

333+
it("should support <type=radio>",
334+
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
335+
var t = `<form [ngFormModel]="form">
336+
<input type="radio" ngControl="foodChicken" name="food">
337+
<input type="radio" ngControl="foodFish" name="food">
338+
</form>`;
339+
340+
tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((fixture) => {
341+
fixture.debugElement.componentInstance.form = new ControlGroup({
342+
"foodChicken": new Control(new RadioButtonState(false, 'chicken')),
343+
"foodFish": new Control(new RadioButtonState(true, 'fish'))
344+
});
345+
fixture.detectChanges();
346+
347+
var input = fixture.debugElement.query(By.css("input"));
348+
expect(input.nativeElement.checked).toEqual(false);
349+
350+
dispatchEvent(input.nativeElement, "change");
351+
fixture.detectChanges();
352+
353+
let value = fixture.debugElement.componentInstance.form.value;
354+
expect(value['foodChicken'].checked).toEqual(true);
355+
expect(value['foodFish'].checked).toEqual(false);
356+
async.done();
357+
});
358+
}));
359+
331360
it("should support <select>",
332361
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
333362
var t = `<div [ngFormModel]="form">
@@ -812,9 +841,50 @@ export function main() {
812841

813842
expect(fixture.debugElement.componentInstance.name).toEqual("updatedValue");
814843
})));
815-
});
816844

817845

846+
it("should support <type=radio>",
847+
inject([TestComponentBuilder], fakeAsync((tcb: TestComponentBuilder) => {
848+
var t = `<form>
849+
<input type="radio" name="food" ngControl="chicken" [(ngModel)]="data['chicken1']">
850+
<input type="radio" name="food" ngControl="fish" [(ngModel)]="data['fish1']">
851+
</form>
852+
853+
<form>
854+
<input type="radio" name="food" ngControl="chicken" [(ngModel)]="data['chicken2']">
855+
<input type="radio" name="food" ngControl="fish" [(ngModel)]="data['fish2']">
856+
</form>`;
857+
858+
var fixture: ComponentFixture;
859+
tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((f) => { fixture = f; });
860+
tick();
861+
862+
fixture.debugElement.componentInstance.data = {
863+
'chicken1': new RadioButtonState(false, 'chicken'),
864+
'fish1': new RadioButtonState(true, 'fish'),
865+
866+
'chicken2': new RadioButtonState(false, 'chicken'),
867+
'fish2': new RadioButtonState(true, 'fish')
868+
};
869+
fixture.detectChanges();
870+
tick();
871+
872+
var input = fixture.debugElement.query(By.css("input"));
873+
expect(input.nativeElement.checked).toEqual(false);
874+
875+
dispatchEvent(input.nativeElement, "change");
876+
tick();
877+
878+
let data = fixture.debugElement.componentInstance.data;
879+
880+
expect(data['chicken1']).toEqual(new RadioButtonState(true, 'chicken'));
881+
expect(data['fish1']).toEqual(new RadioButtonState(false, 'fish'));
882+
883+
expect(data['chicken2']).toEqual(new RadioButtonState(false, 'chicken'));
884+
expect(data['fish2']).toEqual(new RadioButtonState(true, 'fish'));
885+
})));
886+
});
887+
818888
describe("setting status classes", () => {
819889
it("should work with single fields",
820890
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {

modules/angular2/test/public_api_spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ var NG_COMMON = [
5151
'AbstractControl.validator',
5252
'AbstractControl.validator=',
5353
'AbstractControl.value',
54+
'AbstractControl.root',
5455
'AbstractControl.valueChanges',
5556
'AbstractControlDirective',
5657
'AbstractControlDirective.control',
@@ -102,6 +103,7 @@ var NG_COMMON = [
102103
'Control.validator',
103104
'Control.validator=',
104105
'Control.value',
106+
'Control.root',
105107
'Control.valueChanges',
106108
'ControlArray',
107109
'ControlArray.asyncValidator',
@@ -134,6 +136,7 @@ var NG_COMMON = [
134136
'ControlArray.validator',
135137
'ControlArray.validator=',
136138
'ControlArray.value',
139+
'ControlArray.root',
137140
'ControlArray.valueChanges',
138141
'ControlContainer',
139142
'ControlContainer.control',
@@ -179,6 +182,7 @@ var NG_COMMON = [
179182
'ControlGroup.validator',
180183
'ControlGroup.validator=',
181184
'ControlGroup.value',
185+
'ControlGroup.root',
182186
'ControlGroup.valueChanges',
183187
'ControlValueAccessor:dart',
184188
'CurrencyPipe',
@@ -447,7 +451,12 @@ var NG_COMMON = [
447451
'Validators#maxLength()',
448452
'Validators#minLength()',
449453
'Validators#nullValidator()',
450-
'Validators#required()'
454+
'Validators#required()',
455+
'RadioButtonState',
456+
'RadioButtonState.checked',
457+
'RadioButtonState.checked=',
458+
'RadioButtonState.value',
459+
'RadioButtonState.value='
451460
];
452461

453462
var NG_COMPILER = [

0 commit comments

Comments
 (0)
X Tutup