X Tutup
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
}));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to this case where you use the "change" event, you should also have a case where one radio button becomes deselected by clicking another with the same name (because the deselection won't have an associated event).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added tests covering it


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='
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add a trailing comma, it'll reduce diff noise in the future.

];

var NG_COMPILER = [
Expand Down
Loading
X Tutup