refactor: move angular source to /packages rather than modules/@angular

This commit is contained in:
Jason Aden
2017-03-02 10:48:42 -08:00
parent 5ad5301a3e
commit 3e51a19983
1051 changed files with 18 additions and 18 deletions

View File

@ -0,0 +1,81 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NgModule, Type} from '@angular/core';
import {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
import {DefaultValueAccessor} from './directives/default_value_accessor';
import {NgControlStatus, NgControlStatusGroup} from './directives/ng_control_status';
import {NgForm} from './directives/ng_form';
import {NgModel} from './directives/ng_model';
import {NgModelGroup} from './directives/ng_model_group';
import {NgNoValidate} from './directives/ng_no_validate_directive';
import {NumberValueAccessor} from './directives/number_value_accessor';
import {RadioControlValueAccessor} from './directives/radio_control_value_accessor';
import {RangeValueAccessor} from './directives/range_value_accessor';
import {FormControlDirective} from './directives/reactive_directives/form_control_directive';
import {FormControlName} from './directives/reactive_directives/form_control_name';
import {FormGroupDirective} from './directives/reactive_directives/form_group_directive';
import {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name';
import {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
import {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
import {CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators';
export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
export {ControlValueAccessor} from './directives/control_value_accessor';
export {DefaultValueAccessor} from './directives/default_value_accessor';
export {NgControl} from './directives/ng_control';
export {NgControlStatus, NgControlStatusGroup} from './directives/ng_control_status';
export {NgForm} from './directives/ng_form';
export {NgModel} from './directives/ng_model';
export {NgModelGroup} from './directives/ng_model_group';
export {NumberValueAccessor} from './directives/number_value_accessor';
export {RadioControlValueAccessor} from './directives/radio_control_value_accessor';
export {RangeValueAccessor} from './directives/range_value_accessor';
export {FormControlDirective} from './directives/reactive_directives/form_control_directive';
export {FormControlName} from './directives/reactive_directives/form_control_name';
export {FormGroupDirective} from './directives/reactive_directives/form_group_directive';
export {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name';
export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
export {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
export const SHARED_FORM_DIRECTIVES: Type<any>[] = [
NgNoValidate,
NgSelectOption,
NgSelectMultipleOption,
DefaultValueAccessor,
NumberValueAccessor,
RangeValueAccessor,
CheckboxControlValueAccessor,
SelectControlValueAccessor,
SelectMultipleControlValueAccessor,
RadioControlValueAccessor,
NgControlStatus,
NgControlStatusGroup,
RequiredValidator,
MinLengthValidator,
MaxLengthValidator,
PatternValidator,
CheckboxRequiredValidator,
EmailValidator,
];
export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm];
export const REACTIVE_DRIVEN_DIRECTIVES: Type<any>[] =
[FormControlDirective, FormGroupDirective, FormControlName, FormGroupName, FormArrayName];
/**
* Internal module used for sharing directives between FormsModule and ReactiveFormsModule
*/
@NgModule({
declarations: SHARED_FORM_DIRECTIVES,
exports: SHARED_FORM_DIRECTIVES,
})
export class InternalFormsSharedModule {
}

View File

@ -0,0 +1,61 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Observable} from 'rxjs/Observable';
import {AbstractControl} from '../model';
/**
* Base class for control directives.
*
* Only used internally in the forms module.
*
* @stable
*/
export abstract class AbstractControlDirective {
get control(): AbstractControl { throw new Error('unimplemented'); }
get value(): any { return this.control ? this.control.value : null; }
get valid(): boolean { return this.control ? this.control.valid : null; }
get invalid(): boolean { return this.control ? this.control.invalid : null; }
get pending(): boolean { return this.control ? this.control.pending : null; }
get errors(): {[key: string]: any} { return this.control ? this.control.errors : null; }
get pristine(): boolean { return this.control ? this.control.pristine : null; }
get dirty(): boolean { return this.control ? this.control.dirty : null; }
get touched(): boolean { return this.control ? this.control.touched : null; }
get untouched(): boolean { return this.control ? this.control.untouched : null; }
get disabled(): boolean { return this.control ? this.control.disabled : null; }
get enabled(): boolean { return this.control ? this.control.enabled : null; }
get statusChanges(): Observable<any> { return this.control ? this.control.statusChanges : null; }
get valueChanges(): Observable<any> { return this.control ? this.control.valueChanges : null; }
get path(): string[] { return null; }
reset(value: any = undefined): void {
if (this.control) this.control.reset(value);
}
hasError(errorCode: string, path: string[] = null): boolean {
return this.control ? this.control.hasError(errorCode, path) : false;
}
getError(errorCode: string, path: string[] = null): any {
return this.control ? this.control.getError(errorCode, path) : null;
}
}

View File

@ -0,0 +1,67 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {OnDestroy, OnInit} from '@angular/core';
import {FormGroup} from '../model';
import {ControlContainer} from './control_container';
import {Form} from './form_interface';
import {composeAsyncValidators, composeValidators, controlPath} from './shared';
import {AsyncValidatorFn, ValidatorFn} from './validators';
/**
* This is a base class for code shared between {@link NgModelGroup} and {@link FormGroupName}.
*
* @stable
*/
export class AbstractFormGroupDirective extends ControlContainer implements OnInit, OnDestroy {
/** @internal */
_parent: ControlContainer;
/** @internal */
_validators: any[];
/** @internal */
_asyncValidators: any[];
ngOnInit(): void {
this._checkParentType();
this.formDirective.addFormGroup(this);
}
ngOnDestroy(): void {
if (this.formDirective) {
this.formDirective.removeFormGroup(this);
}
}
/**
* Get the {@link FormGroup} backing this binding.
*/
get control(): FormGroup { return this.formDirective.getFormGroup(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 ? this._parent.formDirective : null; }
get validator(): ValidatorFn { return composeValidators(this._validators); }
get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); }
/** @internal */
_checkParentType(): void {}
}

View File

@ -0,0 +1,50 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, ElementRef, Renderer, forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
export const CHECKBOX_VALUE_ACCESSOR: any = {
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
* ```
* <input type="checkbox" name="rememberLogin" ngModel>
* ```
*
* @stable
*/
@Directive({
selector:
'input[type=checkbox][formControlName],input[type=checkbox][formControl],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; }
setDisabledState(isDisabled: boolean): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
}

View File

@ -0,0 +1,32 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbstractControlDirective} from './abstract_control_directive';
import {Form} from './form_interface';
/**
* A directive that contains multiple {@link NgControl}s.
*
* Only used by the forms module.
*
* @stable
*/
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; }
}

View File

@ -0,0 +1,52 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {InjectionToken} 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.
*
* @stable
*/
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;
/**
* This function is called when the control status changes to or from "DISABLED".
* Depending on the value, it will enable or disable the appropriate DOM element.
*
* @param isDisabled
*/
setDisabledState?(isDisabled: boolean): void;
}
/**
* Used to provide a {@link ControlValueAccessor} for form controls.
*
* See {@link DefaultValueAccessor} for how to implement one.
* @stable
*/
export const NG_VALUE_ACCESSOR = new InjectionToken<ControlValueAccessor>('NgValueAccessor');

View File

@ -0,0 +1,56 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, ElementRef, Renderer, forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
export const DEFAULT_VALUE_ACCESSOR: any = {
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 FormControlDirective}, and {@link FormControlName} directives.
*
* ### Example
* ```
* <input type="text" name="searchQuery" ngModel>
* ```
*
* @stable
*/
@Directive({
selector:
'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],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 {
const normalizedValue = value == null ? '' : value;
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
}

View File

@ -0,0 +1,64 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export const FormErrorExamples = {
formControlName: `
<div [formGroup]="myGroup">
<input formControlName="firstName">
</div>
In your class:
this.myGroup = new FormGroup({
firstName: new FormControl()
});`,
formGroupName: `
<div [formGroup]="myGroup">
<div formGroupName="person">
<input formControlName="firstName">
</div>
</div>
In your class:
this.myGroup = new FormGroup({
person: new FormGroup({ firstName: new FormControl() })
});`,
formArrayName: `
<div [formGroup]="myGroup">
<div formArrayName="cities">
<div *ngFor="let city of cityArray.controls; let i=index">
<input [formControlName]="i">
</div>
</div>
</div>
In your class:
this.cityArray = new FormArray([new FormControl('SF')]);
this.myGroup = new FormGroup({
cities: this.cityArray
});`,
ngModelGroup: `
<form>
<div ngModelGroup="person">
<input [(ngModel)]="person.name" name="firstName">
</div>
</form>`,
ngModelWithFormGroup: `
<div [formGroup]="myGroup">
<input formControlName="firstName">
<input [(ngModel)]="showMoreControls" [ngModelOptions]="{standalone: true}">
</div>
`
};
 

View File

@ -0,0 +1,58 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FormControl, FormGroup} from '../model';
import {AbstractFormGroupDirective} from './abstract_form_group_directive';
import {NgControl} from './ng_control';
/**
* An interface that {@link FormGroupDirective} and {@link NgForm} implement.
*
* Only used by the forms module.
*
* @stable
*/
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 FormControl} associated with a particular {@link NgControl}.
*/
getControl(dir: NgControl): FormControl;
/**
* Add a group of controls to this form.
*/
addFormGroup(dir: AbstractFormGroupDirective): void;
/**
* Remove a group of controls from this form.
*/
removeFormGroup(dir: AbstractFormGroupDirective): void;
/**
* Look up the {@link FormGroup} associated with a particular {@link AbstractFormGroupDirective}.
*/
getFormGroup(dir: AbstractFormGroupDirective): FormGroup;
/**
* Update the model for a particular control with a new value.
*/
updateModel(dir: NgControl, value: any): void;
}

View File

@ -0,0 +1,41 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbstractControlDirective} from './abstract_control_directive';
import {ControlContainer} from './control_container';
import {ControlValueAccessor} from './control_value_accessor';
import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from './validators';
function unimplemented(): any {
throw new Error('unimplemented');
}
/**
* A base class that all control directive extend.
* It binds a {@link FormControl} object to a DOM element.
*
* Used internally by Angular forms.
*
* @stable
*/
export abstract class NgControl extends AbstractControlDirective {
/** @internal */
_parent: ControlContainer = null;
name: string = null;
valueAccessor: ControlValueAccessor = null;
/** @internal */
_rawValidators: Array<Validator|ValidatorFn> = [];
/** @internal */
_rawAsyncValidators: Array<AsyncValidator|AsyncValidatorFn> = [];
get validator(): ValidatorFn { return <ValidatorFn>unimplemented(); }
get asyncValidator(): AsyncValidatorFn { return <AsyncValidatorFn>unimplemented(); }
abstract viewToModelUpdate(newValue: any): void;
}

View File

@ -0,0 +1,63 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, Self} from '@angular/core';
import {AbstractControlDirective} from './abstract_control_directive';
import {ControlContainer} from './control_container';
import {NgControl} from './ng_control';
export class AbstractControlStatus {
private _cd: AbstractControlDirective;
constructor(cd: AbstractControlDirective) { this._cd = cd; }
get ngClassUntouched(): boolean { return this._cd.control ? this._cd.control.untouched : false; }
get ngClassTouched(): boolean { return this._cd.control ? this._cd.control.touched : false; }
get ngClassPristine(): boolean { return this._cd.control ? this._cd.control.pristine : false; }
get ngClassDirty(): boolean { return this._cd.control ? this._cd.control.dirty : false; }
get ngClassValid(): boolean { return this._cd.control ? this._cd.control.valid : false; }
get ngClassInvalid(): boolean { return this._cd.control ? this._cd.control.invalid : false; }
get ngClassPending(): boolean { return this._cd.control ? this._cd.control.pending : false; }
}
export const ngControlStatusHost = {
'[class.ng-untouched]': 'ngClassUntouched',
'[class.ng-touched]': 'ngClassTouched',
'[class.ng-pristine]': 'ngClassPristine',
'[class.ng-dirty]': 'ngClassDirty',
'[class.ng-valid]': 'ngClassValid',
'[class.ng-invalid]': 'ngClassInvalid',
'[class.ng-pending]': 'ngClassPending',
};
/**
* Directive automatically applied to Angular form controls that sets CSS classes
* based on control status (valid/invalid/dirty/etc).
*
* @stable
*/
@Directive({selector: '[formControlName],[ngModel],[formControl]', host: ngControlStatusHost})
export class NgControlStatus extends AbstractControlStatus {
constructor(@Self() cd: NgControl) { super(cd); }
}
/**
* Directive automatically applied to Angular form groups that sets CSS classes
* based on control status (valid/invalid/dirty/etc).
*
* @stable
*/
@Directive({
selector:
'[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]',
host: ngControlStatusHost
})
export class NgControlStatusGroup extends AbstractControlStatus {
constructor(@Self() cd: ControlContainer) { super(cd); }
}

View File

@ -0,0 +1,158 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, EventEmitter, Inject, Optional, Self, forwardRef} from '@angular/core';
import {AbstractControl, FormControl, FormGroup} from '../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {ControlContainer} from './control_container';
import {Form} from './form_interface';
import {NgControl} from './ng_control';
import {NgModel} from './ng_model';
import {NgModelGroup} from './ng_model_group';
import {composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from './shared';
export const formDirectiveProvider: any = {
provide: ControlContainer,
useExisting: forwardRef(() => NgForm)
};
const resolvedPromise = Promise.resolve(null);
/**
* @whatItDoes Creates a top-level {@link FormGroup} instance and binds it to a form
* to track aggregate form value and validation status.
*
* @howToUse
*
* As soon as you import the `FormsModule`, this directive becomes active by default on
* all `<form>` tags. You don't need to add a special selector.
*
* You can export the directive into a local template variable using `ngForm` as the key
* (ex: `#myForm="ngForm"`). This is optional, but useful. Many properties from the underlying
* {@link FormGroup} instance are duplicated on the directive itself, so a reference to it
* will give you access to the aggregate value and validity status of the form, as well as
* user interaction properties like `dirty` and `touched`.
*
* To register child controls with the form, you'll want to use {@link NgModel} with a
* `name` attribute. You can also use {@link NgModelGroup} if you'd like to create
* sub-groups within the form.
*
* You can listen to the directive's `ngSubmit` event to be notified when the user has
* triggered a form submission. The `ngSubmit` event will be emitted with the original form
* submission event.
*
* {@example forms/ts/simpleForm/simple_form_example.ts region='Component'}
*
* * **npm package**: `@angular/forms`
*
* * **NgModule**: `FormsModule`
*
* @stable
*/
@Directive({
selector: 'form:not([ngNoForm]):not([formGroup]),ngForm,[ngForm]',
providers: [formDirectiveProvider],
host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
outputs: ['ngSubmit'],
exportAs: 'ngForm'
})
export class NgForm extends ControlContainer implements Form {
private _submitted: boolean = false;
form: FormGroup;
ngSubmit = new EventEmitter();
constructor(
@Optional() @Self() @Inject(NG_VALIDATORS) validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) {
super();
this.form =
new FormGroup({}, composeValidators(validators), composeAsyncValidators(asyncValidators));
}
get submitted(): boolean { return this._submitted; }
get formDirective(): Form { return this; }
get control(): FormGroup { return this.form; }
get path(): string[] { return []; }
get controls(): {[key: string]: AbstractControl} { return this.form.controls; }
addControl(dir: NgModel): void {
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
dir._control = <FormControl>container.registerControl(dir.name, dir.control);
setUpControl(dir.control, dir);
dir.control.updateValueAndValidity({emitEvent: false});
});
}
getControl(dir: NgModel): FormControl { return <FormControl>this.form.get(dir.path); }
removeControl(dir: NgModel): void {
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
if (container) {
container.removeControl(dir.name);
}
});
}
addFormGroup(dir: NgModelGroup): void {
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
const group = new FormGroup({});
setUpFormContainer(group, dir);
container.registerControl(dir.name, group);
group.updateValueAndValidity({emitEvent: false});
});
}
removeFormGroup(dir: NgModelGroup): void {
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
if (container) {
container.removeControl(dir.name);
}
});
}
getFormGroup(dir: NgModelGroup): FormGroup { return <FormGroup>this.form.get(dir.path); }
updateModel(dir: NgControl, value: any): void {
resolvedPromise.then(() => {
const ctrl = <FormControl>this.form.get(dir.path);
ctrl.setValue(value);
});
}
setValue(value: {[key: string]: any}): void { this.control.setValue(value); }
onSubmit($event: Event): boolean {
this._submitted = true;
this.ngSubmit.emit($event);
return false;
}
onReset(): void { this.resetForm(); }
resetForm(value: any = undefined): void {
this.form.reset(value);
this._submitted = false;
}
/** @internal */
_findContainer(path: string[]): FormGroup {
path.pop();
return path.length ? <FormGroup>this.form.get(path) : this.form;
}
}

View File

@ -0,0 +1,241 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, EventEmitter, Host, HostListener, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core';
import {FormControl} from '../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {AbstractFormGroupDirective} from './abstract_form_group_directive';
import {ControlContainer} from './control_container';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
import {NgControl} from './ng_control';
import {NgForm} from './ng_form';
import {NgModelGroup} from './ng_model_group';
import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor, setUpControl} from './shared';
import {TemplateDrivenErrors} from './template_driven_errors';
import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from './validators';
export const formControlBinding: any = {
provide: NgControl,
useExisting: forwardRef(() => NgModel)
};
/**
* `ngModel` forces an additional change detection run when its inputs change:
* E.g.:
* ```
* <div>{{myModel.valid}}</div>
* <input [(ngModel)]="myValue" #myModel="ngModel">
* ```
* I.e. `ngModel` can export itself on the element and then be used in the template.
* Normally, this would result in expressions before the `input` that use the exported directive
* to have and old value as they have been
* dirty checked before. As this is a very common case for `ngModel`, we added this second change
* detection run.
*
* Notes:
* - this is just one extra run no matter how many `ngModel` have been changed.
* - this is a general problem when using `exportAs` for directives!
*/
const resolvedPromise = Promise.resolve(null);
/**
* @whatItDoes Creates a {@link FormControl} instance from a domain model and binds it
* to a form control element.
*
* The {@link FormControl} instance will track the value, user interaction, and
* validation status of the control and keep the view synced with the model. If used
* within a parent form, the directive will also register itself with the form as a child
* control.
*
* @howToUse
*
* This directive can be used by itself or as part of a larger form. All you need is the
* `ngModel` selector to activate it.
*
* It accepts a domain model as an optional {@link @Input}. If you have a one-way binding
* to `ngModel` with `[]` syntax, changing the value of the domain model in the component
* class will set the value in the view. If you have a two-way binding with `[()]` syntax
* (also known as 'banana-box syntax'), the value in the UI will always be synced back to
* the domain model in your class as well.
*
* If you wish to inspect the properties of the associated {@link FormControl} (like
* validity state), you can also export the directive into a local template variable using
* `ngModel` as the key (ex: `#myVar="ngModel"`). You can then access the control using the
* directive's `control` property, but most properties you'll need (like `valid` and `dirty`)
* will fall through to the control anyway, so you can access them directly. You can see a
* full list of properties directly available in {@link AbstractControlDirective}.
*
* The following is an example of a simple standalone control using `ngModel`:
*
* {@example forms/ts/simpleNgModel/simple_ng_model_example.ts region='Component'}
*
* When using the `ngModel` within `<form>` tags, you'll also need to supply a `name` attribute
* so that the control can be registered with the parent form under that name.
*
* It's worth noting that in the context of a parent form, you often can skip one-way or
* two-way binding because the parent form will sync the value for you. You can access
* its properties by exporting it into a local template variable using `ngForm` (ex:
* `#f="ngForm"`). Then you can pass it where it needs to go on submit.
*
* If you do need to populate initial values into your form, using a one-way binding for
* `ngModel` tends to be sufficient as long as you use the exported form's value rather
* than the domain model's value on submit.
*
* Take a look at an example of using `ngModel` within a form:
*
* {@example forms/ts/simpleForm/simple_form_example.ts region='Component'}
*
* To see `ngModel` examples with different form control types, see:
*
* * Radio buttons: {@link RadioControlValueAccessor}
* * Selects: {@link SelectControlValueAccessor}
*
* **npm package**: `@angular/forms`
*
* **NgModule**: `FormsModule`
*
* @stable
*/
@Directive({
selector: '[ngModel]:not([formControlName]):not([formControl])',
providers: [formControlBinding],
exportAs: 'ngModel'
})
export class NgModel extends NgControl implements OnChanges,
OnDestroy {
/** @internal */
_control = new FormControl();
/** @internal */
_registered = false;
private _composing = false;
viewModel: any;
@Input() name: string;
@Input('disabled') isDisabled: boolean;
@Input('ngModel') model: any;
@Input('ngModelOptions') options: {name?: string, standalone?: boolean};
@Output('ngModelChange') update = new EventEmitter();
@HostListener('compositionstart')
compositionStart(): void { this._composing = true; }
@HostListener('compositionend')
compositionEnd(): void {
this._composing = false;
this.update.emit(this.viewModel);
}
constructor(@Optional() @Host() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[]) {
super();
this._parent = parent;
this._rawValidators = validators || [];
this._rawAsyncValidators = asyncValidators || [];
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
ngOnChanges(changes: SimpleChanges) {
this._checkForErrors();
if (!this._registered) this._setUpControl();
if ('isDisabled' in changes) {
this._updateDisabled(changes);
}
if (isPropertyUpdated(changes, this.viewModel)) {
this._updateValue(this.model);
this.viewModel = this.model;
}
}
ngOnDestroy(): void { this.formDirective && this.formDirective.removeControl(this); }
get control(): FormControl { return this._control; }
get path(): string[] {
return this._parent ? controlPath(this.name, this._parent) : [this.name];
}
get formDirective(): any { return this._parent ? this._parent.formDirective : null; }
get validator(): ValidatorFn { return composeValidators(this._rawValidators); }
get asyncValidator(): AsyncValidatorFn {
return composeAsyncValidators(this._rawAsyncValidators);
}
viewToModelUpdate(newValue: any): void {
this.viewModel = newValue;
!this._composing && this.update.emit(newValue);
}
private _setUpControl(): void {
this._isStandalone() ? this._setUpStandalone() :
this.formDirective.addControl(this);
this._registered = true;
}
private _isStandalone(): boolean {
return !this._parent || (this.options && this.options.standalone);
}
private _setUpStandalone(): void {
setUpControl(this._control, this);
this._control.updateValueAndValidity({emitEvent: false});
}
private _checkForErrors(): void {
if (!this._isStandalone()) {
this._checkParentType();
}
this._checkName();
}
private _checkParentType(): void {
if (!(this._parent instanceof NgModelGroup) &&
this._parent instanceof AbstractFormGroupDirective) {
TemplateDrivenErrors.formGroupNameException();
} else if (
!(this._parent instanceof NgModelGroup) && !(this._parent instanceof NgForm)) {
TemplateDrivenErrors.modelParentException();
}
}
private _checkName(): void {
if (this.options && this.options.name) this.name = this.options.name;
if (!this._isStandalone() && !this.name) {
TemplateDrivenErrors.missingNameException();
}
}
private _updateValue(value: any): void {
resolvedPromise.then(
() => { this.control.setValue(value, {emitViewToModelChange: false}); });
}
private _updateDisabled(changes: SimpleChanges) {
const disabledValue = changes['isDisabled'].currentValue;
const isDisabled =
disabledValue === '' || (disabledValue && disabledValue !== 'false');
resolvedPromise.then(() => {
if (isDisabled && !this.control.disabled) {
this.control.disable();
} else if (!isDisabled && this.control.disabled) {
this.control.enable();
}
});
}
}

View File

@ -0,0 +1,68 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf, forwardRef} from '@angular/core';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {AbstractFormGroupDirective} from './abstract_form_group_directive';
import {ControlContainer} from './control_container';
import {NgForm} from './ng_form';
import {TemplateDrivenErrors} from './template_driven_errors';
export const modelGroupProvider: any = {
provide: ControlContainer,
useExisting: forwardRef(() => NgModelGroup)
};
/**
* @whatItDoes Creates and binds a {@link FormGroup} instance to a DOM element.
*
* @howToUse
*
* This directive can only be used as a child of {@link NgForm} (or in other words,
* within `<form>` tags).
*
* Use this directive if you'd like to create a sub-group within a form. This can
* come in handy if you want to validate a sub-group of your form separately from
* the rest of your form, or if some values in your domain model make more sense to
* consume together in a nested object.
*
* Pass in the name you'd like this sub-group to have and it will become the key
* for the sub-group in the form's full value. You can also export the directive into
* a local template variable using `ngModelGroup` (ex: `#myGroup="ngModelGroup"`).
*
* {@example forms/ts/ngModelGroup/ng_model_group_example.ts region='Component'}
*
* * **npm package**: `@angular/forms`
*
* * **NgModule**: `FormsModule`
*
* @stable
*/
@Directive({selector: '[ngModelGroup]', providers: [modelGroupProvider], exportAs: 'ngModelGroup'})
export class NgModelGroup extends AbstractFormGroupDirective implements OnInit, OnDestroy {
@Input('ngModelGroup') name: string;
constructor(
@Host() @SkipSelf() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) {
super();
this._parent = parent;
this._validators = validators;
this._asyncValidators = asyncValidators;
}
/** @internal */
_checkParentType(): void {
if (!(this._parent instanceof NgModelGroup) && !(this._parent instanceof NgForm)) {
TemplateDrivenErrors.modelGroupParentException();
}
}
}

View File

@ -0,0 +1,29 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive} from '@angular/core';
/**
* @whatItDoes Adds `novalidate` attribute to all forms by default.
*
* `novalidate` is used to disable browser's native form validation.
*
* If you want to use native validation with Angular forms, just add `ngNativeValidate` attribute:
*
* ```
* <form ngNativeValidate></form>
* ```
*
* @experimental
*/
@Directive({
selector: 'form:not([ngNoForm]):not([ngNativeValidate])',
host: {'novalidate': ''},
})
export class NgNoValidate {
}

View File

@ -0,0 +1,27 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbstractControl} from '../model';
import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from './validators';
export function normalizeValidator(validator: ValidatorFn | Validator): ValidatorFn {
if ((<Validator>validator).validate) {
return (c: AbstractControl) => (<Validator>validator).validate(c);
} else {
return <ValidatorFn>validator;
}
}
export function normalizeAsyncValidator(validator: AsyncValidatorFn | AsyncValidator):
AsyncValidatorFn {
if ((<AsyncValidator>validator).validate) {
return (c: AbstractControl) => (<AsyncValidator>validator).validate(c);
} else {
return <AsyncValidatorFn>validator;
}
}

View File

@ -0,0 +1,58 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, ElementRef, Renderer, forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
export const NUMBER_VALUE_ACCESSOR: any = {
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 FormControlDirective}, and {@link FormControlName} directives.
*
* ### Example
* ```
* <input type="number" [(ngModel)]="age">
* ```
*/
@Directive({
selector:
'input[type=number][formControlName],input[type=number][formControl],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 {
// The value needs to be normalized for IE9, otherwise it is set to 'null' when null
const normalizedValue = value == null ? '' : value;
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);
}
registerOnChange(fn: (_: number) => void): void {
this.onChange = (value) => { fn(value == '' ? null : parseFloat(value)); };
}
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
}

View File

@ -0,0 +1,153 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, ElementRef, Injectable, Injector, Input, OnDestroy, OnInit, Renderer, forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
import {NgControl} from './ng_control';
export const RADIO_VALUE_ACCESSOR: any = {
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) {
for (let i = this._accessors.length - 1; i >= 0; --i) {
if (this._accessors[i][1] === accessor) {
this._accessors.splice(i, 1);
return;
}
}
}
select(accessor: RadioControlValueAccessor) {
this._accessors.forEach((c) => {
if (this._isSameGroup(c, accessor) && c[1] !== accessor) {
c[1].fireUncheck(accessor.value);
}
});
}
private _isSameGroup(
controlPair: [NgControl, RadioControlValueAccessor],
accessor: RadioControlValueAccessor): boolean {
if (!controlPair[0].control) return false;
return controlPair[0]._parent === accessor._control._parent &&
controlPair[1].name === accessor.name;
}
}
/**
* @whatItDoes Writes radio control values and listens to radio control changes.
*
* Used by {@link NgModel}, {@link FormControlDirective}, and {@link FormControlName}
* to keep the view synced with the {@link FormControl} model.
*
* @howToUse
*
* If you have imported the {@link FormsModule} or the {@link ReactiveFormsModule}, this
* value accessor will be active on any radio control that has a form directive. You do
* **not** need to add a special selector to activate it.
*
* ### How to use radio buttons with form directives
*
* To use radio buttons in a template-driven form, you'll want to ensure that radio buttons
* in the same group have the same `name` attribute. Radio buttons with different `name`
* attributes do not affect each other.
*
* {@example forms/ts/radioButtons/radio_button_example.ts region='TemplateDriven'}
*
* When using radio buttons in a reactive form, radio buttons in the same group should have the
* same `formControlName`. You can also add a `name` attribute, but it's optional.
*
* {@example forms/ts/reactiveRadioButtons/reactive_radio_button_example.ts region='Reactive'}
*
* * **npm package**: `@angular/forms`
*
* @stable
*/
@Directive({
selector:
'input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]',
host: {'(change)': 'onChange()', '(blur)': 'onTouched()'},
providers: [RADIO_VALUE_ACCESSOR]
})
export class RadioControlValueAccessor implements ControlValueAccessor,
OnDestroy, OnInit {
/** @internal */
_state: boolean;
/** @internal */
_control: NgControl;
/** @internal */
_fn: Function;
onChange = () => {};
onTouched = () => {};
@Input() name: string;
@Input() formControlName: string;
@Input() value: any;
constructor(
private _renderer: Renderer, private _elementRef: ElementRef,
private _registry: RadioControlRegistry, private _injector: Injector) {}
ngOnInit(): void {
this._control = this._injector.get(NgControl);
this._checkName();
this._registry.add(this._control, this);
}
ngOnDestroy(): void { this._registry.remove(this); }
writeValue(value: any): void {
this._state = value === this.value;
this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', this._state);
}
registerOnChange(fn: (_: any) => {}): void {
this._fn = fn;
this.onChange = () => {
fn(this.value);
this._registry.select(this);
};
}
fireUncheck(value: any): void { this.writeValue(value); }
registerOnTouched(fn: () => {}): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
private _checkName(): void {
if (this.name && this.formControlName && this.name !== this.formControlName) {
this._throwNameError();
}
if (!this.name && this.formControlName) this.name = this.formControlName;
}
private _throwNameError(): void {
throw new Error(`
If you define both a name and a formControlName attribute on your radio button, their values
must match. Ex: <input type="radio" formControlName="food" name="food">
`);
}
}

View File

@ -0,0 +1,57 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, ElementRef, Provider, Renderer, forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
export const RANGE_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RangeValueAccessor),
multi: true
};
/**
* The accessor for writing a range value and listening to changes that is used by the
* {@link NgModel}, {@link FormControlDirective}, and {@link FormControlName} directives.
*
* ### Example
* ```
* <input type="range" [(ngModel)]="age" >
* ```
*/
@Directive({
selector:
'input[type=range][formControlName],input[type=range][formControl],input[type=range][ngModel]',
host: {
'(change)': 'onChange($event.target.value)',
'(input)': 'onChange($event.target.value)',
'(blur)': 'onTouched()'
},
providers: [RANGE_VALUE_ACCESSOR]
})
export class RangeValueAccessor implements ControlValueAccessor {
onChange = (_: any) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: any): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', parseFloat(value));
}
registerOnChange(fn: (_: number) => void): void {
this.onChange = (value) => { fn(value == '' ? null : parseFloat(value)); };
}
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
}

View File

@ -0,0 +1,120 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, EventEmitter, Inject, Input, OnChanges, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core';
import {FormControl} from '../../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor';
import {NgControl} from '../ng_control';
import {ReactiveErrors} from '../reactive_errors';
import {composeAsyncValidators, composeValidators, isPropertyUpdated, selectValueAccessor, setUpControl} from '../shared';
import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from '../validators';
export const formControlBinding: any = {
provide: NgControl,
useExisting: forwardRef(() => FormControlDirective)
};
/**
* @whatItDoes Syncs a standalone {@link FormControl} instance to a form control element.
*
* In other words, this directive ensures that any values written to the {@link FormControl}
* instance programmatically will be written to the DOM element (model -> view). Conversely,
* any values written to the DOM element through user input will be reflected in the
* {@link FormControl} instance (view -> model).
*
* @howToUse
*
* Use this directive if you'd like to create and manage a {@link FormControl} instance directly.
* Simply create a {@link FormControl}, save it to your component class, and pass it into the
* {@link FormControlDirective}.
*
* This directive is designed to be used as a standalone control. Unlike {@link FormControlName},
* it does not require that your {@link FormControl} instance be part of any parent
* {@link FormGroup}, and it won't be registered to any {@link FormGroupDirective} that
* exists above it.
*
* **Get the value**: the `value` property is always synced and available on the
* {@link FormControl} instance. See a full list of available properties in
* {@link AbstractControl}.
*
* **Set the value**: You can pass in an initial value when instantiating the {@link FormControl},
* or you can set it programmatically later using {@link AbstractControl.setValue} or
* {@link AbstractControl.patchValue}.
*
* **Listen to value**: If you want to listen to changes in the value of the control, you can
* subscribe to the {@link AbstractControl.valueChanges} event. You can also listen to
* {@link AbstractControl.statusChanges} to be notified when the validation status is
* re-calculated.
*
* ### Example
*
* {@example forms/ts/simpleFormControl/simple_form_control_example.ts region='Component'}
*
* * **npm package**: `@angular/forms`
*
* * **NgModule**: `ReactiveFormsModule`
*
* @stable
*/
@Directive({selector: '[formControl]', providers: [formControlBinding], exportAs: 'ngForm'})
export class FormControlDirective extends NgControl implements OnChanges {
viewModel: any;
@Input('formControl') form: FormControl;
@Input('ngModel') model: any;
@Output('ngModelChange') update = new EventEmitter();
@Input('disabled')
set isDisabled(isDisabled: boolean) { ReactiveErrors.disabledAttrWarning(); }
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[]) {
super();
this._rawValidators = validators || [];
this._rawAsyncValidators = asyncValidators || [];
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
ngOnChanges(changes: SimpleChanges): void {
if (this._isControlChanged(changes)) {
setUpControl(this.form, this);
if (this.control.disabled && this.valueAccessor.setDisabledState) {
this.valueAccessor.setDisabledState(true);
}
this.form.updateValueAndValidity({emitEvent: false});
}
if (isPropertyUpdated(changes, this.viewModel)) {
this.form.setValue(this.model);
this.viewModel = this.model;
}
}
get path(): string[] { return []; }
get validator(): ValidatorFn { return composeValidators(this._rawValidators); }
get asyncValidator(): AsyncValidatorFn {
return composeAsyncValidators(this._rawAsyncValidators);
}
get control(): FormControl { return this.form; }
viewToModelUpdate(newValue: any): void {
this.viewModel = newValue;
this.update.emit(newValue);
}
private _isControlChanged(changes: {[key: string]: any}): boolean {
return changes.hasOwnProperty('form');
}
}

View File

@ -0,0 +1,159 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, EventEmitter, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, SkipSelf, forwardRef} from '@angular/core';
import {FormControl} from '../../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {AbstractFormGroupDirective} from '../abstract_form_group_directive';
import {ControlContainer} from '../control_container';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor';
import {NgControl} from '../ng_control';
import {ReactiveErrors} from '../reactive_errors';
import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor} from '../shared';
import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from '../validators';
import {FormGroupDirective} from './form_group_directive';
import {FormArrayName, FormGroupName} from './form_group_name';
export const controlNameBinding: any = {
provide: NgControl,
useExisting: forwardRef(() => FormControlName)
};
/**
* @whatItDoes Syncs a {@link FormControl} in an existing {@link FormGroup} to a form control
* element by name.
*
* In other words, this directive ensures that any values written to the {@link FormControl}
* instance programmatically will be written to the DOM element (model -> view). Conversely,
* any values written to the DOM element through user input will be reflected in the
* {@link FormControl} instance (view -> model).
*
* @howToUse
*
* This directive is designed to be used with a parent {@link FormGroupDirective} (selector:
* `[formGroup]`).
*
* It accepts the string name of the {@link FormControl} instance you want to
* link, and will look for a {@link FormControl} registered with that name in the
* closest {@link FormGroup} or {@link FormArray} above it.
*
* **Access the control**: You can access the {@link FormControl} associated with
* this directive by using the {@link AbstractControl.get} method.
* Ex: `this.form.get('first');`
*
* **Get value**: the `value` property is always synced and available on the {@link FormControl}.
* See a full list of available properties in {@link AbstractControl}.
*
* **Set value**: You can set an initial value for the control when instantiating the
* {@link FormControl}, or you can set it programmatically later using
* {@link AbstractControl.setValue} or {@link AbstractControl.patchValue}.
*
* **Listen to value**: If you want to listen to changes in the value of the control, you can
* subscribe to the {@link AbstractControl.valueChanges} event. You can also listen to
* {@link AbstractControl.statusChanges} to be notified when the validation status is
* re-calculated.
*
* ### Example
*
* In this example, we create form controls for first name and last name.
*
* {@example forms/ts/simpleFormGroup/simple_form_group_example.ts region='Component'}
*
* To see `formControlName` examples with different form control types, see:
*
* * Radio buttons: {@link RadioControlValueAccessor}
* * Selects: {@link SelectControlValueAccessor}
*
* **npm package**: `@angular/forms`
*
* **NgModule**: {@link ReactiveFormsModule}
*
* @stable
*/
@Directive({selector: '[formControlName]', providers: [controlNameBinding]})
export class FormControlName extends NgControl implements OnChanges, OnDestroy {
private _added = false;
/** @internal */
viewModel: any;
/** @internal */
_control: FormControl;
@Input('formControlName') name: string;
// TODO(kara): Replace ngModel with reactive API
@Input('ngModel') model: any;
@Output('ngModelChange') update = new EventEmitter();
@Input('disabled')
set isDisabled(isDisabled: boolean) { ReactiveErrors.disabledAttrWarning(); }
constructor(
@Optional() @Host() @SkipSelf() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators:
Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {
super();
this._parent = parent;
this._rawValidators = validators || [];
this._rawAsyncValidators = asyncValidators || [];
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
ngOnChanges(changes: SimpleChanges) {
if (!this._added) this._setUpControl();
if (isPropertyUpdated(changes, this.viewModel)) {
this.viewModel = this.model;
this.formDirective.updateModel(this, this.model);
}
}
ngOnDestroy(): void {
if (this.formDirective) {
this.formDirective.removeControl(this);
}
}
viewToModelUpdate(newValue: any): void {
this.viewModel = newValue;
this.update.emit(newValue);
}
get path(): string[] { return controlPath(this.name, this._parent); }
get formDirective(): any { return this._parent ? this._parent.formDirective : null; }
get validator(): ValidatorFn { return composeValidators(this._rawValidators); }
get asyncValidator(): AsyncValidatorFn {
return composeAsyncValidators(this._rawAsyncValidators);
}
get control(): FormControl { return this._control; }
private _checkParentType(): void {
if (!(this._parent instanceof FormGroupName) &&
this._parent instanceof AbstractFormGroupDirective) {
ReactiveErrors.ngModelGroupException();
} else if (
!(this._parent instanceof FormGroupName) && !(this._parent instanceof FormGroupDirective) &&
!(this._parent instanceof FormArrayName)) {
ReactiveErrors.controlParentException();
}
}
private _setUpControl() {
this._checkParentType();
this._control = this.formDirective.addControl(this);
if (this.control.disabled && this.valueAccessor.setDisabledState) {
this.valueAccessor.setDisabledState(true);
}
this._added = true;
}
}

View File

@ -0,0 +1,188 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, EventEmitter, Inject, Input, OnChanges, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core';
import {FormArray, FormControl, FormGroup} from '../../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from '../../validators';
import {ControlContainer} from '../control_container';
import {Form} from '../form_interface';
import {ReactiveErrors} from '../reactive_errors';
import {cleanUpControl, composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared';
import {FormControlName} from './form_control_name';
import {FormArrayName, FormGroupName} from './form_group_name';
export const formDirectiveProvider: any = {
provide: ControlContainer,
useExisting: forwardRef(() => FormGroupDirective)
};
/**
* @whatItDoes Binds an existing {@link FormGroup} to a DOM element.
*
* @howToUse
*
* This directive accepts an existing {@link FormGroup} instance. It will then use this
* {@link FormGroup} instance to match any child {@link FormControl}, {@link FormGroup},
* and {@link FormArray} instances to child {@link FormControlName}, {@link FormGroupName},
* and {@link FormArrayName} directives.
*
* **Set value**: You can set the form's initial value when instantiating the
* {@link FormGroup}, or you can set it programmatically later using the {@link FormGroup}'s
* {@link AbstractControl.setValue} or {@link AbstractControl.patchValue} methods.
*
* **Listen to value**: If you want to listen to changes in the value of the form, you can subscribe
* to the {@link FormGroup}'s {@link AbstractControl.valueChanges} event. You can also listen to
* its {@link AbstractControl.statusChanges} event to be notified when the validation status is
* re-calculated.
*
* Furthermore, you can listen to the directive's `ngSubmit` event to be notified when the user has
* triggered a form submission. The `ngSubmit` event will be emitted with the original form
* submission event.
*
* ### Example
*
* In this example, we create form controls for first name and last name.
*
* {@example forms/ts/simpleFormGroup/simple_form_group_example.ts region='Component'}
*
* **npm package**: `@angular/forms`
*
* **NgModule**: {@link ReactiveFormsModule}
*
* @stable
*/
@Directive({
selector: '[formGroup]',
providers: [formDirectiveProvider],
host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
exportAs: 'ngForm'
})
export class FormGroupDirective extends ControlContainer implements Form,
OnChanges {
private _submitted: boolean = false;
private _oldForm: FormGroup;
directives: FormControlName[] = [];
@Input('formGroup') form: FormGroup = null;
@Output() 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 (changes.hasOwnProperty('form')) {
this._updateValidators();
this._updateDomValue();
this._updateRegistrations();
}
}
get submitted(): boolean { return this._submitted; }
get formDirective(): Form { return this; }
get control(): FormGroup { return this.form; }
get path(): string[] { return []; }
addControl(dir: FormControlName): FormControl {
const ctrl: any = this.form.get(dir.path);
setUpControl(ctrl, dir);
ctrl.updateValueAndValidity({emitEvent: false});
this.directives.push(dir);
return ctrl;
}
getControl(dir: FormControlName): FormControl { return <FormControl>this.form.get(dir.path); }
removeControl(dir: FormControlName): void { remove(this.directives, dir); }
addFormGroup(dir: FormGroupName): void {
const ctrl: any = this.form.get(dir.path);
setUpFormContainer(ctrl, dir);
ctrl.updateValueAndValidity({emitEvent: false});
}
removeFormGroup(dir: FormGroupName): void {}
getFormGroup(dir: FormGroupName): FormGroup { return <FormGroup>this.form.get(dir.path); }
addFormArray(dir: FormArrayName): void {
const ctrl: any = this.form.get(dir.path);
setUpFormContainer(ctrl, dir);
ctrl.updateValueAndValidity({emitEvent: false});
}
removeFormArray(dir: FormArrayName): void {}
getFormArray(dir: FormArrayName): FormArray { return <FormArray>this.form.get(dir.path); }
updateModel(dir: FormControlName, value: any): void {
const ctrl  = <FormControl>this.form.get(dir.path);
ctrl.setValue(value);
}
onSubmit($event: Event): boolean {
this._submitted = true;
this.ngSubmit.emit($event);
return false;
}
onReset(): void { this.resetForm(); }
resetForm(value: any = undefined): void {
this.form.reset(value);
this._submitted = false;
}
/** @internal */
_updateDomValue() {
this.directives.forEach(dir => {
const newCtrl: any = this.form.get(dir.path);
if (dir._control !== newCtrl) {
cleanUpControl(dir._control, dir);
if (newCtrl) setUpControl(newCtrl, dir);
dir._control = newCtrl;
}
});
this.form._updateTreeValidity({emitEvent: false});
}
private _updateRegistrations() {
this.form._registerOnCollectionChange(() => this._updateDomValue());
if (this._oldForm) this._oldForm._registerOnCollectionChange(() => {});
this._oldForm = this.form;
}
private _updateValidators() {
const sync = composeValidators(this._validators);
this.form.validator = Validators.compose([this.form.validator, sync]);
const async = composeAsyncValidators(this._asyncValidators);
this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]);
}
private _checkFormPresent() {
if (!this.form) {
ReactiveErrors.missingFormException();
}
}
}
function remove<T>(list: T[], el: T): void {
const index = list.indexOf(el);
if (index > -1) {
list.splice(index, 1);
}
}

View File

@ -0,0 +1,200 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf, forwardRef} from '@angular/core';
import {FormArray} from '../../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {AbstractFormGroupDirective} from '../abstract_form_group_directive';
import {ControlContainer} from '../control_container';
import {ReactiveErrors} from '../reactive_errors';
import {composeAsyncValidators, composeValidators, controlPath} from '../shared';
import {AsyncValidatorFn, ValidatorFn} from '../validators';
import {FormGroupDirective} from './form_group_directive';
export const formGroupNameProvider: any = {
provide: ControlContainer,
useExisting: forwardRef(() => FormGroupName)
};
/**
* @whatItDoes Syncs a nested {@link FormGroup} to a DOM element.
*
* @howToUse
*
* This directive can only be used with a parent {@link FormGroupDirective} (selector:
* `[formGroup]`).
*
* It accepts the string name of the nested {@link FormGroup} you want to link, and
* will look for a {@link FormGroup} registered with that name in the parent
* {@link FormGroup} instance you passed into {@link FormGroupDirective}.
*
* Nested form groups can come in handy when you want to validate a sub-group of a
* form separately from the rest or when you'd like to group the values of certain
* controls into their own nested object.
*
* **Access the group**: You can access the associated {@link FormGroup} using the
* {@link AbstractControl.get} method. Ex: `this.form.get('name')`.
*
* You can also access individual controls within the group using dot syntax.
* Ex: `this.form.get('name.first')`
*
* **Get the value**: the `value` property is always synced and available on the
* {@link FormGroup}. See a full list of available properties in {@link AbstractControl}.
*
* **Set the value**: You can set an initial value for each child control when instantiating
* the {@link FormGroup}, or you can set it programmatically later using
* {@link AbstractControl.setValue} or {@link AbstractControl.patchValue}.
*
* **Listen to value**: If you want to listen to changes in the value of the group, you can
* subscribe to the {@link AbstractControl.valueChanges} event. You can also listen to
* {@link AbstractControl.statusChanges} to be notified when the validation status is
* re-calculated.
*
* ### Example
*
* {@example forms/ts/nestedFormGroup/nested_form_group_example.ts region='Component'}
*
* * **npm package**: `@angular/forms`
*
* * **NgModule**: `ReactiveFormsModule`
*
* @stable
*/
@Directive({selector: '[formGroupName]', providers: [formGroupNameProvider]})
export class FormGroupName extends AbstractFormGroupDirective implements OnInit, OnDestroy {
@Input('formGroupName') name: string;
constructor(
@Optional() @Host() @SkipSelf() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) {
super();
this._parent = parent;
this._validators = validators;
this._asyncValidators = asyncValidators;
}
/** @internal */
_checkParentType(): void {
if (_hasInvalidParent(this._parent)) {
ReactiveErrors.groupParentException();
}
}
}
export const formArrayNameProvider: any = {
provide: ControlContainer,
useExisting: forwardRef(() => FormArrayName)
};
/**
* @whatItDoes Syncs a nested {@link FormArray} to a DOM element.
*
* @howToUse
*
* This directive is designed to be used with a parent {@link FormGroupDirective} (selector:
* `[formGroup]`).
*
* It accepts the string name of the nested {@link FormArray} you want to link, and
* will look for a {@link FormArray} registered with that name in the parent
* {@link FormGroup} instance you passed into {@link FormGroupDirective}.
*
* Nested form arrays can come in handy when you have a group of form controls but
* you're not sure how many there will be. Form arrays allow you to create new
* form controls dynamically.
*
* **Access the array**: You can access the associated {@link FormArray} using the
* {@link AbstractControl.get} method on the parent {@link FormGroup}.
* Ex: `this.form.get('cities')`.
*
* **Get the value**: the `value` property is always synced and available on the
* {@link FormArray}. See a full list of available properties in {@link AbstractControl}.
*
* **Set the value**: You can set an initial value for each child control when instantiating
* the {@link FormArray}, or you can set the value programmatically later using the
* {@link FormArray}'s {@link AbstractControl.setValue} or {@link AbstractControl.patchValue}
* methods.
*
* **Listen to value**: If you want to listen to changes in the value of the array, you can
* subscribe to the {@link FormArray}'s {@link AbstractControl.valueChanges} event. You can also
* listen to its {@link AbstractControl.statusChanges} event to be notified when the validation
* status is re-calculated.
*
* **Add new controls**: You can add new controls to the {@link FormArray} dynamically by
* calling its {@link FormArray.push} method.
* Ex: `this.form.get('cities').push(new FormControl());`
*
* ### Example
*
* {@example forms/ts/nestedFormArray/nested_form_array_example.ts region='Component'}
*
* * **npm package**: `@angular/forms`
*
* * **NgModule**: `ReactiveFormsModule`
*
* @stable
*/
@Directive({selector: '[formArrayName]', providers: [formArrayNameProvider]})
export class FormArrayName extends ControlContainer implements OnInit, OnDestroy {
/** @internal */
_parent: ControlContainer;
/** @internal */
_validators: any[];
/** @internal */
_asyncValidators: any[];
@Input('formArrayName') name: string;
constructor(
@Optional() @Host() @SkipSelf() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) {
super();
this._parent = parent;
this._validators = validators;
this._asyncValidators = asyncValidators;
}
ngOnInit(): void {
this._checkParentType();
this.formDirective.addFormArray(this);
}
ngOnDestroy(): void {
if (this.formDirective) {
this.formDirective.removeFormArray(this);
}
}
get control(): FormArray { return this.formDirective.getFormArray(this); }
get formDirective(): FormGroupDirective {
return this._parent ? <FormGroupDirective>this._parent.formDirective : null;
}
get path(): string[] { return controlPath(this.name, this._parent); }
get validator(): ValidatorFn { return composeValidators(this._validators); }
get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); }
private _checkParentType(): void {
if (_hasInvalidParent(this._parent)) {
ReactiveErrors.arrayParentException();
}
}
}
function _hasInvalidParent(parent: ControlContainer): boolean {
return !(parent instanceof FormGroupName) && !(parent instanceof FormGroupDirective) &&
!(parent instanceof FormArrayName);
}

View File

@ -0,0 +1,77 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FormErrorExamples as Examples} from './error_examples';
export class ReactiveErrors {
static controlParentException(): void {
throw new Error(
`formControlName must be used with a parent formGroup directive. You'll want to add a formGroup
directive and pass it an existing FormGroup instance (you can create one in your class).
Example:
${Examples.formControlName}`);
}
static ngModelGroupException(): void {
throw new Error(
`formControlName cannot be used with an ngModelGroup parent. It is only compatible with parents
that also have a "form" prefix: formGroupName, formArrayName, or formGroup.
Option 1: Update the parent to be formGroupName (reactive form strategy)
${Examples.formGroupName}
Option 2: Use ngModel instead of formControlName (template-driven strategy)
${Examples.ngModelGroup}`);
}
static missingFormException(): void {
throw new Error(`formGroup expects a FormGroup instance. Please pass one in.
Example:
${Examples.formControlName}`);
}
static groupParentException(): void {
throw new Error(
`formGroupName must be used with a parent formGroup directive. You'll want to add a formGroup
directive and pass it an existing FormGroup instance (you can create one in your class).
Example:
${Examples.formGroupName}`);
}
static arrayParentException(): void {
throw new Error(
`formArrayName must be used with a parent formGroup directive. You'll want to add a formGroup
directive and pass it an existing FormGroup instance (you can create one in your class).
Example:
${Examples.formArrayName}`);
}
static disabledAttrWarning(): void {
console.warn(`
It looks like you're using the disabled attribute with a reactive form directive. If you set disabled to true
when you set up this control in your component class, the disabled attribute will actually be set in the DOM for
you. We recommend using this approach to avoid 'changed after checked' errors.
Example:
form = new FormGroup({
first: new FormControl({value: 'Nancy', disabled: true}, Validators.required),
last: new FormControl('Drew', Validators.required)
});
`);
}
}

View File

@ -0,0 +1,203 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Provider, Renderer, forwardRef, ɵlooseIdentical as looseIdentical} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
export const SELECT_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectControlValueAccessor),
multi: true
};
function _buildValueString(id: string, value: any): string {
if (id == null) return `${value}`;
if (value && typeof value === 'object') value = 'Object';
return `${id}: ${value}`.slice(0, 50);
}
function _extractId(valueString: string): string {
return valueString.split(':')[0];
}
/**
* @whatItDoes Writes values and listens to changes on a select element.
*
* Used by {@link NgModel}, {@link FormControlDirective}, and {@link FormControlName}
* to keep the view synced with the {@link FormControl} model.
*
* @howToUse
*
* If you have imported the {@link FormsModule} or the {@link ReactiveFormsModule}, this
* value accessor will be active on any select control that has a form directive. You do
* **not** need to add a special selector to activate it.
*
* ### How to use select controls with form directives
*
* To use a select in a template-driven form, simply add an `ngModel` and a `name`
* attribute to the main `<select>` tag.
*
* If your option values are simple strings, you can bind to the normal `value` property
* on the option. If your option values happen to be objects (and you'd like to save the
* selection in your form as an object), use `ngValue` instead:
*
* {@example forms/ts/selectControl/select_control_example.ts region='Component'}
*
* In reactive forms, you'll also want to add your form directive (`formControlName` or
* `formControl`) on the main `<select>` tag. Like in the former example, you have the
* choice of binding to the `value` or `ngValue` property on the select's options.
*
* {@example forms/ts/reactiveSelectControl/reactive_select_control_example.ts region='Component'}
*
* ### Caveat: Option selection
*
* Angular uses object identity to select option. It's possible for the identities of items
* to change while the data does not. This can happen, for example, if the items are produced
* from an RPC to the server, and that RPC is re-run. Even if the data hasn't changed, the
* second response will produce objects with different identities.
*
* To customize the default option comparison algorithm, `<select>` supports `compareWith` input.
* `compareWith` takes a **function** which has two arguments: `option1` and `option2`.
* If `compareWith` is given, Angular selects option by the return value of the function.
*
* #### Syntax
*
* ```
* <select [compareWith]="compareFn" [(ngModel)]="selectedCountries">
* <option *ngFor="let country of countries" [ngValue]="country">
* {{country.name}}
* </option>
* </select>
*
* compareFn(c1: Country, c2: Country): boolean {
* return c1 && c2 ? c1.id === c2.id : c1 === c2;
* }
* ```
*
* Note: We 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/
*
* * **npm package**: `@angular/forms`
*
* @stable
*/
@Directive({
selector:
'select:not([multiple])[formControlName],select:not([multiple])[formControl],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<string, any> = new Map<string, any>();
/** @internal */
_idCounter: number = 0;
onChange = (_: any) => {};
onTouched = () => {};
@Input()
set compareWith(fn: (o1: any, o2: any) => boolean) {
if (typeof fn !== 'function') {
throw new Error(`compareWith must be a function, but received ${JSON.stringify(fn)}`);
}
this._compareWith = fn;
}
private _compareWith: (o1: any, o2: any) => boolean = looseIdentical;
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: any): void {
this.value = value;
const id: string = this._getOptionId(value);
if (id == null) {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'selectedIndex', -1);
}
const valueString = _buildValueString(id, 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; }
setDisabledState(isDisabled: boolean): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
/** @internal */
_registerOption(): string { return (this._idCounter++).toString(); }
/** @internal */
_getOptionId(value: any): string {
for (const id of Array.from(this._optionMap.keys())) {
if (this._compareWith(this._optionMap.get(id), value)) return id;
}
return null;
}
/** @internal */
_getOptionValue(valueString: string): any {
const id: string = _extractId(valueString);
return this._optionMap.has(id) ? this._optionMap.get(id) : valueString;
}
}
/**
* @whatItDoes Marks `<option>` as dynamic, so Angular can be notified when options change.
*
* @howToUse
*
* See docs for {@link SelectControlValueAccessor} for usage examples.
*
* @stable
*/
@Directive({selector: 'option'})
export class NgSelectOption implements OnDestroy {
id: string;
constructor(
private _element: ElementRef, private _renderer: Renderer,
@Optional() @Host() private _select: SelectControlValueAccessor) {
if (this._select) this.id = this._select._registerOption();
}
@Input('ngValue')
set ngValue(value: any) {
if (this._select == null) return;
this._select._optionMap.set(this.id, value);
this._setElementValue(_buildValueString(this.id, value));
this._select.writeValue(this._select.value);
}
@Input('value')
set value(value: any) {
this._setElementValue(value);
if (this._select) this._select.writeValue(this._select.value);
}
/** @internal */
_setElementValue(value: string): void {
this._renderer.setElementProperty(this._element.nativeElement, 'value', value);
}
ngOnDestroy(): void {
if (this._select) {
this._select._optionMap.delete(this.id);
this._select.writeValue(this._select.value);
}
}
}

View File

@ -0,0 +1,226 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Provider, Renderer, forwardRef, ɵlooseIdentical as looseIdentical} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
export const SELECT_MULTIPLE_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectMultipleControlValueAccessor),
multi: true
};
function _buildValueString(id: string, value: any): string {
if (id == null) return `${value}`;
if (typeof value === 'string') value = `'${value}'`;
if (value && typeof value === 'object') value = 'Object';
return `${id}: ${value}`.slice(0, 50);
}
function _extractId(valueString: string): string {
return valueString.split(':')[0];
}
/** Mock interface for HTML Options */
interface HTMLOption {
value: string;
selected: boolean;
}
/** Mock interface for HTMLCollection */
abstract class HTMLCollection {
length: number;
abstract item(_: number): HTMLOption;
}
/**
* The accessor for writing a value and listening to changes on a select element.
*
* ### Caveat: Options selection
*
* Angular uses object identity to select options. It's possible for the identities of items
* to change while the data does not. This can happen, for example, if the items are produced
* from an RPC to the server, and that RPC is re-run. Even if the data hasn't changed, the
* second response will produce objects with different identities.
*
* To customize the default option comparison algorithm, `<select multiple>` supports `compareWith`
* input. `compareWith` takes a **function** which has two arguments: `option1` and `option2`.
* If `compareWith` is given, Angular selects options by the return value of the function.
*
* #### Syntax
*
* ```
* <select multiple [compareWith]="compareFn" [(ngModel)]="selectedCountries">
* <option *ngFor="let country of countries" [ngValue]="country">
* {{country.name}}
* </option>
* </select>
*
* compareFn(c1: Country, c2: Country): boolean {
* return c1 && c2 ? c1.id === c2.id : c1 === c2;
* }
* ```
*
* @stable
*/
@Directive({
selector:
'select[multiple][formControlName],select[multiple][formControl],select[multiple][ngModel]',
host: {'(change)': 'onChange($event.target)', '(blur)': 'onTouched()'},
providers: [SELECT_MULTIPLE_VALUE_ACCESSOR]
})
export class SelectMultipleControlValueAccessor implements ControlValueAccessor {
value: any;
/** @internal */
_optionMap: Map<string, NgSelectMultipleOption> = new Map<string, NgSelectMultipleOption>();
/** @internal */
_idCounter: number = 0;
onChange = (_: any) => {};
onTouched = () => {};
@Input()
set compareWith(fn: (o1: any, o2: any) => boolean) {
if (typeof fn !== 'function') {
throw new Error(`compareWith must be a function, but received ${JSON.stringify(fn)}`);
}
this._compareWith = fn;
}
private _compareWith: (o1: any, o2: any) => boolean = looseIdentical;
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: any): void {
this.value = value;
let optionSelectedStateSetter: (opt: NgSelectMultipleOption, o: any) => void;
if (Array.isArray(value)) {
// convert values to ids
const ids = value.map((v) => this._getOptionId(v));
optionSelectedStateSetter = (opt, o) => { opt._setSelected(ids.indexOf(o.toString()) > -1); };
} else {
optionSelectedStateSetter = (opt, o) => { opt._setSelected(false); };
}
this._optionMap.forEach(optionSelectedStateSetter);
}
registerOnChange(fn: (value: any) => any): void {
this.onChange = (_: any) => {
const selected: Array<any> = [];
if (_.hasOwnProperty('selectedOptions')) {
const options: HTMLCollection = _.selectedOptions;
for (let i = 0; i < options.length; i++) {
const opt: any = options.item(i);
const val: any = this._getOptionValue(opt.value);
selected.push(val);
}
}
// Degrade on IE
else {
const options: HTMLCollection = <HTMLCollection>_.options;
for (let i = 0; i < options.length; i++) {
const opt: HTMLOption = options.item(i);
if (opt.selected) {
const val: any = this._getOptionValue(opt.value);
selected.push(val);
}
}
}
this.value = selected;
fn(selected);
};
}
registerOnTouched(fn: () => any): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
/** @internal */
_registerOption(value: NgSelectMultipleOption): string {
const id: string = (this._idCounter++).toString();
this._optionMap.set(id, value);
return id;
}
/** @internal */
_getOptionId(value: any): string {
for (const id of Array.from(this._optionMap.keys())) {
if (this._compareWith(this._optionMap.get(id)._value, value)) return id;
}
return null;
}
/** @internal */
_getOptionValue(valueString: string): any {
const id: string = _extractId(valueString);
return this._optionMap.has(id) ? this._optionMap.get(id)._value : valueString;
}
}
/**
* Marks `<option>` as dynamic, so Angular can be notified when options change.
*
* ### Example
*
* ```
* <select multiple name="city" ngModel>
* <option *ngFor="let c of cities" [value]="c"></option>
* </select>
* ```
*/
@Directive({selector: 'option'})
export class NgSelectMultipleOption implements OnDestroy {
id: string;
/** @internal */
_value: any;
constructor(
private _element: ElementRef, private _renderer: Renderer,
@Optional() @Host() private _select: SelectMultipleControlValueAccessor) {
if (this._select) {
this.id = this._select._registerOption(this);
}
}
@Input('ngValue')
set ngValue(value: any) {
if (this._select == null) return;
this._value = value;
this._setElementValue(_buildValueString(this.id, value));
this._select.writeValue(this._select.value);
}
@Input('value')
set value(value: any) {
if (this._select) {
this._value = value;
this._setElementValue(_buildValueString(this.id, value));
this._select.writeValue(this._select.value);
} else {
this._setElementValue(value);
}
}
/** @internal */
_setElementValue(value: string): void {
this._renderer.setElementProperty(this._element.nativeElement, 'value', value);
}
/** @internal */
_setSelected(selected: boolean) {
this._renderer.setElementProperty(this._element.nativeElement, 'selected', selected);
}
ngOnDestroy(): void {
if (this._select) {
this._select._optionMap.delete(this.id);
this._select.writeValue(this._select.value);
}
}
}

View File

@ -0,0 +1,178 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵlooseIdentical as looseIdentical} from '@angular/core';
import {FormArray, FormControl, FormGroup} from '../model';
import {Validators} from '../validators';
import {AbstractControlDirective} from './abstract_control_directive';
import {AbstractFormGroupDirective} from './abstract_form_group_directive';
import {CheckboxControlValueAccessor} from './checkbox_value_accessor';
import {ControlContainer} from './control_container';
import {ControlValueAccessor} from './control_value_accessor';
import {DefaultValueAccessor} from './default_value_accessor';
import {NgControl} from './ng_control';
import {normalizeAsyncValidator, normalizeValidator} from './normalize_validator';
import {NumberValueAccessor} from './number_value_accessor';
import {RadioControlValueAccessor} from './radio_control_value_accessor';
import {RangeValueAccessor} from './range_value_accessor';
import {FormArrayName} from './reactive_directives/form_group_name';
import {SelectControlValueAccessor} from './select_control_value_accessor';
import {SelectMultipleControlValueAccessor} from './select_multiple_control_value_accessor';
import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from './validators';
export function controlPath(name: string, parent: ControlContainer): string[] {
return [...parent.path, name];
}
export function setUpControl(control: FormControl, dir: NgControl): void {
if (!control) _throwError(dir, 'Cannot find control with');
if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');
control.validator = Validators.compose([control.validator, dir.validator]);
control.asyncValidator = Validators.composeAsync([control.asyncValidator, dir.asyncValidator]);
dir.valueAccessor.writeValue(control.value);
// view -> model
dir.valueAccessor.registerOnChange((newValue: any) => {
dir.viewToModelUpdate(newValue);
control.markAsDirty();
control.setValue(newValue, {emitModelToViewChange: false});
});
// touched
dir.valueAccessor.registerOnTouched(() => control.markAsTouched());
control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
// control -> view
dir.valueAccessor.writeValue(newValue);
// control -> ngModel
if (emitModelEvent) dir.viewToModelUpdate(newValue);
});
if (dir.valueAccessor.setDisabledState) {
control.registerOnDisabledChange(
(isDisabled: boolean) => { dir.valueAccessor.setDisabledState(isDisabled); });
}
// re-run validation when validator binding changes, e.g. minlength=3 -> minlength=4
dir._rawValidators.forEach((validator: Validator | ValidatorFn) => {
if ((<Validator>validator).registerOnValidatorChange)
(<Validator>validator).registerOnValidatorChange(() => control.updateValueAndValidity());
});
dir._rawAsyncValidators.forEach((validator: AsyncValidator | AsyncValidatorFn) => {
if ((<Validator>validator).registerOnValidatorChange)
(<Validator>validator).registerOnValidatorChange(() => control.updateValueAndValidity());
});
}
export function cleanUpControl(control: FormControl, dir: NgControl) {
dir.valueAccessor.registerOnChange(() => _noControlError(dir));
dir.valueAccessor.registerOnTouched(() => _noControlError(dir));
dir._rawValidators.forEach((validator: any) => {
if (validator.registerOnValidatorChange) {
validator.registerOnValidatorChange(null);
}
});
dir._rawAsyncValidators.forEach((validator: any) => {
if (validator.registerOnValidatorChange) {
validator.registerOnValidatorChange(null);
}
});
if (control) control._clearChangeFns();
}
export function setUpFormContainer(
control: FormGroup | FormArray, dir: AbstractFormGroupDirective | FormArrayName) {
if (control == null) _throwError(dir, 'Cannot find control with');
control.validator = Validators.compose([control.validator, dir.validator]);
control.asyncValidator = Validators.composeAsync([control.asyncValidator, dir.asyncValidator]);
}
function _noControlError(dir: NgControl) {
return _throwError(dir, 'There is no FormControl instance attached to form control element with');
}
function _throwError(dir: AbstractControlDirective, message: string): void {
let messageEnd: string;
if (dir.path.length > 1) {
messageEnd = `path: '${dir.path.join(' -> ')}'`;
} else if (dir.path[0]) {
messageEnd = `name: '${dir.path}'`;
} else {
messageEnd = 'unspecified name attribute';
}
throw new Error(`${message} ${messageEnd}`);
}
export function composeValidators(validators: Array<Validator|Function>): ValidatorFn {
return validators != null ? Validators.compose(validators.map(normalizeValidator)) : null;
}
export function composeAsyncValidators(validators: Array<Validator|Function>): AsyncValidatorFn {
return validators != null ? Validators.composeAsync(validators.map(normalizeAsyncValidator)) :
null;
}
export function isPropertyUpdated(changes: {[key: string]: any}, viewModel: any): boolean {
if (!changes.hasOwnProperty('model')) return false;
const change = changes['model'];
if (change.isFirstChange()) return true;
return !looseIdentical(viewModel, change.currentValue);
}
const BUILTIN_ACCESSORS = [
CheckboxControlValueAccessor,
RangeValueAccessor,
NumberValueAccessor,
SelectControlValueAccessor,
SelectMultipleControlValueAccessor,
RadioControlValueAccessor,
];
export function isBuiltInAccessor(valueAccessor: ControlValueAccessor): boolean {
return BUILTIN_ACCESSORS.some(a => valueAccessor.constructor === a);
}
// TODO: vsavkin remove it once https://github.com/angular/angular/issues/3011 is implemented
export function selectValueAccessor(
dir: NgControl, valueAccessors: ControlValueAccessor[]): ControlValueAccessor {
if (!valueAccessors) return null;
let defaultAccessor: ControlValueAccessor;
let builtinAccessor: ControlValueAccessor;
let customAccessor: ControlValueAccessor;
valueAccessors.forEach((v: ControlValueAccessor) => {
if (v.constructor === DefaultValueAccessor) {
defaultAccessor = v;
} else if (isBuiltInAccessor(v)) {
if (builtinAccessor)
_throwError(dir, 'More than one built-in value accessor matches form control with');
builtinAccessor = v;
} else {
if (customAccessor)
_throwError(dir, 'More than one custom value accessor matches form control with');
customAccessor = v;
}
});
if (customAccessor) return customAccessor;
if (builtinAccessor) return builtinAccessor;
if (defaultAccessor) return defaultAccessor;
_throwError(dir, 'No valid value accessor for form control with');
return null;
}

View File

@ -0,0 +1,60 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FormErrorExamples as Examples} from './error_examples';
export class TemplateDrivenErrors {
static modelParentException(): void {
throw new Error(`
ngModel cannot be used to register form controls with a parent formGroup directive. Try using
formGroup's partner directive "formControlName" instead. Example:
${Examples.formControlName}
Or, if you'd like to avoid registering this form control, indicate that it's standalone in ngModelOptions:
Example:
${Examples.ngModelWithFormGroup}`);
}
static formGroupNameException(): void {
throw new Error(`
ngModel cannot be used to register form controls with a parent formGroupName or formArrayName directive.
Option 1: Use formControlName instead of ngModel (reactive strategy):
${Examples.formGroupName}
Option 2: Update ngModel's parent be ngModelGroup (template-driven strategy):
${Examples.ngModelGroup}`);
}
static missingNameException() {
throw new Error(
`If ngModel is used within a form tag, either the name attribute must be set or the form
control must be defined as 'standalone' in ngModelOptions.
Example 1: <input [(ngModel)]="person.firstName" name="first">
Example 2: <input [(ngModel)]="person.firstName" [ngModelOptions]="{standalone: true}">`);
}
static modelGroupParentException() {
throw new Error(`
ngModelGroup cannot be used with a parent formGroup directive.
Option 1: Use formGroupName instead of ngModelGroup (reactive strategy):
${Examples.formGroupName}
Option 2: Use a regular form tag instead of the formGroup directive (template-driven strategy):
${Examples.ngModelGroup}`);
}
}

View File

@ -0,0 +1,316 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, Input, OnChanges, Provider, SimpleChanges, forwardRef} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {AbstractControl} from '../model';
import {NG_VALIDATORS, Validators} from '../validators';
/**
* An interface that can be implemented by classes that can act as validators.
*
* ## Usage
*
* ```typescript
* @Directive({
* selector: '[custom-validator]',
* providers: [{provide: NG_VALIDATORS, useExisting: CustomValidatorDirective, multi: true}]
* })
* class CustomValidatorDirective implements Validator {
* validate(c: Control): {[key: string]: any} {
* return {"custom": true};
* }
* }
* ```
*
* @stable
*/
export interface Validator {
validate(c: AbstractControl): {[key: string]: any};
registerOnValidatorChange?(fn: () => void): void;
}
/** @experimental */
export interface AsyncValidator extends Validator {
validate(c: AbstractControl): Promise<{[key: string]: any}>|Observable<{[key: string]: any}>;
}
export const REQUIRED_VALIDATOR: Provider = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => RequiredValidator),
multi: true
};
export const CHECKBOX_REQUIRED_VALIDATOR: Provider = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CheckboxRequiredValidator),
multi: true
};
/**
* A Directive that adds the `required` validator to any controls marked with the
* `required` attribute, via the {@link NG_VALIDATORS} binding.
*
* ### Example
*
* ```
* <input name="fullName" ngModel required>
* ```
*
* @stable
*/
@Directive({
selector:
':not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]',
providers: [REQUIRED_VALIDATOR],
host: {'[attr.required]': 'required ? "" : null'}
})
export class RequiredValidator implements Validator {
private _required: boolean;
private _onChange: () => void;
@Input()
get required(): boolean /*| string*/ { return this._required; }
set required(value: boolean) {
this._required = value != null && value !== false && `${value}` !== 'false';
if (this._onChange) this._onChange();
}
validate(c: AbstractControl): {[key: string]: any} {
return this.required ? Validators.required(c) : null;
}
registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
}
/**
* A Directive that adds the `required` validator to checkbox controls marked with the
* `required` attribute, via the {@link NG_VALIDATORS} binding.
*
* ### Example
*
* ```
* <input type="checkbox" name="active" ngModel required>
* ```
*
* @experimental
*/
@Directive({
selector:
'input[type=checkbox][required][formControlName],input[type=checkbox][required][formControl],input[type=checkbox][required][ngModel]',
providers: [CHECKBOX_REQUIRED_VALIDATOR],
host: {'[attr.required]': 'required ? "" : null'}
})
export class CheckboxRequiredValidator extends RequiredValidator {
validate(c: AbstractControl): {[key: string]: any} {
return this.required ? Validators.requiredTrue(c) : null;
}
}
/**
* Provider which adds {@link EmailValidator} to {@link NG_VALIDATORS}.
*/
export const EMAIL_VALIDATOR: any = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => EmailValidator),
multi: true
};
/**
* A Directive that adds the `email` validator to controls marked with the
* `email` attribute, via the {@link NG_VALIDATORS} binding.
*
* ### Example
*
* ```
* <input type="email" name="email" ngModel email>
* <input type="email" name="email" ngModel email="true">
* <input type="email" name="email" ngModel [email]="true">
* ```
*
* @experimental
*/
@Directive({
selector: '[email][formControlName],[email][formControl],[email][ngModel]',
providers: [EMAIL_VALIDATOR]
})
export class EmailValidator implements Validator {
private _enabled: boolean;
private _onChange: () => void;
@Input()
set email(value: boolean|string) {
this._enabled = value === '' || value === true || value === 'true';
if (this._onChange) this._onChange();
}
validate(c: AbstractControl): {[key: string]: any} {
return this._enabled ? Validators.email(c) : null;
}
registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
}
/**
* @stable
*/
export interface ValidatorFn { (c: AbstractControl): {[key: string]: any}; }
/**
* @stable
*/
export interface AsyncValidatorFn {
(c: AbstractControl): any /*Promise<{[key: string]: any}>|Observable<{[key: string]: any}>*/;
}
/**
* Provider which adds {@link MinLengthValidator} to {@link NG_VALIDATORS}.
*
* ## Example:
*
* {@example common/forms/ts/validators/validators.ts region='min'}
*/
export const MIN_LENGTH_VALIDATOR: any = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MinLengthValidator),
multi: true
};
/**
* A directive which installs the {@link MinLengthValidator} for any `formControlName`,
* `formControl`, or control with `ngModel` that also has a `minlength` attribute.
*
* @stable
*/
@Directive({
selector: '[minlength][formControlName],[minlength][formControl],[minlength][ngModel]',
providers: [MIN_LENGTH_VALIDATOR],
host: {'[attr.minlength]': 'minlength ? minlength : null'}
})
export class MinLengthValidator implements Validator,
OnChanges {
private _validator: ValidatorFn;
private _onChange: () => void;
@Input() minlength: string;
ngOnChanges(changes: SimpleChanges): void {
if ('minlength' in changes) {
this._createValidator();
if (this._onChange) this._onChange();
}
}
validate(c: AbstractControl): {[key: string]: any} {
return this.minlength == null ? null : this._validator(c);
}
registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
private _createValidator(): void {
this._validator = Validators.minLength(parseInt(this.minlength, 10));
}
}
/**
* Provider which adds {@link MaxLengthValidator} to {@link NG_VALIDATORS}.
*
* ## Example:
*
* {@example common/forms/ts/validators/validators.ts region='max'}
*/
export const MAX_LENGTH_VALIDATOR: any = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MaxLengthValidator),
multi: true
};
/**
* A directive which installs the {@link MaxLengthValidator} for any `formControlName,
* `formControl`,
* or control with `ngModel` that also has a `maxlength` attribute.
*
* @stable
*/
@Directive({
selector: '[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]',
providers: [MAX_LENGTH_VALIDATOR],
host: {'[attr.maxlength]': 'maxlength ? maxlength : null'}
})
export class MaxLengthValidator implements Validator,
OnChanges {
private _validator: ValidatorFn;
private _onChange: () => void;
@Input() maxlength: string;
ngOnChanges(changes: SimpleChanges): void {
if ('maxlength' in changes) {
this._createValidator();
if (this._onChange) this._onChange();
}
}
validate(c: AbstractControl): {[key: string]: any} {
return this.maxlength != null ? this._validator(c) : null;
}
registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
private _createValidator(): void {
this._validator = Validators.maxLength(parseInt(this.maxlength, 10));
}
}
export const PATTERN_VALIDATOR: any = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => PatternValidator),
multi: true
};
/**
* A Directive that adds the `pattern` validator to any controls marked with the
* `pattern` attribute, via the {@link NG_VALIDATORS} binding. Uses attribute value
* as the regex to validate Control value against. Follows pattern attribute
* semantics; i.e. regex must match entire Control value.
*
* ### Example
*
* ```
* <input [name]="fullName" pattern="[a-zA-Z ]*" ngModel>
* ```
* @stable
*/
@Directive({
selector: '[pattern][formControlName],[pattern][formControl],[pattern][ngModel]',
providers: [PATTERN_VALIDATOR],
host: {'[attr.pattern]': 'pattern ? pattern : null'}
})
export class PatternValidator implements Validator,
OnChanges {
private _validator: ValidatorFn;
private _onChange: () => void;
@Input() pattern: string /*|RegExp*/;
ngOnChanges(changes: SimpleChanges): void {
if ('pattern' in changes) {
this._createValidator();
if (this._onChange) this._onChange();
}
}
validate(c: AbstractControl): {[key: string]: any} { return this._validator(c); }
registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
private _createValidator(): void { this._validator = Validators.pattern(this.pattern); }
}

View File

@ -0,0 +1,98 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injectable} from '@angular/core';
import {AsyncValidatorFn, ValidatorFn} from './directives/validators';
import {AbstractControl, FormArray, FormControl, FormGroup} from './model';
/**
* @whatItDoes Creates an {@link AbstractControl} from a user-specified configuration.
*
* It is essentially syntactic sugar that shortens the `new FormGroup()`,
* `new FormControl()`, and `new FormArray()` boilerplate that can build up in larger
* forms.
*
* @howToUse
*
* To use, inject `FormBuilder` into your component class. You can then call its methods
* directly.
*
* {@example forms/ts/formBuilder/form_builder_example.ts region='Component'}
*
* * **npm package**: `@angular/forms`
*
* * **NgModule**: {@link ReactiveFormsModule}
*
* @stable
*/
@Injectable()
export class FormBuilder {
/**
* Construct a new {@link FormGroup} with the given map of configuration.
* Valid keys for the `extra` parameter map are `validator` and `asyncValidator`.
*
* See the {@link FormGroup} constructor for more details.
*/
group(controlsConfig: {[key: string]: any}, extra: {[key: string]: any} = null): FormGroup {
const controls = this._reduceControls(controlsConfig);
const validator: ValidatorFn = extra != null ? extra['validator'] : null;
const asyncValidator: AsyncValidatorFn = extra != null ? extra['asyncValidator'] : null;
return new FormGroup(controls, validator, asyncValidator);
}
/**
* Construct a new {@link FormControl} with the given `formState`,`validator`, and
* `asyncValidator`.
*
* `formState` can either be a standalone value for the form control or an object
* that contains both a value and a disabled status.
*
*/
control(
formState: Object, validator: ValidatorFn|ValidatorFn[] = null,
asyncValidator: AsyncValidatorFn|AsyncValidatorFn[] = null): FormControl {
return new FormControl(formState, validator, asyncValidator);
}
/**
* Construct a {@link FormArray} from the given `controlsConfig` array of
* configuration, with the given optional `validator` and `asyncValidator`.
*/
array(
controlsConfig: any[], validator: ValidatorFn = null,
asyncValidator: AsyncValidatorFn = null): FormArray {
const controls = controlsConfig.map(c => this._createControl(c));
return new FormArray(controls, validator, asyncValidator);
}
/** @internal */
_reduceControls(controlsConfig: {[k: string]: any}): {[key: string]: AbstractControl} {
const controls: {[key: string]: AbstractControl} = {};
Object.keys(controlsConfig).forEach(controlName => {
controls[controlName] = this._createControl(controlsConfig[controlName]);
});
return controls;
}
/** @internal */
_createControl(controlConfig: any): AbstractControl {
if (controlConfig instanceof FormControl || controlConfig instanceof FormGroup ||
controlConfig instanceof FormArray) {
return controlConfig;
} else if (Array.isArray(controlConfig)) {
const value = controlConfig[0];
const validator: ValidatorFn = controlConfig.length > 1 ? controlConfig[1] : null;
const asyncValidator: AsyncValidatorFn = controlConfig.length > 2 ? controlConfig[2] : null;
return this.control(value, validator, asyncValidator);
} else {
return this.control(controlConfig);
}
}
}

View File

@ -0,0 +1,38 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NgModule} from '@angular/core';
import {InternalFormsSharedModule, REACTIVE_DRIVEN_DIRECTIVES, TEMPLATE_DRIVEN_DIRECTIVES} from './directives';
import {RadioControlRegistry} from './directives/radio_control_value_accessor';
import {FormBuilder} from './form_builder';
/**
* The ng module for forms.
* @stable
*/
@NgModule({
declarations: TEMPLATE_DRIVEN_DIRECTIVES,
providers: [RadioControlRegistry],
exports: [InternalFormsSharedModule, TEMPLATE_DRIVEN_DIRECTIVES]
})
export class FormsModule {
}
/**
* The ng module for reactive forms.
* @stable
*/
@NgModule({
declarations: [REACTIVE_DRIVEN_DIRECTIVES],
providers: [FormBuilder, RadioControlRegistry],
exports: [InternalFormsSharedModule, REACTIVE_DRIVEN_DIRECTIVES]
})
export class ReactiveFormsModule {
}

View File

@ -0,0 +1,46 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* @module
* @description
* This module is used for handling user input, by defining and building a {@link FormGroup} that
* consists of {@link FormControl} objects, and mapping them onto the DOM. {@link FormControl}
* 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 {AbstractControlDirective} from './directives/abstract_control_directive';
export {AbstractFormGroupDirective} from './directives/abstract_form_group_directive';
export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
export {ControlContainer} from './directives/control_container';
export {ControlValueAccessor, NG_VALUE_ACCESSOR} from './directives/control_value_accessor';
export {DefaultValueAccessor} from './directives/default_value_accessor';
export {Form} from './directives/form_interface';
export {NgControl} from './directives/ng_control';
export {NgControlStatus, NgControlStatusGroup} from './directives/ng_control_status';
export {NgForm} from './directives/ng_form';
export {NgModel} from './directives/ng_model';
export {NgModelGroup} from './directives/ng_model_group';
export {RadioControlValueAccessor} from './directives/radio_control_value_accessor';
export {FormControlDirective} from './directives/reactive_directives/form_control_directive';
export {FormControlName} from './directives/reactive_directives/form_control_name';
export {FormGroupDirective} from './directives/reactive_directives/form_group_directive';
export {FormArrayName} from './directives/reactive_directives/form_group_name';
export {FormGroupName} from './directives/reactive_directives/form_group_name';
export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
export {SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator, Validator, ValidatorFn} from './directives/validators';
export {FormBuilder} from './form_builder';
export {AbstractControl, FormArray, FormControl, FormGroup} from './model';
export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators';
export {VERSION} from './version';
export * from './form_providers';

1385
packages/forms/src/model.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,211 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {InjectionToken, ɵisPromise as isPromise, ɵmerge as merge} from '@angular/core';
import {toPromise} from 'rxjs/operator/toPromise';
import {AsyncValidatorFn, Validator, ValidatorFn} from './directives/validators';
import {AbstractControl, FormControl, FormGroup} from './model';
function isEmptyInputValue(value: any): boolean {
// we don't check for string here so it also works with arrays
return value == null || value.length === 0;
}
/**
* Providers for validators to be used for {@link FormControl}s in a form.
*
* Provide this using `multi: true` to add validators.
*
* ### Example
*
* {@example core/forms/ts/ng_validators/ng_validators.ts region='ng_validators'}
* @stable
*/
export const NG_VALIDATORS = new InjectionToken<Array<Validator|Function>>('NgValidators');
/**
* Providers for asynchronous validators to be used for {@link FormControl}s
* in a form.
*
* Provide this using `multi: true` to add validators.
*
* See {@link NG_VALIDATORS} for more details.
*
* @stable
*/
export const NG_ASYNC_VALIDATORS =
new InjectionToken<Array<Validator|Function>>('NgAsyncValidators');
const EMAIL_REGEXP =
/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
/**
* Provides a set of validators used by form controls.
*
* A validator is a function that processes a {@link FormControl} or collection of
* controls and returns a map of errors. A null map means that validation has passed.
*
* ### Example
*
* ```typescript
* var loginControl = new FormControl("", Validators.required)
* ```
*
* @stable
*/
export class Validators {
/**
* Validator that compares the value of the given FormControls
*/
static equalsTo(...fieldPaths: string[]): ValidatorFn {
return function(control: FormControl): {[key: string]: any} {
if (fieldPaths.length < 1) {
throw new Error('You must compare to at least 1 other field');
}
for (let fieldName of fieldPaths) {
let field = (<FormGroup>control.parent).get(fieldName);
if (!field) {
throw new Error(
`Field: ${fieldName} undefined, are you sure that ${fieldName} exists in the group`);
}
if (field.value !== control.value) {
return {'equalsTo': {'unequalField': fieldName}};
}
}
return null;
};
}
/**
* Validator that requires controls to have a non-empty value.
*/
static required(control: AbstractControl): {[key: string]: boolean} {
return isEmptyInputValue(control.value) ? {'required': true} : null;
}
/**
* Validator that requires control value to be true.
*/
static requiredTrue(control: AbstractControl): {[key: string]: boolean} {
return control.value === true ? null : {'required': true};
}
/**
* Validator that performs email validation.
*/
static email(control: AbstractControl): {[key: string]: boolean} {
return EMAIL_REGEXP.test(control.value) ? null : {'email': true};
}
/**
* Validator that requires controls to have a value of a minimum length.
*/
static minLength(minLength: number): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
if (isEmptyInputValue(control.value)) {
return null; // don't validate empty values to allow optional controls
}
const length: number = control.value ? control.value.length : 0;
return length < minLength ?
{'minlength': {'requiredLength': minLength, 'actualLength': length}} :
null;
};
}
/**
* Validator that requires controls to have a value of a maximum length.
*/
static maxLength(maxLength: number): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
const length: number = control.value ? control.value.length : 0;
return length > maxLength ?
{'maxlength': {'requiredLength': maxLength, 'actualLength': length}} :
null;
};
}
/**
* Validator that requires a control to match a regex to its value.
*/
static pattern(pattern: string|RegExp): ValidatorFn {
if (!pattern) return Validators.nullValidator;
let regex: RegExp;
let regexStr: string;
if (typeof pattern === 'string') {
regexStr = `^${pattern}$`;
regex = new RegExp(regexStr);
} else {
regexStr = pattern.toString();
regex = pattern;
}
return (control: AbstractControl): {[key: string]: any} => {
if (isEmptyInputValue(control.value)) {
return null; // don't validate empty values to allow optional controls
}
const value: string = control.value;
return regex.test(value) ? null :
{'pattern': {'requiredPattern': regexStr, 'actualValue': value}};
};
}
/**
* No-op validator.
*/
static nullValidator(c: AbstractControl): {[key: string]: boolean} { return null; }
/**
* Compose multiple validators into a single function that returns the union
* of the individual error maps.
*/
static compose(validators: ValidatorFn[]): ValidatorFn {
if (!validators) return null;
const presentValidators = validators.filter(isPresent);
if (presentValidators.length == 0) return null;
return function(control: AbstractControl) {
return _mergeErrors(_executeValidators(control, presentValidators));
};
}
static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn {
if (!validators) return null;
const presentValidators = validators.filter(isPresent);
if (presentValidators.length == 0) return null;
return function(control: AbstractControl) {
const promises = _executeAsyncValidators(control, presentValidators).map(_convertToPromise);
return Promise.all(promises).then(_mergeErrors);
};
}
}
function isPresent(o: any): boolean {
return o != null;
}
function _convertToPromise(obj: any): Promise<any> {
return isPromise(obj) ? obj : toPromise.call(obj);
}
function _executeValidators(control: AbstractControl, validators: ValidatorFn[]): any[] {
return validators.map(v => v(control));
}
function _executeAsyncValidators(control: AbstractControl, validators: AsyncValidatorFn[]): any[] {
return validators.map(v => v(control));
}
function _mergeErrors(arrayOfErrors: any[]): {[key: string]: any} {
const res: {[key: string]: any} =
arrayOfErrors.reduce((res: {[key: string]: any}, errors: {[key: string]: any}) => {
return errors != null ? merge(res, errors) : res;
}, {});
return Object.keys(res).length === 0 ? null : res;
}

View File

@ -0,0 +1,19 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* @module
* @description
* Entry point for all public APIs of the common package.
*/
import {Version} from '@angular/core';
/**
* @stable
*/
export const VERSION = new Version('0.0.0-PLACEHOLDER');