diff --git a/modules/@angular/common/src/forms.ts b/modules/@angular/common/src/forms.ts new file mode 100644 index 0000000000..13c6f31d74 --- /dev/null +++ b/modules/@angular/common/src/forms.ts @@ -0,0 +1,58 @@ +/** + * @module + * @description + * This module is used for handling user input, by defining and building a {@link ControlGroup} that + * consists of + * {@link Control} objects, and mapping them onto the DOM. {@link Control} objects can then be used + * to read information + * from the form DOM elements. + * + * Forms providers are not included in default providers; you must import these providers + * explicitly. + */ +export {AbstractControl, Control, ControlGroup, ControlArray} from './forms/model'; + +export {AbstractControlDirective} from './forms/directives/abstract_control_directive'; +export {Form} from './forms/directives/form_interface'; +export {ControlContainer} from './forms/directives/control_container'; +export {NgControlName} from './forms/directives/ng_control_name'; +export {NgFormControl} from './forms/directives/ng_form_control'; +export {NgModel} from './forms/directives/ng_model'; +export {NgControl} from './forms/directives/ng_control'; +export {NgControlGroup} from './forms/directives/ng_control_group'; +export {NgFormModel} from './forms/directives/ng_form_model'; +export {NgForm} from './forms/directives/ng_form'; +export {ControlValueAccessor, NG_VALUE_ACCESSOR} from './forms/directives/control_value_accessor'; +export {DefaultValueAccessor} from './forms/directives/default_value_accessor'; +export {NgControlStatus} from './forms/directives/ng_control_status'; +export {CheckboxControlValueAccessor} from './forms/directives/checkbox_value_accessor'; +export { + NgSelectOption, + SelectControlValueAccessor +} from './forms/directives/select_control_value_accessor'; +export {FORM_DIRECTIVES, RadioButtonState} from './forms/directives'; +export {NG_VALIDATORS, NG_ASYNC_VALIDATORS, Validators} from './forms/validators'; +export { + RequiredValidator, + MinLengthValidator, + MaxLengthValidator, + PatternValidator, + Validator +} from './forms/directives/validators'; +export {FormBuilder} from './forms/form_builder'; +import {FormBuilder} from './forms/form_builder'; +import {RadioControlRegistry} from './forms/directives/radio_control_value_accessor'; +import {Type} from '@angular/core'; + +/** + * Shorthand set of providers used for building Angular forms. + * + * ### Example + * + * ```typescript + * bootstrap(MyApp, [FORM_PROVIDERS]); + * ``` + * + * @experimental + */ +export const FORM_PROVIDERS: Type[] = /*@ts2dart_const*/[FormBuilder, RadioControlRegistry]; diff --git a/modules/@angular/common/src/forms/directives.ts b/modules/@angular/common/src/forms/directives.ts new file mode 100644 index 0000000000..6f370f5796 --- /dev/null +++ b/modules/@angular/common/src/forms/directives.ts @@ -0,0 +1,99 @@ +import {Type} from '@angular/core'; +import {NgControlName} from './directives/ng_control_name'; +import {NgFormControl} from './directives/ng_form_control'; +import {NgModel} from './directives/ng_model'; +import {NgControlGroup} from './directives/ng_control_group'; +import {NgFormModel} from './directives/ng_form_model'; +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, + NgSelectOption +} from './directives/select_control_value_accessor'; +import { + SelectMultipleControlValueAccessor, + NgSelectMultipleOption +} from './directives/select_multiple_control_value_accessor'; +import { + RequiredValidator, + MinLengthValidator, + MaxLengthValidator, + PatternValidator +} from './directives/validators'; + +export {NgControlName} from './directives/ng_control_name'; +export {NgFormControl} from './directives/ng_form_control'; +export {NgModel} from './directives/ng_model'; +export {NgControlGroup} from './directives/ng_control_group'; +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 { + SelectControlValueAccessor, + NgSelectOption +} from './directives/select_control_value_accessor'; +export { + SelectMultipleControlValueAccessor, + NgSelectMultipleOption +} from './directives/select_multiple_control_value_accessor'; +export { + RequiredValidator, + MinLengthValidator, + MaxLengthValidator, + PatternValidator +} from './directives/validators'; +export {NgControl} from './directives/ng_control'; +export {ControlValueAccessor} from './directives/control_value_accessor'; + +/** + * + * A list of all the form directives used as part of a `@Component` annotation. + * + * This is a shorthand for importing them each individually. + * + * ### Example + * + * ```typescript + * @Component({ + * selector: 'my-app', + * directives: [FORM_DIRECTIVES] + * }) + * class MyApp {} + * ``` + * @experimental + */ +export const FORM_DIRECTIVES: Type[] = /*@ts2dart_const*/[ + NgControlName, + NgControlGroup, + + NgFormControl, + NgModel, + NgFormModel, + NgForm, + + NgSelectOption, + NgSelectMultipleOption, + DefaultValueAccessor, + NumberValueAccessor, + CheckboxControlValueAccessor, + SelectControlValueAccessor, + SelectMultipleControlValueAccessor, + RadioControlValueAccessor, + NgControlStatus, + + RequiredValidator, + MinLengthValidator, + MaxLengthValidator, + PatternValidator +]; diff --git a/modules/@angular/common/src/forms/directives/abstract_control_directive.ts b/modules/@angular/common/src/forms/directives/abstract_control_directive.ts new file mode 100644 index 0000000000..584d1cb976 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/abstract_control_directive.ts @@ -0,0 +1,32 @@ +import {AbstractControl} from '../model'; +import {isPresent} from '../../facade/lang'; +import {unimplemented} from '../../facade/exceptions'; + +/** + * Base class for control directives. + * + * Only used internally in the forms module. + * + * @experimental + */ +export abstract class AbstractControlDirective { + get control(): AbstractControl { return unimplemented(); } + + get value(): any { return isPresent(this.control) ? this.control.value : null; } + + get valid(): boolean { return isPresent(this.control) ? this.control.valid : null; } + + get errors(): {[key: string]: any} { + return isPresent(this.control) ? this.control.errors : null; + } + + get pristine(): boolean { return isPresent(this.control) ? this.control.pristine : null; } + + get dirty(): boolean { return isPresent(this.control) ? this.control.dirty : null; } + + get touched(): boolean { return isPresent(this.control) ? this.control.touched : null; } + + get untouched(): boolean { return isPresent(this.control) ? this.control.untouched : null; } + + get path(): string[] { return null; } +} diff --git a/modules/@angular/common/src/forms/directives/checkbox_value_accessor.ts b/modules/@angular/common/src/forms/directives/checkbox_value_accessor.ts new file mode 100644 index 0000000000..15a1c9b238 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/checkbox_value_accessor.ts @@ -0,0 +1,37 @@ +import {Directive, Renderer, ElementRef, forwardRef} from '@angular/core'; +import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor'; + +export const CHECKBOX_VALUE_ACCESSOR: any = /*@ts2dart_const*/ { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CheckboxControlValueAccessor), + multi: true +}; + +/** + * The accessor for writing a value and listening to changes on a checkbox input element. + * + * ### Example + * ``` + * + * ``` + * + * @experimental + */ +@Directive({ + selector: + 'input[type=checkbox][ngControl],input[type=checkbox][ngFormControl],input[type=checkbox][ngModel]', + host: {'(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()'}, + providers: [CHECKBOX_VALUE_ACCESSOR] +}) +export class CheckboxControlValueAccessor implements ControlValueAccessor { + onChange = (_: any) => {}; + onTouched = () => {}; + + constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} + + writeValue(value: any): void { + this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', value); + } + registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; } + registerOnTouched(fn: () => {}): void { this.onTouched = fn; } +} diff --git a/modules/@angular/common/src/forms/directives/control_container.ts b/modules/@angular/common/src/forms/directives/control_container.ts new file mode 100644 index 0000000000..e321b51aa8 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/control_container.ts @@ -0,0 +1,23 @@ +import {Form} from './form_interface'; +import {AbstractControlDirective} from './abstract_control_directive'; + +/** + * A directive that contains multiple {@link NgControl}s. + * + * Only used by the forms module. + * + * @experimental + */ +export class ControlContainer extends AbstractControlDirective { + name: string; + + /** + * Get the form to which this container belongs. + */ + get formDirective(): Form { return null; } + + /** + * Get the path to this container. + */ + get path(): string[] { return null; } +} diff --git a/modules/@angular/common/src/forms/directives/control_value_accessor.ts b/modules/@angular/common/src/forms/directives/control_value_accessor.ts new file mode 100644 index 0000000000..f080268e77 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/control_value_accessor.ts @@ -0,0 +1,37 @@ +import {OpaqueToken} from '@angular/core'; + +/** + * A bridge between a control and a native element. + * + * A `ControlValueAccessor` abstracts the operations of writing a new value to a + * DOM element representing an input control. + * + * Please see {@link DefaultValueAccessor} for more information. + * + * @experimental + */ +export interface ControlValueAccessor { + /** + * Write a new value to the element. + */ + writeValue(obj: any): void; + + /** + * Set the function to be called when the control receives a change event. + */ + registerOnChange(fn: any): void; + + /** + * Set the function to be called when the control receives a touch event. + */ + registerOnTouched(fn: any): void; +} + +/** + * Used to provide a {@link ControlValueAccessor} for form controls. + * + * See {@link DefaultValueAccessor} for how to implement one. + * @experimental + */ +export const NG_VALUE_ACCESSOR: OpaqueToken = + /*@ts2dart_const*/ new OpaqueToken("NgValueAccessor"); diff --git a/modules/@angular/common/src/forms/directives/default_value_accessor.ts b/modules/@angular/common/src/forms/directives/default_value_accessor.ts new file mode 100644 index 0000000000..e8a962cc66 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/default_value_accessor.ts @@ -0,0 +1,45 @@ +import {Directive, ElementRef, Renderer, forwardRef} from '@angular/core'; +import {isBlank} from '../../facade/lang'; +import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor'; + +export const DEFAULT_VALUE_ACCESSOR: any = /*@ts2dart_const*/ + /* @ts2dart_Provider */ { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultValueAccessor), + multi: true + }; + +/** + * The default accessor for writing a value and listening to changes that is used by the + * {@link NgModel}, {@link NgFormControl}, and {@link NgControlName} directives. + * + * ### Example + * ``` + * + * ``` + * + * @experimental + */ +@Directive({ + selector: + 'input:not([type=checkbox])[ngControl],textarea[ngControl],input:not([type=checkbox])[ngFormControl],textarea[ngFormControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]', + // TODO: vsavkin replace the above selector with the one below it once + // https://github.com/angular/angular/issues/3011 is implemented + // selector: '[ngControl],[ngModel],[ngFormControl]', + host: {'(input)': 'onChange($event.target.value)', '(blur)': 'onTouched()'}, + providers: [DEFAULT_VALUE_ACCESSOR] +}) +export class DefaultValueAccessor implements ControlValueAccessor { + onChange = (_: any) => {}; + onTouched = () => {}; + + constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} + + writeValue(value: any): void { + var normalizedValue = isBlank(value) ? '' : value; + this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue); + } + + registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } + registerOnTouched(fn: () => void): void { this.onTouched = fn; } +} diff --git a/modules/@angular/common/src/forms/directives/form_interface.ts b/modules/@angular/common/src/forms/directives/form_interface.ts new file mode 100644 index 0000000000..f1092f46b2 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/form_interface.ts @@ -0,0 +1,47 @@ +import {NgControl} from './ng_control'; +import {NgControlGroup} from './ng_control_group'; +import {Control, ControlGroup} from '../model'; + +/** + * An interface that {@link NgFormModel} and {@link NgForm} implement. + * + * Only used by the forms module. + * + * @experimental + */ +export interface Form { + /** + * Add a control to this form. + */ + addControl(dir: NgControl): void; + + /** + * Remove a control from this form. + */ + removeControl(dir: NgControl): void; + + /** + * Look up the {@link Control} associated with a particular {@link NgControl}. + */ + getControl(dir: NgControl): Control; + + /** + * Add a group of controls to this form. + */ + addControlGroup(dir: NgControlGroup): void; + + /** + * Remove a group of controls from this form. + */ + removeControlGroup(dir: NgControlGroup): void; + + /** + * Look up the {@link ControlGroup} associated with a particular {@link NgControlGroup}. + */ + getControlGroup(dir: NgControlGroup): ControlGroup; + + /** + * Update the model for a particular control with a new value. + */ + updateModel(dir: NgControl, value: any): void; +} diff --git a/modules/@angular/common/src/forms/directives/ng_control.ts b/modules/@angular/common/src/forms/directives/ng_control.ts new file mode 100644 index 0000000000..f96af30570 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/ng_control.ts @@ -0,0 +1,23 @@ +import {unimplemented} from '../../facade/exceptions'; + +import {ControlValueAccessor} from './control_value_accessor'; +import {AbstractControlDirective} from './abstract_control_directive'; +import {AsyncValidatorFn, ValidatorFn} from './validators'; + +/** + * A base class that all control directive extend. + * It binds a {@link Control} object to a DOM element. + * + * Used internally by Angular forms. + * + * @experimental + */ +export abstract class NgControl extends AbstractControlDirective { + name: string = null; + valueAccessor: ControlValueAccessor = null; + + get validator(): ValidatorFn { return unimplemented(); } + get asyncValidator(): AsyncValidatorFn { return unimplemented(); } + + abstract viewToModelUpdate(newValue: any): void; +} diff --git a/modules/@angular/common/src/forms/directives/ng_control_group.ts b/modules/@angular/common/src/forms/directives/ng_control_group.ts new file mode 100644 index 0000000000..5b80b72555 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/ng_control_group.ts @@ -0,0 +1,111 @@ +import { + OnInit, + OnDestroy, + Directive, + Optional, + Inject, + Host, + SkipSelf, + forwardRef, + Self +} from '@angular/core'; +import {ControlContainer} from './control_container'; +import {controlPath, composeValidators, composeAsyncValidators} from './shared'; +import {ControlGroup} from '../model'; +import {Form} from './form_interface'; +import {NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; +import {AsyncValidatorFn, ValidatorFn} from './validators'; + +export const controlGroupProvider: any = + /*@ts2dart_const*/ /* @ts2dart_Provider */ { + provide: ControlContainer, + useExisting: forwardRef(() => NgControlGroup) + }; + +/** + * Creates and binds a control group to a DOM element. + * + * This directive can only be used as a child of {@link NgForm} or {@link NgFormModel}. + * + * ### Example ([live demo](http://plnkr.co/edit/7EJ11uGeaggViYM6T5nq?p=preview)) + * + * ```typescript + * @Component({ + * selector: 'my-app', + * template: ` + *
+ *

Angular Control & ControlGroup Example

+ *
+ *
+ *

Enter your name:

+ *

First:

+ *

Middle:

+ *

Last:

+ *
+ *

Name value:

+ *
{{valueOf(cgName)}}
+ *

Name is {{cgName?.control?.valid ? "valid" : "invalid"}}

+ *

What's your favorite food?

+ *

+ *

Form value

+ *
{{valueOf(f)}}
+ *
+ *
+ * ` + * }) + * export class App { + * valueOf(cg: NgControlGroup): string { + * if (cg.control == null) { + * return null; + * } + * return JSON.stringify(cg.control.value, null, 2); + * } + * } + * ``` + * + * This example declares a control group for a user's name. The value and validation state of + * this group can be accessed separately from the overall form. + * + * @experimental + */ +@Directive({ + selector: '[ngControlGroup]', + providers: [controlGroupProvider], + inputs: ['name: ngControlGroup'], + exportAs: 'ngForm' +}) +export class NgControlGroup extends ControlContainer implements OnInit, + OnDestroy { + /** @internal */ + _parent: ControlContainer; + + constructor(@Host() @SkipSelf() parent: ControlContainer, + @Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[], + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[]) { + super(); + this._parent = parent; + } + + ngOnInit(): void { this.formDirective.addControlGroup(this); } + + ngOnDestroy(): void { this.formDirective.removeControlGroup(this); } + + /** + * Get the {@link ControlGroup} backing this binding. + */ + get control(): ControlGroup { return this.formDirective.getControlGroup(this); } + + /** + * Get the path to this control group. + */ + get path(): string[] { return controlPath(this.name, this._parent); } + + /** + * Get the {@link Form} to which this group belongs. + */ + get formDirective(): Form { return this._parent.formDirective; } + + get validator(): ValidatorFn { return composeValidators(this._validators); } + + get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); } +} diff --git a/modules/@angular/common/src/forms/directives/ng_control_name.ts b/modules/@angular/common/src/forms/directives/ng_control_name.ts new file mode 100644 index 0000000000..48a481bc2b --- /dev/null +++ b/modules/@angular/common/src/forms/directives/ng_control_name.ts @@ -0,0 +1,146 @@ +import { + OnChanges, + OnDestroy, + SimpleChanges, + Directive, + forwardRef, + Host, + SkipSelf, + Inject, + Optional, + Self +} from '@angular/core'; + +import {EventEmitter, ObservableWrapper} from '../../facade/async'; +import {ControlContainer} from './control_container'; +import {NgControl} from './ng_control'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; +import { + controlPath, + composeValidators, + composeAsyncValidators, + isPropertyUpdated, + selectValueAccessor +} from './shared'; +import {Control} from '../model'; +import {NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; +import {ValidatorFn, AsyncValidatorFn} from './validators'; + + +export const controlNameBinding: any = + /*@ts2dart_const*/ /* @ts2dart_Provider */ { + provide: NgControl, + useExisting: forwardRef(() => NgControlName) + }; + +/** + * Creates and binds a control with a specified name to a DOM element. + * + * This directive can only be used as a child of {@link NgForm} or {@link NgFormModel}. + + * ### Example + * + * In this example, we create the login and password controls. + * We can work with each control separately: check its validity, get its value, listen to its + * changes. + * + * ``` + * @Component({ + * selector: "login-comp", + * directives: [FORM_DIRECTIVES], + * template: ` + *
+ * Login + *
Login is invalid
+ * + * Password + * + *
+ * `}) + * class LoginComp { + * onLogIn(value): void { + * // value === {login: 'some login', password: 'some password'} + * } + * } + * ``` + * + * We can also use ngModel to bind a domain model to the form. + * + * ``` + * @Component({ + * selector: "login-comp", + * directives: [FORM_DIRECTIVES], + * template: ` + *
+ * Login + * Password + * + *
+ * `}) + * class LoginComp { + * credentials: {login:string, password:string}; + * + * onLogIn(): void { + * // this.credentials.login === "some login" + * // this.credentials.password === "some password" + * } + * } + * ``` + * + * @experimental + */ +@Directive({ + selector: '[ngControl]', + providers: [controlNameBinding], + inputs: ['name: ngControl', 'model: ngModel'], + outputs: ['update: ngModelChange'], + exportAs: 'ngForm' +}) +export class NgControlName extends NgControl implements OnChanges, + OnDestroy { + /** @internal */ + update = new EventEmitter(); + model: any; + viewModel: any; + private _added = false; + + constructor(@Host() @SkipSelf() private _parent: ControlContainer, + @Optional() @Self() @Inject(NG_VALIDATORS) private _validators: + /* Array */ any[], + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: + /* Array */ any[], + @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) + valueAccessors: ControlValueAccessor[]) { + super(); + this.valueAccessor = selectValueAccessor(this, valueAccessors); + } + + ngOnChanges(changes: SimpleChanges) { + if (!this._added) { + this.formDirective.addControl(this); + this._added = true; + } + if (isPropertyUpdated(changes, this.viewModel)) { + this.viewModel = this.model; + this.formDirective.updateModel(this, this.model); + } + } + + ngOnDestroy(): void { this.formDirective.removeControl(this); } + + viewToModelUpdate(newValue: any): void { + this.viewModel = newValue; + ObservableWrapper.callEmit(this.update, newValue); + } + + get path(): string[] { return controlPath(this.name, this._parent); } + + get formDirective(): any { return this._parent.formDirective; } + + get validator(): ValidatorFn { return composeValidators(this._validators); } + + get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); } + + get control(): Control { return this.formDirective.getControl(this); } +} diff --git a/modules/@angular/common/src/forms/directives/ng_control_status.ts b/modules/@angular/common/src/forms/directives/ng_control_status.ts new file mode 100644 index 0000000000..5f29cc6adb --- /dev/null +++ b/modules/@angular/common/src/forms/directives/ng_control_status.ts @@ -0,0 +1,45 @@ +import {Directive, Self} from '@angular/core'; +import {NgControl} from './ng_control'; +import {isPresent} from '../../facade/lang'; + +/** + * Directive automatically applied to Angular forms that sets CSS classes + * based on control status (valid/invalid/dirty/etc). + * + * @experimental + */ +@Directive({ + selector: '[ngControl],[ngModel],[ngFormControl]', + host: { + '[class.ng-untouched]': 'ngClassUntouched', + '[class.ng-touched]': 'ngClassTouched', + '[class.ng-pristine]': 'ngClassPristine', + '[class.ng-dirty]': 'ngClassDirty', + '[class.ng-valid]': 'ngClassValid', + '[class.ng-invalid]': 'ngClassInvalid' + } +}) +export class NgControlStatus { + private _cd: NgControl; + + constructor(@Self() cd: NgControl) { this._cd = cd; } + + get ngClassUntouched(): boolean { + return isPresent(this._cd.control) ? this._cd.control.untouched : false; + } + get ngClassTouched(): boolean { + return isPresent(this._cd.control) ? this._cd.control.touched : false; + } + get ngClassPristine(): boolean { + return isPresent(this._cd.control) ? this._cd.control.pristine : false; + } + get ngClassDirty(): boolean { + return isPresent(this._cd.control) ? this._cd.control.dirty : false; + } + get ngClassValid(): boolean { + return isPresent(this._cd.control) ? this._cd.control.valid : false; + } + get ngClassInvalid(): boolean { + return isPresent(this._cd.control) ? !this._cd.control.valid : false; + } +} diff --git a/modules/@angular/common/src/forms/directives/ng_form.ts b/modules/@angular/common/src/forms/directives/ng_form.ts new file mode 100644 index 0000000000..dfd20788c7 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/ng_form.ts @@ -0,0 +1,175 @@ +import {Directive, forwardRef, Optional, Inject, Self} from '@angular/core'; +import { + PromiseWrapper, + ObservableWrapper, + EventEmitter +} from '../../facade/async'; +import {ListWrapper} from '../../facade/collection'; +import {isPresent} from '../../facade/lang'; +import {NgControl} from './ng_control'; +import {Form} from './form_interface'; +import {NgControlGroup} from './ng_control_group'; +import {ControlContainer} from './control_container'; +import {AbstractControl, ControlGroup, Control} from '../model'; +import {setUpControl, setUpControlGroup, composeValidators, composeAsyncValidators} from './shared'; +import {NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; + +export const formDirectiveProvider: any = + /*@ts2dart_const*/ {provide: ControlContainer, useExisting: forwardRef(() => NgForm)}; + +/** + * If `NgForm` is bound in a component, `
` elements in that component will be + * upgraded to use the Angular form system. + * + * ### Typical Use + * + * Include `FORM_DIRECTIVES` in the `directives` section of a {@link View} annotation + * to use `NgForm` and its associated controls. + * + * ### Structure + * + * An Angular form is a collection of `Control`s in some hierarchy. + * `Control`s can be at the top level or can be organized in `ControlGroup`s + * or `ControlArray`s. This hierarchy is reflected in the form's `value`, a + * JSON object that mirrors the form structure. + * + * ### Submission + * + * The `ngSubmit` event signals when the user triggers a form submission. + * + * ### Example ([live demo](http://plnkr.co/edit/ltdgYj4P0iY64AR71EpL?p=preview)) + * + * ```typescript + * @Component({ + * selector: 'my-app', + * template: ` + *
+ *

Submit the form to see the data object Angular builds

+ *

NgForm demo

+ * + *

Control group: credentials

+ *
+ *

Login:

+ *

Password:

+ *
+ *

Control group: person

+ *
+ *

First name:

+ *

Last name:

+ *
+ * + *

Form data submitted:

+ * + *
{{data}}
+ *
+ * `, + * directives: [CORE_DIRECTIVES, FORM_DIRECTIVES] + * }) + * export class App { + * constructor() {} + * + * data: string; + * + * onSubmit(data) { + * this.data = JSON.stringify(data, null, 2); + * } + * } + * ``` + * + * @experimental + */ +@Directive({ + selector: 'form:not([ngNoForm]):not([ngFormModel]),ngForm,[ngForm]', + providers: [formDirectiveProvider], + host: { + '(submit)': 'onSubmit()', + }, + outputs: ['ngSubmit'], + exportAs: 'ngForm' +}) +export class NgForm extends ControlContainer implements Form { + private _submitted: boolean = false; + + form: ControlGroup; + ngSubmit = new EventEmitter(); + + constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: any[], + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) { + super(); + this.form = new ControlGroup({}, null, composeValidators(validators), + composeAsyncValidators(asyncValidators)); + } + + get submitted(): boolean { return this._submitted; } + + get formDirective(): Form { return this; } + + get control(): ControlGroup { return this.form; } + + get path(): string[] { return []; } + + get controls(): {[key: string]: AbstractControl} { return this.form.controls; } + + addControl(dir: NgControl): void { + PromiseWrapper.scheduleMicrotask(() => { + var container = this._findContainer(dir.path); + var ctrl = new Control(); + setUpControl(ctrl, dir); + container.registerControl(dir.name, ctrl); + ctrl.updateValueAndValidity({emitEvent: false}); + }); + } + + getControl(dir: NgControl): Control { return this.form.find(dir.path); } + + removeControl(dir: NgControl): void { + PromiseWrapper.scheduleMicrotask(() => { + var container = this._findContainer(dir.path); + if (isPresent(container)) { + container.removeControl(dir.name); + } + }); + } + + addControlGroup(dir: NgControlGroup): void { + PromiseWrapper.scheduleMicrotask(() => { + var container = this._findContainer(dir.path); + var group = new ControlGroup({}); + setUpControlGroup(group, dir); + container.registerControl(dir.name, group); + group.updateValueAndValidity({emitEvent: false}); + }); + } + + removeControlGroup(dir: NgControlGroup): void { + PromiseWrapper.scheduleMicrotask(() => { + var container = this._findContainer(dir.path); + if (isPresent(container)) { + container.removeControl(dir.name); + } + }); + } + + getControlGroup(dir: NgControlGroup): ControlGroup { + return this.form.find(dir.path); + } + + updateModel(dir: NgControl, value: any): void { + PromiseWrapper.scheduleMicrotask(() => { + var ctrl = this.form.find(dir.path); + ctrl.updateValue(value); + }); + } + + onSubmit(): boolean { + this._submitted = true; + ObservableWrapper.callEmit(this.ngSubmit, null); + return false; + } + + /** @internal */ + _findContainer(path: string[]): ControlGroup { + path.pop(); + return ListWrapper.isEmpty(path) ? this.form : this.form.find(path); + } +} diff --git a/modules/@angular/common/src/forms/directives/ng_form_control.ts b/modules/@angular/common/src/forms/directives/ng_form_control.ts new file mode 100644 index 0000000000..17b9885079 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/ng_form_control.ts @@ -0,0 +1,132 @@ +import { + OnChanges, + SimpleChanges, + Directive, + forwardRef, + Inject, + Optional, + Self +} from '@angular/core'; + +import {StringMapWrapper} from '../../facade/collection'; +import {EventEmitter, ObservableWrapper} from '../../facade/async'; + +import {NgControl} from './ng_control'; +import {Control} from '../model'; +import {NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; +import { + setUpControl, + composeValidators, + composeAsyncValidators, + isPropertyUpdated, + selectValueAccessor +} from './shared'; +import {ValidatorFn, AsyncValidatorFn} from './validators'; + +export const formControlBinding: any = + /*@ts2dart_const*/ /* @ts2dart_Provider */ { + provide: NgControl, + useExisting: forwardRef(() => NgFormControl) + }; + +/** + * Binds an existing {@link Control} to a DOM element. + * + * ### Example ([live demo](http://plnkr.co/edit/jcQlZ2tTh22BZZ2ucNAT?p=preview)) + * + * In this example, we bind the control to an input element. When the value of the input element + * changes, the value of the control will reflect that change. Likewise, if the value of the + * control changes, the input element reflects that change. + * + * ```typescript + * @Component({ + * selector: 'my-app', + * template: ` + *
+ *

NgFormControl Example

+ *
+ *

Element with existing control:

+ *

Value of existing control: {{loginControl.value}}

+ *
+ *
+ * `, + * directives: [CORE_DIRECTIVES, FORM_DIRECTIVES] + * }) + * export class App { + * loginControl: Control = new Control(''); + * } + * ``` + * + * ### ngModel + * + * We can also use `ngModel` to bind a domain model to the form. + * + * ### Example ([live demo](http://plnkr.co/edit/yHMLuHO7DNgT8XvtjTDH?p=preview)) + * + * ```typescript + * @Component({ + * selector: "login-comp", + * directives: [FORM_DIRECTIVES], + * template: "" + * }) + * class LoginComp { + * loginControl: Control = new Control(''); + * login:string; + * } + * ``` + * + * @experimental + */ +@Directive({ + selector: '[ngFormControl]', + providers: [formControlBinding], + inputs: ['form: ngFormControl', 'model: ngModel'], + outputs: ['update: ngModelChange'], + exportAs: 'ngForm' +}) +export class NgFormControl extends NgControl implements OnChanges { + form: Control; + update = new EventEmitter(); + model: any; + viewModel: any; + + constructor(@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: + /* Array */ any[], + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: + /* Array */ any[], + @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) + valueAccessors: ControlValueAccessor[]) { + super(); + this.valueAccessor = selectValueAccessor(this, valueAccessors); + } + + ngOnChanges(changes: SimpleChanges): void { + if (this._isControlChanged(changes)) { + setUpControl(this.form, this); + this.form.updateValueAndValidity({emitEvent: false}); + } + if (isPropertyUpdated(changes, this.viewModel)) { + this.form.updateValue(this.model); + this.viewModel = this.model; + } + } + + get path(): string[] { return []; } + + get validator(): ValidatorFn { return composeValidators(this._validators); } + + get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); } + + get control(): Control { return this.form; } + + viewToModelUpdate(newValue: any): void { + this.viewModel = newValue; + ObservableWrapper.callEmit(this.update, newValue); + } + + private _isControlChanged(changes: {[key: string]: any}): boolean { + return StringMapWrapper.contains(changes, "form"); + } +} diff --git a/modules/@angular/common/src/forms/directives/ng_form_model.ts b/modules/@angular/common/src/forms/directives/ng_form_model.ts new file mode 100644 index 0000000000..9b68180830 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/ng_form_model.ts @@ -0,0 +1,194 @@ +import { + SimpleChanges, + OnChanges, + Directive, + forwardRef, + Inject, + Optional, + Self +} from '@angular/core'; +import {isBlank} from '../../facade/lang'; +import {ListWrapper, StringMapWrapper} from '../../facade/collection'; +import {BaseException} from '../../facade/exceptions'; +import {ObservableWrapper, EventEmitter} from '../../facade/async'; +import {NgControl} from './ng_control'; +import {NgControlGroup} from './ng_control_group'; +import {ControlContainer} from './control_container'; +import {Form} from './form_interface'; +import {Control, ControlGroup} from '../model'; +import {setUpControl, setUpControlGroup, composeValidators, composeAsyncValidators} from './shared'; +import {Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; + +export const formDirectiveProvider: any = + /*@ts2dart_const*/ /* @ts2dart_Provider */ { + provide: ControlContainer, + useExisting: forwardRef(() => NgFormModel) + }; + +/** + * Binds an existing control group to a DOM element. + * + * ### Example ([live demo](http://plnkr.co/edit/jqrVirudY8anJxTMUjTP?p=preview)) + * + * In this example, we bind the control group to the form element, and we bind the login and + * password controls to the login and password elements. + * + * ```typescript + * @Component({ + * selector: 'my-app', + * template: ` + *
+ *

NgFormModel Example

+ *
+ *

Login:

+ *

Password:

+ *
+ *

Value:

+ *
{{value}}
+ *
+ * `, + * directives: [FORM_DIRECTIVES] + * }) + * export class App { + * loginForm: ControlGroup; + * + * constructor() { + * this.loginForm = new ControlGroup({ + * login: new Control(""), + * password: new Control("") + * }); + * } + * + * get value(): string { + * return JSON.stringify(this.loginForm.value, null, 2); + * } + * } + * ``` + * + * We can also use ngModel to bind a domain model to the form. + * + * ```typescript + * @Component({ + * selector: "login-comp", + * directives: [FORM_DIRECTIVES], + * template: ` + *
+ * Login + * Password + * + *
` + * }) + * class LoginComp { + * credentials: {login: string, password: string}; + * loginForm: ControlGroup; + * + * constructor() { + * this.loginForm = new ControlGroup({ + * login: new Control(""), + * password: new Control("") + * }); + * } + * + * onLogin(): void { + * // this.credentials.login === 'some login' + * // this.credentials.password === 'some password' + * } + * } + * ``` + * + * @experimental + */ +@Directive({ + selector: '[ngFormModel]', + providers: [formDirectiveProvider], + inputs: ['form: ngFormModel'], + host: {'(submit)': 'onSubmit()'}, + outputs: ['ngSubmit'], + exportAs: 'ngForm' +}) +export class NgFormModel extends ControlContainer implements Form, + OnChanges { + private _submitted: boolean = false; + + form: ControlGroup = null; + directives: NgControl[] = []; + ngSubmit = new EventEmitter(); + + constructor(@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[], + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[]) { + super(); + } + + ngOnChanges(changes: SimpleChanges): void { + this._checkFormPresent(); + if (StringMapWrapper.contains(changes, "form")) { + var sync = composeValidators(this._validators); + this.form.validator = Validators.compose([this.form.validator, sync]); + + var async = composeAsyncValidators(this._asyncValidators); + this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]); + + this.form.updateValueAndValidity({onlySelf: true, emitEvent: false}); + } + + this._updateDomValue(); + } + + get submitted(): boolean { return this._submitted; } + + get formDirective(): Form { return this; } + + get control(): ControlGroup { return this.form; } + + get path(): string[] { return []; } + + addControl(dir: NgControl): void { + var ctrl: any = this.form.find(dir.path); + setUpControl(ctrl, dir); + ctrl.updateValueAndValidity({emitEvent: false}); + this.directives.push(dir); + } + + getControl(dir: NgControl): Control { return this.form.find(dir.path); } + + removeControl(dir: NgControl): void { ListWrapper.remove(this.directives, dir); } + + addControlGroup(dir: NgControlGroup) { + var ctrl: any = this.form.find(dir.path); + setUpControlGroup(ctrl, dir); + ctrl.updateValueAndValidity({emitEvent: false}); + } + + removeControlGroup(dir: NgControlGroup) {} + + getControlGroup(dir: NgControlGroup): ControlGroup { + return this.form.find(dir.path); + } + + updateModel(dir: NgControl, value: any): void { + var ctrl  = this.form.find(dir.path); + ctrl.updateValue(value); + } + + onSubmit(): boolean { + this._submitted = true; + ObservableWrapper.callEmit(this.ngSubmit, null); + return false; + } + + /** @internal */ + _updateDomValue() { + this.directives.forEach(dir => { + var ctrl: any = this.form.find(dir.path); + dir.valueAccessor.writeValue(ctrl.value); + }); + } + + private _checkFormPresent() { + if (isBlank(this.form)) { + throw new BaseException( + `ngFormModel expects a form. Please pass one in. Example:
`); + } + } +} diff --git a/modules/@angular/common/src/forms/directives/ng_model.ts b/modules/@angular/common/src/forms/directives/ng_model.ts new file mode 100644 index 0000000000..ae84f28871 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/ng_model.ts @@ -0,0 +1,102 @@ +import { + OnChanges, + SimpleChanges, + Directive, + forwardRef, + Inject, + Optional, + Self +} from '@angular/core'; +import {EventEmitter, ObservableWrapper} from '../../facade/async'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; +import {NgControl} from './ng_control'; +import {Control} from '../model'; +import {NG_VALIDATORS, NG_ASYNC_VALIDATORS} from '../validators'; +import { + setUpControl, + isPropertyUpdated, + selectValueAccessor, + composeValidators, + composeAsyncValidators +} from './shared'; +import {ValidatorFn, AsyncValidatorFn} from './validators'; + +export const formControlBinding: any = + /*@ts2dart_const*/ /* @ts2dart_Provider */ { + provide: NgControl, + useExisting: forwardRef(() => NgModel) + }; + +/** + * Binds a domain model to a form control. + * + * ### Usage + * + * `ngModel` binds an existing domain model to a form control. For a + * two-way binding, use `[(ngModel)]` to ensure the model updates in + * both directions. + * + * ### Example ([live demo](http://plnkr.co/edit/R3UX5qDaUqFO2VYR0UzH?p=preview)) + * ```typescript + * @Component({ + * selector: "search-comp", + * directives: [FORM_DIRECTIVES], + * template: `` + * }) + * class SearchComp { + * searchQuery: string; + * } + * ``` + * + * @experimental + */ +@Directive({ + selector: '[ngModel]:not([ngControl]):not([ngFormControl])', + providers: [formControlBinding], + inputs: ['model: ngModel'], + outputs: ['update: ngModelChange'], + exportAs: 'ngForm' +}) +export class NgModel extends NgControl implements OnChanges { + /** @internal */ + _control = new Control(); + /** @internal */ + _added = false; + update = new EventEmitter(); + model: any; + viewModel: any; + + constructor(@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[], + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[], + @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) + valueAccessors: ControlValueAccessor[]) { + super(); + this.valueAccessor = selectValueAccessor(this, valueAccessors); + } + + ngOnChanges(changes: SimpleChanges) { + if (!this._added) { + setUpControl(this._control, this); + this._control.updateValueAndValidity({emitEvent: false}); + this._added = true; + } + + if (isPropertyUpdated(changes, this.viewModel)) { + this._control.updateValue(this.model); + this.viewModel = this.model; + } + } + + get control(): Control { return this._control; } + + get path(): string[] { return []; } + + get validator(): ValidatorFn { return composeValidators(this._validators); } + + get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); } + + viewToModelUpdate(newValue: any): void { + this.viewModel = newValue; + ObservableWrapper.callEmit(this.update, newValue); + } +} diff --git a/modules/@angular/common/src/forms/directives/normalize_validator.dart b/modules/@angular/common/src/forms/directives/normalize_validator.dart new file mode 100644 index 0000000000..510abb3001 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/normalize_validator.dart @@ -0,0 +1,20 @@ +library angular2.core.forms.normalize_validators; + +import 'package:angular2/src/common/forms/directives/validators.dart' show Validator; + +Function normalizeValidator(dynamic validator){ + if (validator is Validator) { + return (c) => validator.validate(c); + } else { + return validator; + } +} + + +Function normalizeAsyncValidator(dynamic validator){ + if (validator is Validator) { + return (c) => validator.validate(c); + } else { + return validator; + } +} diff --git a/modules/@angular/common/src/forms/directives/normalize_validator.ts b/modules/@angular/common/src/forms/directives/normalize_validator.ts new file mode 100644 index 0000000000..c956c8d4ae --- /dev/null +++ b/modules/@angular/common/src/forms/directives/normalize_validator.ts @@ -0,0 +1,18 @@ +import {AbstractControl} from '../model'; +import {Validator, ValidatorFn, AsyncValidatorFn} from './validators'; + +export function normalizeValidator(validator: ValidatorFn | Validator): ValidatorFn { + if ((validator).validate !== undefined) { + return (c: AbstractControl) => (validator).validate(c); + } else { + return validator; + } +} + +export function normalizeAsyncValidator(validator: AsyncValidatorFn | Validator): AsyncValidatorFn { + if ((validator).validate !== undefined) { + return (c: AbstractControl) => Promise.resolve((validator).validate(c)); + } else { + return validator; + } +} diff --git a/modules/@angular/common/src/forms/directives/number_value_accessor.ts b/modules/@angular/common/src/forms/directives/number_value_accessor.ts new file mode 100644 index 0000000000..ca84ed50af --- /dev/null +++ b/modules/@angular/common/src/forms/directives/number_value_accessor.ts @@ -0,0 +1,44 @@ +import {Directive, ElementRef, Renderer, forwardRef} from '@angular/core'; +import {NumberWrapper} from '../../facade/lang'; +import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor'; + +export const NUMBER_VALUE_ACCESSOR: any = /*@ts2dart_const*/ /*@ts2dart_Provider*/ { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NumberValueAccessor), + multi: true +}; + +/** + * The accessor for writing a number value and listening to changes that is used by the + * {@link NgModel}, {@link NgFormControl}, and {@link NgControlName} directives. + * + * ### Example + * ``` + * + * ``` + */ +@Directive({ + selector: + 'input[type=number][ngControl],input[type=number][ngFormControl],input[type=number][ngModel]', + host: { + '(change)': 'onChange($event.target.value)', + '(input)': 'onChange($event.target.value)', + '(blur)': 'onTouched()' + }, + providers: [NUMBER_VALUE_ACCESSOR] +}) +export class NumberValueAccessor implements ControlValueAccessor { + onChange = (_: any) => {}; + onTouched = () => {}; + + constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} + + writeValue(value: number): void { + this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', value); + } + + registerOnChange(fn: (_: number) => void): void { + this.onChange = (value) => { fn(value == '' ? null : NumberWrapper.parseFloat(value)); }; + } + registerOnTouched(fn: () => void): void { this.onTouched = fn; } +} diff --git a/modules/@angular/common/src/forms/directives/radio_control_value_accessor.ts b/modules/@angular/common/src/forms/directives/radio_control_value_accessor.ts new file mode 100644 index 0000000000..f87656f195 --- /dev/null +++ b/modules/@angular/common/src/forms/directives/radio_control_value_accessor.ts @@ -0,0 +1,133 @@ +import { + Directive, + ElementRef, + Renderer, + forwardRef, + Input, + OnInit, + OnDestroy, + Injector, + Injectable +} from '@angular/core'; +import {isPresent} from '../../facade/lang'; +import {ListWrapper} from '../../facade/collection'; +import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor'; +import {NgControl} from './ng_control'; + +export const RADIO_VALUE_ACCESSOR: any = /*@ts2dart_const*/ /*@ts2dart_Provider*/ { + provide: 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 (this._isSameGroup(c, accessor) && c[1] !== accessor) { + c[1].fireUncheck(); + } + }); + } + + private _isSameGroup(controlPair:[NgControl, RadioControlValueAccessor], + accessor: RadioControlValueAccessor) { + return controlPair[0].control.root === accessor._control.control.root && + controlPair[1].name === accessor.name; + } +} + +/** + * 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 + * {@link NgModel}, {@link NgFormControl}, and {@link NgControlName} directives. + * + * ### Example + * ``` + * @Component({ + * template: ` + * + * + * ` + * }) + * 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 { + /** @internal */ + _state: RadioButtonState; + /** @internal */ + _control: NgControl; + @Input() name: string; + /** @internal */ + _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; } +} diff --git a/modules/@angular/common/src/forms/directives/select_control_value_accessor.ts b/modules/@angular/common/src/forms/directives/select_control_value_accessor.ts new file mode 100644 index 0000000000..4ba6d0061b --- /dev/null +++ b/modules/@angular/common/src/forms/directives/select_control_value_accessor.ts @@ -0,0 +1,145 @@ +import { + Directive, + Renderer, + forwardRef, + ElementRef, + Input, + Host, + OnDestroy, + Optional +} from '@angular/core'; +import { + StringWrapper, + isPrimitive, + isPresent, + isBlank, + looseIdentical +} from '../../facade/lang'; +import {MapWrapper} from '../../facade/collection'; + +import {NG_VALUE_ACCESSOR, ControlValueAccessor} from './control_value_accessor'; + +export const SELECT_VALUE_ACCESSOR: any = /*@ts2dart_const*/ /*@ts2dart_Provider*/ { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SelectControlValueAccessor), + multi: true +}; + +function _buildValueString(id: string, value: any): string { + if (isBlank(id)) return `${value}`; + if (!isPrimitive(value)) value = "Object"; + return StringWrapper.slice(`${id}: ${value}`, 0, 50); +} + +function _extractId(valueString: string): string { + return valueString.split(":")[0]; +} + +/** + * The accessor for writing a value and listening to changes on a select element. + * + * Note: We have to listen to the 'change' event because 'input' events aren't fired + * for selects in Firefox and IE: + * https://bugzilla.mozilla.org/show_bug.cgi?id=1024350 + * https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4660045/ + * + * @experimental + */ +@Directive({ + selector: + 'select:not([multiple])[ngControl],select:not([multiple])[ngFormControl],select:not([multiple])[ngModel]', + host: {'(change)': 'onChange($event.target.value)', '(blur)': 'onTouched()'}, + providers: [SELECT_VALUE_ACCESSOR] +}) +export class SelectControlValueAccessor implements ControlValueAccessor { + value: any; + /** @internal */ + _optionMap: Map = new Map(); + /** @internal */ + _idCounter: number = 0; + + onChange = (_: any) => {}; + onTouched = () => {}; + + constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} + + writeValue(value: any): void { + this.value = value; + var valueString = _buildValueString(this._getOptionId(value), value); + this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', valueString); + } + + registerOnChange(fn: (value: any) => any): void { + this.onChange = (valueString: string) => { + this.value = valueString; + fn(this._getOptionValue(valueString)); + }; + } + registerOnTouched(fn: () => any): void { this.onTouched = fn; } + + /** @internal */ + _registerOption(): string { return (this._idCounter++).toString(); } + + /** @internal */ + _getOptionId(value: any): string { + for (let id of MapWrapper.keys(this._optionMap)) { + if (looseIdentical(this._optionMap.get(id), value)) return id; + } + return null; + } + + /** @internal */ + _getOptionValue(valueString: string): any { + let value = this._optionMap.get(_extractId(valueString)); + return isPresent(value) ? value : valueString; + } +} + +/** + * Marks `