Skip to content

Commit

Permalink
fix(forms): add support for radio buttons
Browse files Browse the repository at this point in the history
Closes #6877
  • Loading branch information
vsavkin committed Feb 9, 2016
1 parent 2337469 commit e725542
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 25 deletions.
25 changes: 23 additions & 2 deletions modules/angular2/src/common/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,33 @@ 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,
MinLengthValidator,
MaxLengthValidator,
Validator
} from './forms/directives/validators';
export {FormBuilder, FORM_PROVIDERS, FORM_BINDINGS} from './forms/form_builder';
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;
6 changes: 6 additions & 0 deletions modules/angular2/src/common/forms/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -63,6 +68,7 @@ export const FORM_DIRECTIVES: Type[] = CONST_EXPR([
NumberValueAccessor,
CheckboxControlValueAccessor,
SelectControlValueAccessor,
RadioControlValueAccessor,
NgControlStatus,

RequiredValidator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (_) => {};
Expand Down
Original file line number Diff line number Diff line change
@@ -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: `
* <input type="radio" name="food" [(ngModel)]="foodChicken">
* <input type="radio" name="food" [(ngModel)]="foodFish">
* `
* })
* 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; }
}
20 changes: 1 addition & 19 deletions modules/angular2/src/common/forms/form_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
10 changes: 10 additions & 0 deletions modules/angular2/src/common/forms/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
74 changes: 72 additions & 2 deletions modules/angular2/test/common/forms/integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
dispatchEvent,
fakeAsync,
tick,
flushMicrotasks,
expect,
it,
inject,
Expand All @@ -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';
Expand Down Expand Up @@ -328,6 +330,33 @@ export function main() {
});
}));

it("should support <type=radio>",
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
var t = `<form [ngFormModel]="form">
<input type="radio" ngControl="foodChicken" name="food">
<input type="radio" ngControl="foodFish" name="food">
</form>`;

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 <select>",
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
var t = `<div [ngFormModel]="form">
Expand Down Expand Up @@ -812,9 +841,50 @@ export function main() {

expect(fixture.debugElement.componentInstance.name).toEqual("updatedValue");
})));
});


it("should support <type=radio>",
inject([TestComponentBuilder], fakeAsync((tcb: TestComponentBuilder) => {
var t = `<form>
<input type="radio" name="food" ngControl="chicken" [(ngModel)]="data['chicken1']">
<input type="radio" name="food" ngControl="fish" [(ngModel)]="data['fish1']">
</form>
<form>
<input type="radio" name="food" ngControl="chicken" [(ngModel)]="data['chicken2']">
<input type="radio" name="food" ngControl="fish" [(ngModel)]="data['fish2']">
</form>`;

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) => {
Expand Down
11 changes: 10 additions & 1 deletion modules/angular2/test/public_api_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ var NG_COMMON = [
'AbstractControl.validator',
'AbstractControl.validator=',
'AbstractControl.value',
'AbstractControl.root',
'AbstractControl.valueChanges',
'AbstractControlDirective',
'AbstractControlDirective.control',
Expand Down Expand Up @@ -102,6 +103,7 @@ var NG_COMMON = [
'Control.validator',
'Control.validator=',
'Control.value',
'Control.root',
'Control.valueChanges',
'ControlArray',
'ControlArray.asyncValidator',
Expand Down Expand Up @@ -134,6 +136,7 @@ var NG_COMMON = [
'ControlArray.validator',
'ControlArray.validator=',
'ControlArray.value',
'ControlArray.root',
'ControlArray.valueChanges',
'ControlContainer',
'ControlContainer.control',
Expand Down Expand Up @@ -179,6 +182,7 @@ var NG_COMMON = [
'ControlGroup.validator',
'ControlGroup.validator=',
'ControlGroup.value',
'ControlGroup.root',
'ControlGroup.valueChanges',
'ControlValueAccessor:dart',
'CurrencyPipe',
Expand Down Expand Up @@ -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 = [
Expand Down
Loading

0 comments on commit e725542

Please sign in to comment.