refactor(core): move more modules into core
This commit is contained in:
53
modules/angular2/src/core/forms/directives.ts
Normal file
53
modules/angular2/src/core/forms/directives.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import {Type, CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
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 {
|
||||
SelectControlValueAccessor,
|
||||
NgSelectOption
|
||||
} from './directives/select_control_value_accessor';
|
||||
import {DefaultValidators} from './directives/validators';
|
||||
|
||||
export {NgControlName} from './directives/ng_control_name';
|
||||
export {NgFormControl} from './directives/ng_form_control';
|
||||
export {NgModel} from './directives/ng_model';
|
||||
export {NgControl} from './directives/ng_control';
|
||||
export {NgControlGroup} from './directives/ng_control_group';
|
||||
export {NgFormModel} from './directives/ng_form_model';
|
||||
export {NgForm} from './directives/ng_form';
|
||||
export {ControlValueAccessor} from './directives/control_value_accessor';
|
||||
export {DefaultValueAccessor} from './directives/default_value_accessor';
|
||||
export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
|
||||
export {
|
||||
SelectControlValueAccessor,
|
||||
NgSelectOption
|
||||
} from './directives/select_control_value_accessor';
|
||||
export {DefaultValidators} from './directives/validators';
|
||||
|
||||
/**
|
||||
*
|
||||
* A list of all the form directives used as part of a `@View` annotation.
|
||||
*
|
||||
* This is a shorthand for importing them each individually.
|
||||
*/
|
||||
export const FORM_DIRECTIVES: Type[] = CONST_EXPR([
|
||||
NgControlName,
|
||||
NgControlGroup,
|
||||
|
||||
NgFormControl,
|
||||
NgModel,
|
||||
NgFormModel,
|
||||
NgForm,
|
||||
|
||||
NgSelectOption,
|
||||
DefaultValueAccessor,
|
||||
CheckboxControlValueAccessor,
|
||||
SelectControlValueAccessor,
|
||||
|
||||
DefaultValidators
|
||||
]);
|
@ -0,0 +1,19 @@
|
||||
import {AbstractControl} from '../model';
|
||||
|
||||
export class AbstractControlDirective {
|
||||
get control(): AbstractControl { return null; }
|
||||
|
||||
get value(): any { return this.control.value; }
|
||||
|
||||
get valid(): boolean { return this.control.valid; }
|
||||
|
||||
get errors(): StringMap<string, any> { return this.control.errors; }
|
||||
|
||||
get pristine(): boolean { return this.control.pristine; }
|
||||
|
||||
get dirty(): boolean { return this.control.dirty; }
|
||||
|
||||
get touched(): boolean { return this.control.touched; }
|
||||
|
||||
get untouched(): boolean { return this.control.untouched; }
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import {Renderer} from 'angular2/render';
|
||||
import {Directive} from 'angular2/metadata';
|
||||
import {ElementRef} from 'angular2/core';
|
||||
import {Self} from 'angular2/di';
|
||||
|
||||
import {NgControl} from './ng_control';
|
||||
import {ControlValueAccessor} from './control_value_accessor';
|
||||
import {isPresent} from 'angular2/src/core/facade/lang';
|
||||
import {setProperty} from './shared';
|
||||
|
||||
/**
|
||||
* The accessor for writing a value and listening to changes on a checkbox input element.
|
||||
*
|
||||
* # Example
|
||||
* ```
|
||||
* <input type="checkbox" [ng-control]="rememberLogin">
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector:
|
||||
'input[type=checkbox][ng-control],input[type=checkbox][ng-form-control],input[type=checkbox][ng-model]',
|
||||
host: {
|
||||
'(change)': 'onChange($event.target.checked)',
|
||||
'(blur)': 'onTouched()',
|
||||
'[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 CheckboxControlValueAccessor implements ControlValueAccessor {
|
||||
private _cd: NgControl;
|
||||
onChange = (_) => {};
|
||||
onTouched = () => {};
|
||||
|
||||
constructor(@Self() cd: NgControl, private _renderer: Renderer, private _elementRef: ElementRef) {
|
||||
this._cd = cd;
|
||||
cd.valueAccessor = this;
|
||||
}
|
||||
|
||||
writeValue(value: any) { setProperty(this._renderer, this._elementRef, "checked", value); }
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; }
|
||||
registerOnTouched(fn: () => {}): void { this.onTouched = fn; }
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import {Form} from './form_interface';
|
||||
import {AbstractControlDirective} from './abstract_control_directive';
|
||||
|
||||
/**
|
||||
* A directive that contains a group of [NgControl].
|
||||
*
|
||||
* Only used by the forms module.
|
||||
*/
|
||||
export class ControlContainer extends AbstractControlDirective {
|
||||
name: string;
|
||||
get formDirective(): Form { return null; }
|
||||
get path(): string[] { return null; }
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* A bridge between a control and a native element.
|
||||
*
|
||||
* Please see {@link DefaultValueAccessor} for more information.
|
||||
*/
|
||||
export interface ControlValueAccessor {
|
||||
writeValue(obj: any): void;
|
||||
registerOnChange(fn: any): void;
|
||||
registerOnTouched(fn: any): void;
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import {Renderer} from 'angular2/render';
|
||||
import {Directive} from 'angular2/metadata';
|
||||
import {ElementRef} from 'angular2/core';
|
||||
import {Self} from 'angular2/di';
|
||||
import {NgControl} from './ng_control';
|
||||
import {ControlValueAccessor} from './control_value_accessor';
|
||||
import {isBlank, isPresent} from 'angular2/src/core/facade/lang';
|
||||
import {setProperty} from './shared';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* ```
|
||||
* <input type="text" [(ng-model)]="searchQuery">
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector:
|
||||
'input:not([type=checkbox])[ng-control],textarea[ng-control],input:not([type=checkbox])[ng-form-control],textarea[ng-form-control],input:not([type=checkbox])[ng-model],textarea[ng-model]',
|
||||
host: {
|
||||
'(change)': 'onChange($event.target.value)',
|
||||
'(input)': 'onChange($event.target.value)',
|
||||
'(blur)': 'onTouched()',
|
||||
'[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 DefaultValueAccessor implements ControlValueAccessor {
|
||||
private _cd: NgControl;
|
||||
onChange = (_) => {};
|
||||
onTouched = () => {};
|
||||
|
||||
constructor(@Self() cd: NgControl, private _renderer: Renderer, private _elementRef: ElementRef) {
|
||||
this._cd = cd;
|
||||
cd.valueAccessor = this;
|
||||
}
|
||||
|
||||
writeValue(value: any) {
|
||||
// both this.value and setProperty are required at the moment
|
||||
// remove when a proper imperative API is provided
|
||||
var normalizedValue = isBlank(value) ? '' : value;
|
||||
setProperty(this._renderer, this._elementRef, 'value', normalizedValue);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
|
||||
|
||||
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
|
||||
}
|
18
modules/angular2/src/core/forms/directives/form_interface.ts
Normal file
18
modules/angular2/src/core/forms/directives/form_interface.ts
Normal file
@ -0,0 +1,18 @@
|
||||
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.
|
||||
*/
|
||||
export interface Form {
|
||||
addControl(dir: NgControl): void;
|
||||
removeControl(dir: NgControl): void;
|
||||
getControl(dir: NgControl): Control;
|
||||
addControlGroup(dir: NgControlGroup): void;
|
||||
removeControlGroup(dir: NgControlGroup): void;
|
||||
getControlGroup(dir: NgControlGroup): ControlGroup;
|
||||
updateModel(dir: NgControl, value: any): void;
|
||||
}
|
17
modules/angular2/src/core/forms/directives/ng_control.ts
Normal file
17
modules/angular2/src/core/forms/directives/ng_control.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {ControlValueAccessor} from './control_value_accessor';
|
||||
import {AbstractControlDirective} from './abstract_control_directive';
|
||||
|
||||
/**
|
||||
* An abstract class that all control directive extend.
|
||||
*
|
||||
* It binds a {@link Control} object to a DOM element.
|
||||
*/
|
||||
export class NgControl extends AbstractControlDirective {
|
||||
name: string = null;
|
||||
valueAccessor: ControlValueAccessor = null;
|
||||
|
||||
get validator(): Function { return null; }
|
||||
get path(): string[] { return null; }
|
||||
|
||||
viewToModelUpdate(newValue: any): void {}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import {Directive} from 'angular2/metadata';
|
||||
import {OnInit, OnDestroy} from 'angular2/lifecycle_hooks';
|
||||
import {Inject, Host, SkipSelf, forwardRef, Binding} from 'angular2/di';
|
||||
import {ListWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
|
||||
import {ControlContainer} from './control_container';
|
||||
import {controlPath} from './shared';
|
||||
import {ControlGroup} from '../model';
|
||||
import {Form} from './form_interface';
|
||||
|
||||
const controlGroupBinding =
|
||||
CONST_EXPR(new Binding(ControlContainer, {toAlias: 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
|
||||
*
|
||||
* In this example, we create the credentials and personal control groups.
|
||||
* We can work with each group separately: check its validity, get its value, listen to its changes.
|
||||
*
|
||||
* ```
|
||||
* @Component({selector: "signup-comp"})
|
||||
* @View({
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: `
|
||||
* <form #f="form" (submit)='onSignUp(f.value)'>
|
||||
* <div ng-control-group='credentials' #credentials="form">
|
||||
* Login <input type='text' ng-control='login'>
|
||||
* Password <input type='password' ng-control='password'>
|
||||
* </div>
|
||||
* <div *ng-if="!credentials.valid">Credentials are invalid</div>
|
||||
*
|
||||
* <div ng-control-group='personal'>
|
||||
* Name <input type='text' ng-control='name'>
|
||||
* </div>
|
||||
* <button type='submit'>Sign Up!</button>
|
||||
* </form>
|
||||
* `})
|
||||
* class SignupComp {
|
||||
* onSignUp(value) {
|
||||
* // value === {personal: {name: 'some name'},
|
||||
* // credentials: {login: 'some login', password: 'some password'}}
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ng-control-group]',
|
||||
bindings: [controlGroupBinding],
|
||||
properties: ['name: ng-control-group'],
|
||||
exportAs: 'form'
|
||||
})
|
||||
export class NgControlGroup extends ControlContainer implements OnInit,
|
||||
OnDestroy {
|
||||
_parent: ControlContainer;
|
||||
constructor(@Host() @SkipSelf() _parent: ControlContainer) {
|
||||
super();
|
||||
this._parent = _parent;
|
||||
}
|
||||
|
||||
onInit() { this.formDirective.addControlGroup(this); }
|
||||
|
||||
onDestroy() { this.formDirective.removeControlGroup(this); }
|
||||
|
||||
get control(): ControlGroup { return this.formDirective.getControlGroup(this); }
|
||||
|
||||
get path(): string[] { return controlPath(this.name, this._parent); }
|
||||
|
||||
get formDirective(): Form { return this._parent.formDirective; }
|
||||
}
|
123
modules/angular2/src/core/forms/directives/ng_control_name.ts
Normal file
123
modules/angular2/src/core/forms/directives/ng_control_name.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
import {EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
|
||||
import {StringMap} from 'angular2/src/core/facade/collection';
|
||||
|
||||
import {Query, Directive} from 'angular2/metadata';
|
||||
import {forwardRef, Host, SkipSelf, Binding, Inject, Optional} from 'angular2/di';
|
||||
import {OnChanges, OnDestroy} from 'angular2/lifecycle_hooks';
|
||||
|
||||
import {ControlContainer} from './control_container';
|
||||
import {NgControl} from './ng_control';
|
||||
import {controlPath, isPropertyUpdated} from './shared';
|
||||
import {Control} from '../model';
|
||||
import {Validators, NG_VALIDATORS} from '../validators';
|
||||
|
||||
|
||||
const controlNameBinding =
|
||||
CONST_EXPR(new Binding(NgControl, {toAlias: 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"})
|
||||
* @View({
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: `
|
||||
* <form #f="form" (submit)='onLogIn(f.value)'>
|
||||
* Login <input type='text' ng-control='login' #l="form">
|
||||
* <div *ng-if="!l.valid">Login is invalid</div>
|
||||
*
|
||||
* Password <input type='password' ng-control='password'>
|
||||
|
||||
* <button type='submit'>Log in!</button>
|
||||
* </form>
|
||||
* `})
|
||||
* class LoginComp {
|
||||
* onLogIn(value) {
|
||||
* // value === {login: 'some login', password: 'some password'}
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* We can also use ng-model to bind a domain model to the form.
|
||||
*
|
||||
* ```
|
||||
* @Component({selector: "login-comp"})
|
||||
* @View({
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: `
|
||||
* <form (submit)='onLogIn()'>
|
||||
* Login <input type='text' ng-control='login' [(ng-model)]="credentials.login">
|
||||
* Password <input type='password' ng-control='password'
|
||||
[(ng-model)]="credentials.password">
|
||||
* <button type='submit'>Log in!</button>
|
||||
* </form>
|
||||
* `})
|
||||
* class LoginComp {
|
||||
* credentials: {login:string, password:string};
|
||||
*
|
||||
* onLogIn() {
|
||||
* // this.credentials.login === "some login"
|
||||
* // this.credentials.password === "some password"
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ng-control]',
|
||||
bindings: [controlNameBinding],
|
||||
properties: ['name: ngControl', 'model: ngModel'],
|
||||
events: ['update: ngModel'],
|
||||
exportAs: 'form'
|
||||
})
|
||||
export class NgControlName extends NgControl implements OnChanges,
|
||||
OnDestroy {
|
||||
_parent: ControlContainer;
|
||||
update = new EventEmitter();
|
||||
model: any;
|
||||
viewModel: any;
|
||||
validators: Function[];
|
||||
_added = false;
|
||||
|
||||
constructor(@Host() @SkipSelf() parent: ControlContainer,
|
||||
@Optional() @Inject(NG_VALIDATORS) validators: Function[]) {
|
||||
super();
|
||||
this._parent = parent;
|
||||
this.validators = validators;
|
||||
}
|
||||
|
||||
onChanges(c: StringMap<string, any>) {
|
||||
if (!this._added) {
|
||||
this.formDirective.addControl(this);
|
||||
this._added = true;
|
||||
}
|
||||
if (isPropertyUpdated(c, this.viewModel)) {
|
||||
this.viewModel = this.model;
|
||||
this.formDirective.updateModel(this, this.model);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() { this.formDirective.removeControl(this); }
|
||||
|
||||
viewToModelUpdate(newValue: any): void {
|
||||
this.viewModel = newValue;
|
||||
ObservableWrapper.callNext(this.update, newValue);
|
||||
}
|
||||
|
||||
get path(): string[] { return controlPath(this.name, this._parent); }
|
||||
|
||||
get formDirective(): any { return this._parent.formDirective; }
|
||||
|
||||
get control(): Control { return this.formDirective.getControl(this); }
|
||||
|
||||
get validator(): Function { return Validators.compose(this.validators); }
|
||||
}
|
146
modules/angular2/src/core/forms/directives/ng_form.ts
Normal file
146
modules/angular2/src/core/forms/directives/ng_form.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import {
|
||||
PromiseWrapper,
|
||||
ObservableWrapper,
|
||||
EventEmitter,
|
||||
PromiseCompleter
|
||||
} from 'angular2/src/core/facade/async';
|
||||
import {StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {isPresent, isBlank, CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
import {Directive} from 'angular2/metadata';
|
||||
import {forwardRef, Binding} from 'angular2/di';
|
||||
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} from './shared';
|
||||
|
||||
const formDirectiveBinding =
|
||||
CONST_EXPR(new Binding(ControlContainer, {toAlias: forwardRef(() => NgForm)}));
|
||||
|
||||
/**
|
||||
* Creates and binds a form object to a DOM element.
|
||||
*
|
||||
* # Example
|
||||
*
|
||||
* ```
|
||||
* @Component({selector: "signup-comp"})
|
||||
* @View({
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: `
|
||||
* <form #f="form" (submit)='onSignUp(f.value)'>
|
||||
* <div ng-control-group='credentials' #credentials="form">
|
||||
* Login <input type='text' ng-control='login'>
|
||||
* Password <input type='password' ng-control='password'>
|
||||
* </div>
|
||||
* <div *ng-if="!credentials.valid">Credentials are invalid</div>
|
||||
*
|
||||
* <div ng-control-group='personal'>
|
||||
* Name <input type='text' ng-control='name'>
|
||||
* </div>
|
||||
* <button type='submit'>Sign Up!</button>
|
||||
* </form>
|
||||
* `})
|
||||
* class SignupComp {
|
||||
* onSignUp(value) {
|
||||
* // value === {personal: {name: 'some name'},
|
||||
* // credentials: {login: 'some login', password: 'some password'}}
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'form:not([ng-no-form]):not([ng-form-model]),ng-form,[ng-form]',
|
||||
bindings: [formDirectiveBinding],
|
||||
host: {
|
||||
'(submit)': 'onSubmit()',
|
||||
},
|
||||
events: ['ngSubmit'],
|
||||
exportAs: 'form'
|
||||
})
|
||||
export class NgForm extends ControlContainer implements Form {
|
||||
form: ControlGroup;
|
||||
ngSubmit = new EventEmitter();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.form = new ControlGroup({});
|
||||
}
|
||||
|
||||
get formDirective(): Form { return this; }
|
||||
|
||||
get control(): ControlGroup { return this.form; }
|
||||
|
||||
get path(): string[] { return []; }
|
||||
|
||||
get controls(): StringMap<string, AbstractControl> { return this.form.controls; }
|
||||
|
||||
addControl(dir: NgControl): void {
|
||||
this._later(_ => {
|
||||
var container = this._findContainer(dir.path);
|
||||
var c = new Control();
|
||||
setUpControl(c, dir);
|
||||
container.addControl(dir.name, c);
|
||||
c.updateValidity();
|
||||
});
|
||||
}
|
||||
|
||||
getControl(dir: NgControl): Control { return <Control>this.form.find(dir.path); }
|
||||
|
||||
removeControl(dir: NgControl): void {
|
||||
this._later(_ => {
|
||||
var container = this._findContainer(dir.path);
|
||||
if (isPresent(container)) {
|
||||
container.removeControl(dir.name);
|
||||
container.updateValidity();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addControlGroup(dir: NgControlGroup): void {
|
||||
this._later(_ => {
|
||||
var container = this._findContainer(dir.path);
|
||||
var c = new ControlGroup({});
|
||||
container.addControl(dir.name, c);
|
||||
c.updateValidity();
|
||||
});
|
||||
}
|
||||
|
||||
removeControlGroup(dir: NgControlGroup): void {
|
||||
this._later(_ => {
|
||||
var container = this._findContainer(dir.path);
|
||||
if (isPresent(container)) {
|
||||
container.removeControl(dir.name);
|
||||
container.updateValidity();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getControlGroup(dir: NgControlGroup): ControlGroup {
|
||||
return <ControlGroup>this.form.find(dir.path);
|
||||
}
|
||||
|
||||
updateModel(dir: NgControl, value: any): void {
|
||||
this._later(_ => {
|
||||
var c = <Control>this.form.find(dir.path);
|
||||
c.updateValue(value);
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
ObservableWrapper.callNext(this.ngSubmit, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
_findContainer(path: string[]): ControlGroup {
|
||||
ListWrapper.removeLast(path);
|
||||
return ListWrapper.isEmpty(path) ? this.form : <ControlGroup>this.form.find(path);
|
||||
}
|
||||
|
||||
_later(fn) {
|
||||
var c: PromiseCompleter<any> = PromiseWrapper.completer();
|
||||
PromiseWrapper.then(c.promise, fn, (_) => {});
|
||||
c.resolve(null);
|
||||
}
|
||||
}
|
102
modules/angular2/src/core/forms/directives/ng_form_control.ts
Normal file
102
modules/angular2/src/core/forms/directives/ng_form_control.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
import {EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
|
||||
|
||||
import {Query, Directive} from 'angular2/metadata';
|
||||
import {forwardRef, Binding, Inject, Optional} from 'angular2/di';
|
||||
import {OnChanges} from 'angular2/lifecycle_hooks';
|
||||
|
||||
import {NgControl} from './ng_control';
|
||||
import {Control} from '../model';
|
||||
import {Validators, NG_VALIDATORS} from '../validators';
|
||||
import {setUpControl, isPropertyUpdated} from './shared';
|
||||
|
||||
const formControlBinding =
|
||||
CONST_EXPR(new Binding(NgControl, {toAlias: forwardRef(() => NgFormControl)}));
|
||||
|
||||
/**
|
||||
* Binds an existing control to a DOM element.
|
||||
*
|
||||
* # Example
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* ```
|
||||
* @Component({selector: "login-comp"})
|
||||
* @View({
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: "<input type='text' [ng-form-control]='loginControl'>"
|
||||
* })
|
||||
* class LoginComp {
|
||||
* loginControl:Control;
|
||||
*
|
||||
* constructor() {
|
||||
* this.loginControl = new Control('');
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* We can also use ng-model to bind a domain model to the form.
|
||||
*
|
||||
* ```
|
||||
* @Component({selector: "login-comp"})
|
||||
* @View({
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: "<input type='text' [ng-form-control]='loginControl' [(ng-model)]='login'>"
|
||||
* })
|
||||
* class LoginComp {
|
||||
* loginControl:Control;
|
||||
* login:string;
|
||||
*
|
||||
* constructor() {
|
||||
* this.loginControl = new Control('');
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ng-form-control]',
|
||||
bindings: [formControlBinding],
|
||||
properties: ['form: ngFormControl', 'model: ngModel'],
|
||||
events: ['update: ngModel'],
|
||||
exportAs: 'form'
|
||||
})
|
||||
export class NgFormControl extends NgControl implements OnChanges {
|
||||
form: Control;
|
||||
update = new EventEmitter();
|
||||
_added = false;
|
||||
model: any;
|
||||
viewModel: any;
|
||||
validators: Function[];
|
||||
|
||||
constructor(@Optional() @Inject(NG_VALIDATORS) validators: Function[]) {
|
||||
super();
|
||||
this.validators = validators;
|
||||
}
|
||||
|
||||
onChanges(c: StringMap<string, any>) {
|
||||
if (!this._added) {
|
||||
setUpControl(this.form, this);
|
||||
this.form.updateValidity();
|
||||
this._added = true;
|
||||
}
|
||||
if (isPropertyUpdated(c, this.viewModel)) {
|
||||
this.form.updateValue(this.model);
|
||||
}
|
||||
}
|
||||
|
||||
get path(): string[] { return []; }
|
||||
|
||||
get control(): Control { return this.form; }
|
||||
|
||||
get validator(): Function { return Validators.compose(this.validators); }
|
||||
|
||||
viewToModelUpdate(newValue: any): void {
|
||||
this.viewModel = newValue;
|
||||
ObservableWrapper.callNext(this.update, newValue);
|
||||
}
|
||||
}
|
143
modules/angular2/src/core/forms/directives/ng_form_model.ts
Normal file
143
modules/angular2/src/core/forms/directives/ng_form_model.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
import {ListWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {ObservableWrapper, EventEmitter} from 'angular2/src/core/facade/async';
|
||||
|
||||
import {Directive} from 'angular2/metadata';
|
||||
import {OnChanges} from 'angular2/lifecycle_hooks';
|
||||
import {forwardRef, Binding} from 'angular2/di';
|
||||
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} from './shared';
|
||||
|
||||
const formDirectiveBinding =
|
||||
CONST_EXPR(new Binding(ControlContainer, {toAlias: forwardRef(() => NgFormModel)}));
|
||||
|
||||
/**
|
||||
* Binds an existing control group to a DOM element.
|
||||
*
|
||||
* # Example
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* ```
|
||||
* @Component({selector: "login-comp"})
|
||||
* @View({
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: "<form [ng-form-model]='loginForm'>" +
|
||||
* "Login <input type='text' ng-control='login'>" +
|
||||
* "Password <input type='password' ng-control='password'>" +
|
||||
* "<button (click)="onLogin()">Login</button>" +
|
||||
* "</form>"
|
||||
* })
|
||||
* class LoginComp {
|
||||
* loginForm:ControlGroup;
|
||||
*
|
||||
* constructor() {
|
||||
* this.loginForm = new ControlGroup({
|
||||
* login: new Control(""),
|
||||
* password: new Control("")
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* onLogin() {
|
||||
* // this.loginForm.value
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* We can also use ng-model to bind a domain model to the form.
|
||||
*
|
||||
* ```
|
||||
* @Component({selector: "login-comp"})
|
||||
* @View({
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: "<form [ng-form-model]='loginForm'>" +
|
||||
* "Login <input type='text' ng-control='login' [(ng-model)]='login'>" +
|
||||
* "Password <input type='password' ng-control='password' [(ng-model)]='password'>" +
|
||||
* "<button (click)="onLogin()">Login</button>" +
|
||||
* "</form>"
|
||||
* })
|
||||
* class LoginComp {
|
||||
* credentials:{login:string, password:string}
|
||||
* loginForm:ControlGroup;
|
||||
*
|
||||
* constructor() {
|
||||
* this.loginForm = new ControlGroup({
|
||||
* login: new Control(""),
|
||||
* password: new Control("")
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* onLogin() {
|
||||
* // this.credentials.login === 'some login'
|
||||
* // this.credentials.password === 'some password'
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ng-form-model]',
|
||||
bindings: [formDirectiveBinding],
|
||||
properties: ['form: ng-form-model'],
|
||||
host: {
|
||||
'(submit)': 'onSubmit()',
|
||||
},
|
||||
events: ['ngSubmit'],
|
||||
exportAs: 'form'
|
||||
})
|
||||
export class NgFormModel extends ControlContainer implements Form,
|
||||
OnChanges {
|
||||
form: ControlGroup = null;
|
||||
directives: NgControl[] = [];
|
||||
ngSubmit = new EventEmitter();
|
||||
|
||||
onChanges(_) { this._updateDomValue(); }
|
||||
|
||||
get formDirective(): Form { return this; }
|
||||
|
||||
get control(): ControlGroup { return this.form; }
|
||||
|
||||
get path(): string[] { return []; }
|
||||
|
||||
addControl(dir: NgControl): void {
|
||||
var c: any = this.form.find(dir.path);
|
||||
setUpControl(c, dir);
|
||||
c.updateValidity();
|
||||
this.directives.push(dir);
|
||||
}
|
||||
|
||||
getControl(dir: NgControl): Control { return <Control>this.form.find(dir.path); }
|
||||
|
||||
removeControl(dir: NgControl): void { ListWrapper.remove(this.directives, dir); }
|
||||
|
||||
addControlGroup(dir: NgControlGroup) {}
|
||||
|
||||
removeControlGroup(dir: NgControlGroup) {}
|
||||
|
||||
getControlGroup(dir: NgControlGroup): ControlGroup {
|
||||
return <ControlGroup>this.form.find(dir.path);
|
||||
}
|
||||
|
||||
updateModel(dir: NgControl, value: any): void {
|
||||
var c = <Control>this.form.find(dir.path);
|
||||
c.updateValue(value);
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
ObservableWrapper.callNext(this.ngSubmit, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
_updateDomValue() {
|
||||
ListWrapper.forEach(this.directives, dir => {
|
||||
var c: any = this.form.find(dir.path);
|
||||
dir.valueAccessor.writeValue(c.value);
|
||||
});
|
||||
}
|
||||
}
|
73
modules/angular2/src/core/forms/directives/ng_model.ts
Normal file
73
modules/angular2/src/core/forms/directives/ng_model.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
import {EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
|
||||
|
||||
import {Query, Directive} from 'angular2/metadata';
|
||||
import {forwardRef, Binding, Inject, Optional} from 'angular2/di';
|
||||
import {OnChanges} from 'angular2/lifecycle_hooks';
|
||||
|
||||
import {NgControl} from './ng_control';
|
||||
import {Control} from '../model';
|
||||
import {Validators, NG_VALIDATORS} from '../validators';
|
||||
import {setUpControl, isPropertyUpdated} from './shared';
|
||||
|
||||
const formControlBinding = CONST_EXPR(new Binding(NgControl, {toAlias: forwardRef(() => NgModel)}));
|
||||
|
||||
/**
|
||||
* Binds a domain model to the form.
|
||||
*
|
||||
* # Example
|
||||
* ```
|
||||
* @Component({selector: "search-comp"})
|
||||
* @View({
|
||||
* directives: [FORM_DIRECTIVES],
|
||||
* template: `
|
||||
<input type='text' [(ng-model)]="searchQuery">
|
||||
* `})
|
||||
* class SearchComp {
|
||||
* searchQuery: string;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[ng-model]:not([ng-control]):not([ng-form-control])',
|
||||
bindings: [formControlBinding],
|
||||
properties: ['model: ngModel'],
|
||||
events: ['update: ngModel'],
|
||||
exportAs: 'form'
|
||||
})
|
||||
export class NgModel extends NgControl implements OnChanges {
|
||||
_control = new Control();
|
||||
_added = false;
|
||||
update = new EventEmitter();
|
||||
model: any;
|
||||
viewModel: any;
|
||||
validators: Function[];
|
||||
|
||||
constructor(@Optional() @Inject(NG_VALIDATORS) validators: Function[]) {
|
||||
super();
|
||||
this.validators = validators;
|
||||
}
|
||||
|
||||
onChanges(c: StringMap<string, any>) {
|
||||
if (!this._added) {
|
||||
setUpControl(this._control, this);
|
||||
this._control.updateValidity();
|
||||
this._added = true;
|
||||
}
|
||||
|
||||
if (isPropertyUpdated(c, this.viewModel)) {
|
||||
this._control.updateValue(this.model);
|
||||
}
|
||||
}
|
||||
|
||||
get control(): Control { return this._control; }
|
||||
|
||||
get path(): string[] { return []; }
|
||||
|
||||
get validator(): Function { return Validators.compose(this.validators); }
|
||||
|
||||
viewToModelUpdate(newValue: any): void {
|
||||
this.viewModel = newValue;
|
||||
ObservableWrapper.callNext(this.update, newValue);
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import {Renderer} from 'angular2/render';
|
||||
import {ElementRef, QueryList} from 'angular2/core';
|
||||
import {Self} from 'angular2/di';
|
||||
import {Query, Directive} from 'angular2/metadata';
|
||||
|
||||
import {NgControl} from './ng_control';
|
||||
import {ControlValueAccessor} from './control_value_accessor';
|
||||
import {isPresent} from 'angular2/src/core/facade/lang';
|
||||
import {setProperty} from './shared';
|
||||
|
||||
/**
|
||||
* Marks <option> as dynamic, so Angular can be notified when options change.
|
||||
*
|
||||
* #Example:
|
||||
*
|
||||
* ```
|
||||
* <select ng-control="city">
|
||||
* <option *ng-for="#c of cities" [value]="c"></option>
|
||||
* </select>
|
||||
* ```
|
||||
*/
|
||||
@Directive({selector: 'option'})
|
||||
export class NgSelectOption {
|
||||
}
|
||||
|
||||
/**
|
||||
* The accessor for writing a value and listening to changes on a select element.
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'select[ng-control],select[ng-form-control],select[ng-model]',
|
||||
host: {
|
||||
'(change)': 'onChange($event.target.value)',
|
||||
'(input)': 'onChange($event.target.value)',
|
||||
'(blur)': 'onTouched()',
|
||||
'[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 SelectControlValueAccessor implements ControlValueAccessor {
|
||||
private _cd: NgControl;
|
||||
value: string;
|
||||
onChange = (_) => {};
|
||||
onTouched = () => {};
|
||||
|
||||
constructor(@Self() cd: NgControl, private _renderer: Renderer, private _elementRef: ElementRef,
|
||||
@Query(NgSelectOption, {descendants: true}) query: QueryList<NgSelectOption>) {
|
||||
this._cd = cd;
|
||||
cd.valueAccessor = this;
|
||||
this._updateValueWhenListOfOptionsChanges(query);
|
||||
}
|
||||
|
||||
writeValue(value: any) {
|
||||
this.value = value;
|
||||
setProperty(this._renderer, this._elementRef, "value", value);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
registerOnChange(fn: () => any): void { this.onChange = fn; }
|
||||
registerOnTouched(fn: () => any): void { this.onTouched = fn; }
|
||||
|
||||
private _updateValueWhenListOfOptionsChanges(query: QueryList<NgSelectOption>) {
|
||||
query.onChange(() => this.writeValue(this.value));
|
||||
}
|
||||
}
|
55
modules/angular2/src/core/forms/directives/shared.ts
Normal file
55
modules/angular2/src/core/forms/directives/shared.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {isBlank, BaseException, looseIdentical} from 'angular2/src/core/facade/lang';
|
||||
|
||||
import {ControlContainer} from './control_container';
|
||||
import {NgControl} from './ng_control';
|
||||
import {Control} from '../model';
|
||||
import {Validators} from '../validators';
|
||||
import {Renderer} from 'angular2/render';
|
||||
import {ElementRef, QueryList} from 'angular2/core';
|
||||
|
||||
|
||||
export function controlPath(name: string, parent: ControlContainer): string[] {
|
||||
var p = ListWrapper.clone(parent.path);
|
||||
p.push(name);
|
||||
return p;
|
||||
}
|
||||
|
||||
export function setUpControl(c: Control, dir: NgControl) {
|
||||
if (isBlank(c)) _throwError(dir, "Cannot find control");
|
||||
if (isBlank(dir.valueAccessor)) _throwError(dir, "No value accessor for");
|
||||
|
||||
c.validator = Validators.compose([c.validator, dir.validator]);
|
||||
dir.valueAccessor.writeValue(c.value);
|
||||
|
||||
// view -> model
|
||||
dir.valueAccessor.registerOnChange(newValue => {
|
||||
dir.viewToModelUpdate(newValue);
|
||||
c.updateValue(newValue, {emitModelToViewChange: false});
|
||||
c.markAsDirty();
|
||||
});
|
||||
|
||||
// model -> view
|
||||
c.registerOnChange(newValue => dir.valueAccessor.writeValue(newValue));
|
||||
|
||||
// touched
|
||||
dir.valueAccessor.registerOnTouched(() => c.markAsTouched());
|
||||
}
|
||||
|
||||
function _throwError(dir: NgControl, message: string): void {
|
||||
var path = ListWrapper.join(dir.path, " -> ");
|
||||
throw new BaseException(`${message} '${path}'`);
|
||||
}
|
||||
|
||||
export function setProperty(renderer: Renderer, elementRef: ElementRef, propName: string,
|
||||
propValue: any) {
|
||||
renderer.setElementProperty(elementRef, propName, propValue);
|
||||
}
|
||||
|
||||
export function isPropertyUpdated(changes: StringMap<string, any>, viewModel: any): boolean {
|
||||
if (!StringMapWrapper.contains(changes, "model")) return false;
|
||||
var change = changes["model"];
|
||||
|
||||
if (change.isFirstChange()) return true;
|
||||
return !looseIdentical(viewModel, change.currentValue);
|
||||
}
|
14
modules/angular2/src/core/forms/directives/validators.ts
Normal file
14
modules/angular2/src/core/forms/directives/validators.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {forwardRef, OpaqueToken, Binding} from 'angular2/di';
|
||||
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
import {Directive} from 'angular2/metadata';
|
||||
import {Validators, NG_VALIDATORS} from '../validators';
|
||||
|
||||
const DEFAULT_VALIDATORS =
|
||||
CONST_EXPR(new Binding(NG_VALIDATORS, {toValue: Validators.required, multi: true}));
|
||||
|
||||
@Directive({
|
||||
selector: '[required][ng-control],[required][ng-form-control],[required][ng-model]',
|
||||
bindings: [DEFAULT_VALIDATORS]
|
||||
})
|
||||
export class DefaultValidators {
|
||||
}
|
126
modules/angular2/src/core/forms/form_builder.ts
Normal file
126
modules/angular2/src/core/forms/form_builder.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import {Injectable} from 'angular2/di';
|
||||
import {StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {isPresent, isArray} from 'angular2/src/core/facade/lang';
|
||||
import * as modelModule from './model';
|
||||
|
||||
|
||||
/**
|
||||
* Creates a form object from a user-specified configuration.
|
||||
*
|
||||
* # Example
|
||||
*
|
||||
* ```
|
||||
* import {Component, View, bootstrap} from 'angular2/angular2';
|
||||
* import {FormBuilder, Validators, FORM_DIRECTIVES, ControlGroup} from 'angular2/forms';
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'login-comp',
|
||||
* viewBindings: [
|
||||
* FormBuilder
|
||||
* ]
|
||||
* })
|
||||
* @View({
|
||||
* template: `
|
||||
* <form [control-group]="loginForm">
|
||||
* Login <input control="login">
|
||||
*
|
||||
* <div control-group="passwordRetry">
|
||||
* Password <input type="password" control="password">
|
||||
* Confirm password <input type="password" control="passwordConfirmation">
|
||||
* </div>
|
||||
* </form>
|
||||
* `,
|
||||
* directives: [
|
||||
* FORM_DIRECTIVES
|
||||
* ]
|
||||
* })
|
||||
* class LoginComp {
|
||||
* loginForm: ControlGroup;
|
||||
*
|
||||
* constructor(builder: FormBuilder) {
|
||||
* this.loginForm = builder.group({
|
||||
* login: ["", Validators.required],
|
||||
*
|
||||
* passwordRetry: builder.group({
|
||||
* password: ["", Validators.required],
|
||||
* passwordConfirmation: ["", Validators.required]
|
||||
* })
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* bootstrap(LoginComp)
|
||||
* ```
|
||||
*
|
||||
* This example creates a {@link ControlGroup} that consists of a `login` {@link Control}, and a
|
||||
* nested
|
||||
* {@link ControlGroup} that defines a `password` and a `passwordConfirmation` {@link Control}:
|
||||
*
|
||||
* ```
|
||||
* var loginForm = builder.group({
|
||||
* login: ["", Validators.required],
|
||||
*
|
||||
* passwordRetry: builder.group({
|
||||
* password: ["", Validators.required],
|
||||
* passwordConfirmation: ["", Validators.required]
|
||||
* })
|
||||
* });
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class FormBuilder {
|
||||
group(controlsConfig: StringMap<string, any>,
|
||||
extra: StringMap<string, any> = null): modelModule.ControlGroup {
|
||||
var controls = this._reduceControls(controlsConfig);
|
||||
var optionals = isPresent(extra) ? StringMapWrapper.get(extra, "optionals") : null;
|
||||
var validator = isPresent(extra) ? StringMapWrapper.get(extra, "validator") : null;
|
||||
|
||||
if (isPresent(validator)) {
|
||||
return new modelModule.ControlGroup(controls, optionals, validator);
|
||||
} else {
|
||||
return new modelModule.ControlGroup(controls, optionals);
|
||||
}
|
||||
}
|
||||
|
||||
control(value: Object, validator: Function = null): modelModule.Control {
|
||||
if (isPresent(validator)) {
|
||||
return new modelModule.Control(value, validator);
|
||||
} else {
|
||||
return new modelModule.Control(value);
|
||||
}
|
||||
}
|
||||
|
||||
array(controlsConfig: any[], validator: Function = null): modelModule.ControlArray {
|
||||
var controls = ListWrapper.map(controlsConfig, (c) => this._createControl(c));
|
||||
if (isPresent(validator)) {
|
||||
return new modelModule.ControlArray(controls, validator);
|
||||
} else {
|
||||
return new modelModule.ControlArray(controls);
|
||||
}
|
||||
}
|
||||
|
||||
_reduceControls(controlsConfig: any): StringMap<string, modelModule.AbstractControl> {
|
||||
var controls = {};
|
||||
StringMapWrapper.forEach(controlsConfig, (controlConfig, controlName) => {
|
||||
controls[controlName] = this._createControl(controlConfig);
|
||||
});
|
||||
return controls;
|
||||
}
|
||||
|
||||
_createControl(controlConfig: any): modelModule.AbstractControl {
|
||||
if (controlConfig instanceof modelModule.Control ||
|
||||
controlConfig instanceof modelModule.ControlGroup ||
|
||||
controlConfig instanceof modelModule.ControlArray) {
|
||||
return controlConfig;
|
||||
|
||||
} else if (isArray(controlConfig)) {
|
||||
var value = controlConfig[0];
|
||||
var validator = controlConfig.length > 1 ? controlConfig[1] : null;
|
||||
return this.control(value, validator);
|
||||
|
||||
} else {
|
||||
return this.control(controlConfig);
|
||||
}
|
||||
}
|
||||
}
|
304
modules/angular2/src/core/forms/model.ts
Normal file
304
modules/angular2/src/core/forms/model.ts
Normal file
@ -0,0 +1,304 @@
|
||||
import {StringWrapper, isPresent, isBlank} from 'angular2/src/core/facade/lang';
|
||||
import {Observable, EventEmitter, ObservableWrapper} from 'angular2/src/core/facade/async';
|
||||
import {StringMap, StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {Validators} from './validators';
|
||||
|
||||
/**
|
||||
* Indicates that a Control is valid, i.e. that no errors exist in the input value.
|
||||
*/
|
||||
export const VALID = "VALID";
|
||||
|
||||
/**
|
||||
* Indicates that a Control is invalid, i.e. that an error exists in the input value.
|
||||
*/
|
||||
export const INVALID = "INVALID";
|
||||
|
||||
export function isControl(c: Object): boolean {
|
||||
return c instanceof AbstractControl;
|
||||
}
|
||||
|
||||
function _find(c: AbstractControl, path: Array<string | number>| string) {
|
||||
if (isBlank(path)) return null;
|
||||
if (!(path instanceof Array)) {
|
||||
path = StringWrapper.split(<string>path, new RegExp("/"));
|
||||
}
|
||||
if (path instanceof Array && ListWrapper.isEmpty(path)) return null;
|
||||
|
||||
return ListWrapper.reduce(<Array<string | number>>path, (v, name) => {
|
||||
if (v instanceof ControlGroup) {
|
||||
return isPresent(v.controls[name]) ? v.controls[name] : null;
|
||||
} else if (v instanceof ControlArray) {
|
||||
var index = <number>name;
|
||||
return isPresent(v.at(index)) ? v.at(index) : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Omitting from external API doc as this is really an abstract internal concept.
|
||||
*/
|
||||
export class AbstractControl {
|
||||
_value: any;
|
||||
_status: string;
|
||||
_errors: StringMap<string, any>;
|
||||
_pristine: boolean;
|
||||
_touched: boolean;
|
||||
_parent: ControlGroup | ControlArray;
|
||||
validator: Function;
|
||||
|
||||
_valueChanges: EventEmitter;
|
||||
|
||||
constructor(validator: Function) {
|
||||
this.validator = validator;
|
||||
this._pristine = true;
|
||||
this._touched = false;
|
||||
}
|
||||
|
||||
get value(): any { return this._value; }
|
||||
|
||||
get status(): string { return this._status; }
|
||||
|
||||
get valid(): boolean { return this._status === VALID; }
|
||||
|
||||
get errors(): StringMap<string, any> { return this._errors; }
|
||||
|
||||
get pristine(): boolean { return this._pristine; }
|
||||
|
||||
get dirty(): boolean { return !this.pristine; }
|
||||
|
||||
get touched(): boolean { return this._touched; }
|
||||
|
||||
get untouched(): boolean { return !this._touched; }
|
||||
|
||||
get valueChanges(): Observable { return this._valueChanges; }
|
||||
|
||||
markAsTouched(): void { this._touched = true; }
|
||||
|
||||
markAsDirty({onlySelf}: {onlySelf?: boolean} = {}): void {
|
||||
onlySelf = isPresent(onlySelf) ? onlySelf : false;
|
||||
|
||||
this._pristine = false;
|
||||
if (isPresent(this._parent) && !onlySelf) {
|
||||
this._parent.markAsDirty({onlySelf: onlySelf});
|
||||
}
|
||||
}
|
||||
|
||||
setParent(parent: ControlGroup | ControlArray) { this._parent = parent; }
|
||||
|
||||
updateValidity({onlySelf}: {onlySelf?: boolean} = {}): void {
|
||||
onlySelf = isPresent(onlySelf) ? onlySelf : false;
|
||||
|
||||
this._errors = this.validator(this);
|
||||
this._status = isPresent(this._errors) ? INVALID : VALID;
|
||||
|
||||
if (isPresent(this._parent) && !onlySelf) {
|
||||
this._parent.updateValidity({onlySelf: onlySelf});
|
||||
}
|
||||
}
|
||||
|
||||
updateValueAndValidity({onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}):
|
||||
void {
|
||||
onlySelf = isPresent(onlySelf) ? onlySelf : false;
|
||||
emitEvent = isPresent(emitEvent) ? emitEvent : true;
|
||||
|
||||
this._updateValue();
|
||||
|
||||
if (emitEvent) {
|
||||
ObservableWrapper.callNext(this._valueChanges, this._value);
|
||||
}
|
||||
|
||||
this._errors = this.validator(this);
|
||||
this._status = isPresent(this._errors) ? INVALID : VALID;
|
||||
if (isPresent(this._parent) && !onlySelf) {
|
||||
this._parent.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
|
||||
}
|
||||
}
|
||||
|
||||
find(path: Array<string | number>| string): AbstractControl { return _find(this, path); }
|
||||
|
||||
getError(errorCode: string, path: string[] = null): any {
|
||||
var c = isPresent(path) && !ListWrapper.isEmpty(path) ? this.find(path) : this;
|
||||
if (isPresent(c) && isPresent(c._errors)) {
|
||||
return StringMapWrapper.get(c._errors, errorCode);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
hasError(errorCode: string, path: string[] = null): boolean {
|
||||
return isPresent(this.getError(errorCode, path));
|
||||
}
|
||||
|
||||
_updateValue(): void {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a part of a form that cannot be divided into other controls.
|
||||
*
|
||||
* `Control` is one of the three fundamental building blocks used to define forms in Angular, along
|
||||
* with
|
||||
* {@link ControlGroup} and {@link ControlArray}.
|
||||
*/
|
||||
export class Control extends AbstractControl {
|
||||
_onChange: Function;
|
||||
|
||||
constructor(value: any = null, validator: Function = Validators.nullValidator) {
|
||||
super(validator);
|
||||
this._value = value;
|
||||
this.updateValidity({onlySelf: true});
|
||||
this._valueChanges = new EventEmitter();
|
||||
}
|
||||
|
||||
updateValue(value: any,
|
||||
{onlySelf, emitEvent, emitModelToViewChange}:
|
||||
{onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean} = {}):
|
||||
void {
|
||||
emitModelToViewChange = isPresent(emitModelToViewChange) ? emitModelToViewChange : true;
|
||||
this._value = value;
|
||||
if (isPresent(this._onChange) && emitModelToViewChange) this._onChange(this._value);
|
||||
this.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
|
||||
}
|
||||
|
||||
registerOnChange(fn: Function): void { this._onChange = fn; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a part of a form, of fixed length, that can contain other controls.
|
||||
*
|
||||
* A ControlGroup aggregates the values and errors of each {@link Control} in the group. Thus, if
|
||||
* one of the controls
|
||||
* in a group is invalid, the entire group is invalid. Similarly, if a control changes its value,
|
||||
* the entire group
|
||||
* changes as well.
|
||||
*
|
||||
* `ControlGroup` is one of the three fundamental building blocks used to define forms in Angular,
|
||||
* along with
|
||||
* {@link Control} and {@link ControlArray}. {@link ControlArray} can also contain other controls,
|
||||
* but is of variable
|
||||
* length.
|
||||
*/
|
||||
export class ControlGroup extends AbstractControl {
|
||||
controls: StringMap<string, AbstractControl>;
|
||||
_optionals: StringMap<string, boolean>;
|
||||
|
||||
constructor(controls: StringMap<string, AbstractControl>,
|
||||
optionals: StringMap<string, boolean> = null,
|
||||
validator: Function = Validators.group) {
|
||||
super(validator);
|
||||
this.controls = controls;
|
||||
this._optionals = isPresent(optionals) ? optionals : {};
|
||||
|
||||
this._valueChanges = new EventEmitter();
|
||||
|
||||
this._setParentForControls();
|
||||
this._value = this._reduceValue();
|
||||
this.updateValidity({onlySelf: true});
|
||||
}
|
||||
|
||||
addControl(name: string, c: AbstractControl) {
|
||||
this.controls[name] = c;
|
||||
c.setParent(this);
|
||||
}
|
||||
|
||||
removeControl(name: string) { StringMapWrapper.delete(this.controls, name); }
|
||||
|
||||
include(controlName: string): void {
|
||||
StringMapWrapper.set(this._optionals, controlName, true);
|
||||
this.updateValueAndValidity();
|
||||
}
|
||||
|
||||
exclude(controlName: string): void {
|
||||
StringMapWrapper.set(this._optionals, controlName, false);
|
||||
this.updateValueAndValidity();
|
||||
}
|
||||
|
||||
contains(controlName: string): boolean {
|
||||
var c = StringMapWrapper.contains(this.controls, controlName);
|
||||
return c && this._included(controlName);
|
||||
}
|
||||
|
||||
_setParentForControls() {
|
||||
StringMapWrapper.forEach(this.controls, (control, name) => { control.setParent(this); });
|
||||
}
|
||||
|
||||
_updateValue() { this._value = this._reduceValue(); }
|
||||
|
||||
_reduceValue() {
|
||||
return this._reduceChildren({}, (acc, control, name) => {
|
||||
acc[name] = control.value;
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
|
||||
_reduceChildren(initValue: any, fn: Function) {
|
||||
var res = initValue;
|
||||
StringMapWrapper.forEach(this.controls, (control, name) => {
|
||||
if (this._included(name)) {
|
||||
res = fn(res, control, name);
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
_included(controlName: string): boolean {
|
||||
var isOptional = StringMapWrapper.contains(this._optionals, controlName);
|
||||
return !isOptional || StringMapWrapper.get(this._optionals, controlName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a part of a form, of variable length, that can contain other controls.
|
||||
*
|
||||
* A `ControlArray` aggregates the values and errors of each {@link Control} in the group. Thus, if
|
||||
* one of the controls
|
||||
* in a group is invalid, the entire group is invalid. Similarly, if a control changes its value,
|
||||
* the entire group
|
||||
* changes as well.
|
||||
*
|
||||
* `ControlArray` is one of the three fundamental building blocks used to define forms in Angular,
|
||||
* along with {@link Control} and {@link ControlGroup}. {@link ControlGroup} can also contain
|
||||
* other controls, but is of fixed length.
|
||||
*/
|
||||
export class ControlArray extends AbstractControl {
|
||||
controls: AbstractControl[];
|
||||
|
||||
constructor(controls: AbstractControl[], validator: Function = Validators.array) {
|
||||
super(validator);
|
||||
this.controls = controls;
|
||||
|
||||
this._valueChanges = new EventEmitter();
|
||||
|
||||
this._setParentForControls();
|
||||
this._updateValue();
|
||||
this.updateValidity({onlySelf: true});
|
||||
}
|
||||
|
||||
at(index: number): AbstractControl { return this.controls[index]; }
|
||||
|
||||
push(control: AbstractControl): void {
|
||||
this.controls.push(control);
|
||||
control.setParent(this);
|
||||
this.updateValueAndValidity();
|
||||
}
|
||||
|
||||
insert(index: number, control: AbstractControl): void {
|
||||
ListWrapper.insert(this.controls, index, control);
|
||||
control.setParent(this);
|
||||
this.updateValueAndValidity();
|
||||
}
|
||||
|
||||
removeAt(index: number): void {
|
||||
ListWrapper.removeAt(this.controls, index);
|
||||
this.updateValueAndValidity();
|
||||
}
|
||||
|
||||
get length(): number { return this.controls.length; }
|
||||
|
||||
_updateValue() { this._value = ListWrapper.map(this.controls, (c) => c.value); }
|
||||
|
||||
_setParentForControls() {
|
||||
ListWrapper.forEach(this.controls, (control) => { control.setParent(this); });
|
||||
}
|
||||
}
|
67
modules/angular2/src/core/forms/validators.ts
Normal file
67
modules/angular2/src/core/forms/validators.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import {isBlank, isPresent} from 'angular2/src/core/facade/lang';
|
||||
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {OpaqueToken} from 'angular2/di';
|
||||
|
||||
import * as modelModule from './model';
|
||||
|
||||
export const NG_VALIDATORS: OpaqueToken = CONST_EXPR(new OpaqueToken("NgValidators"));
|
||||
|
||||
/**
|
||||
* Provides a set of validators used by form controls.
|
||||
*
|
||||
* # Example
|
||||
*
|
||||
* ```
|
||||
* var loginControl = new Control("", Validators.required)
|
||||
* ```
|
||||
*/
|
||||
export class Validators {
|
||||
static required(c: modelModule.Control): StringMap<string, boolean> {
|
||||
return isBlank(c.value) || c.value == "" ? {"required": true} : null;
|
||||
}
|
||||
|
||||
static nullValidator(c: any): StringMap<string, boolean> { return null; }
|
||||
|
||||
static compose(validators: Function[]): Function {
|
||||
if (isBlank(validators)) return Validators.nullValidator;
|
||||
|
||||
return function(c: modelModule.Control) {
|
||||
var res = ListWrapper.reduce(validators, (res, validator) => {
|
||||
var errors = validator(c);
|
||||
return isPresent(errors) ? StringMapWrapper.merge(res, errors) : res;
|
||||
}, {});
|
||||
return StringMapWrapper.isEmpty(res) ? null : res;
|
||||
};
|
||||
}
|
||||
|
||||
static group(c: modelModule.ControlGroup): StringMap<string, boolean> {
|
||||
var res = {};
|
||||
StringMapWrapper.forEach(c.controls, (control, name) => {
|
||||
if (c.contains(name) && isPresent(control.errors)) {
|
||||
Validators._mergeErrors(control, res);
|
||||
}
|
||||
});
|
||||
return StringMapWrapper.isEmpty(res) ? null : res;
|
||||
}
|
||||
|
||||
static array(c: modelModule.ControlArray): StringMap<string, boolean> {
|
||||
var res = {};
|
||||
ListWrapper.forEach(c.controls, (control) => {
|
||||
if (isPresent(control.errors)) {
|
||||
Validators._mergeErrors(control, res);
|
||||
}
|
||||
});
|
||||
return StringMapWrapper.isEmpty(res) ? null : res;
|
||||
}
|
||||
|
||||
static _mergeErrors(control: modelModule.AbstractControl, res: StringMap<string, any[]>): void {
|
||||
StringMapWrapper.forEach(control.errors, (value, error) => {
|
||||
if (!StringMapWrapper.contains(res, error)) {
|
||||
res[error] = [];
|
||||
}
|
||||
var current: any[] = res[error];
|
||||
current.push(control);
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user