refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
81
packages/forms/src/directives.ts
Normal file
81
packages/forms/src/directives.ts
Normal 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 {
|
||||
}
|
61
packages/forms/src/directives/abstract_control_directive.ts
Normal file
61
packages/forms/src/directives/abstract_control_directive.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
}
|
50
packages/forms/src/directives/checkbox_value_accessor.ts
Normal file
50
packages/forms/src/directives/checkbox_value_accessor.ts
Normal 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);
|
||||
}
|
||||
}
|
32
packages/forms/src/directives/control_container.ts
Normal file
32
packages/forms/src/directives/control_container.ts
Normal 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; }
|
||||
}
|
52
packages/forms/src/directives/control_value_accessor.ts
Normal file
52
packages/forms/src/directives/control_value_accessor.ts
Normal 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');
|
56
packages/forms/src/directives/default_value_accessor.ts
Normal file
56
packages/forms/src/directives/default_value_accessor.ts
Normal 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);
|
||||
}
|
||||
}
|
64
packages/forms/src/directives/error_examples.ts
Normal file
64
packages/forms/src/directives/error_examples.ts
Normal 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>
|
||||
`
|
||||
};
|
||||
|
58
packages/forms/src/directives/form_interface.ts
Normal file
58
packages/forms/src/directives/form_interface.ts
Normal 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;
|
||||
}
|
41
packages/forms/src/directives/ng_control.ts
Normal file
41
packages/forms/src/directives/ng_control.ts
Normal 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;
|
||||
}
|
63
packages/forms/src/directives/ng_control_status.ts
Normal file
63
packages/forms/src/directives/ng_control_status.ts
Normal 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); }
|
||||
}
|
158
packages/forms/src/directives/ng_form.ts
Normal file
158
packages/forms/src/directives/ng_form.ts
Normal 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;
|
||||
}
|
||||
}
|
241
packages/forms/src/directives/ng_model.ts
Normal file
241
packages/forms/src/directives/ng_model.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
68
packages/forms/src/directives/ng_model_group.ts
Normal file
68
packages/forms/src/directives/ng_model_group.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
29
packages/forms/src/directives/ng_no_validate_directive.ts
Normal file
29
packages/forms/src/directives/ng_no_validate_directive.ts
Normal 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 {
|
||||
}
|
27
packages/forms/src/directives/normalize_validator.ts
Normal file
27
packages/forms/src/directives/normalize_validator.ts
Normal 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;
|
||||
}
|
||||
}
|
58
packages/forms/src/directives/number_value_accessor.ts
Normal file
58
packages/forms/src/directives/number_value_accessor.ts
Normal 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);
|
||||
}
|
||||
}
|
153
packages/forms/src/directives/radio_control_value_accessor.ts
Normal file
153
packages/forms/src/directives/radio_control_value_accessor.ts
Normal 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">
|
||||
`);
|
||||
}
|
||||
}
|
57
packages/forms/src/directives/range_value_accessor.ts
Normal file
57
packages/forms/src/directives/range_value_accessor.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
77
packages/forms/src/directives/reactive_errors.ts
Normal file
77
packages/forms/src/directives/reactive_errors.ts
Normal 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)
|
||||
});
|
||||
`);
|
||||
}
|
||||
}
|
203
packages/forms/src/directives/select_control_value_accessor.ts
Normal file
203
packages/forms/src/directives/select_control_value_accessor.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
178
packages/forms/src/directives/shared.ts
Normal file
178
packages/forms/src/directives/shared.ts
Normal 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;
|
||||
}
|
60
packages/forms/src/directives/template_driven_errors.ts
Normal file
60
packages/forms/src/directives/template_driven_errors.ts
Normal 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}`);
|
||||
}
|
||||
}
|
316
packages/forms/src/directives/validators.ts
Normal file
316
packages/forms/src/directives/validators.ts
Normal 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); }
|
||||
}
|
98
packages/forms/src/form_builder.ts
Normal file
98
packages/forms/src/form_builder.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
38
packages/forms/src/form_providers.ts
Normal file
38
packages/forms/src/form_providers.ts
Normal 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 {
|
||||
}
|
46
packages/forms/src/forms.ts
Normal file
46
packages/forms/src/forms.ts
Normal 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
1385
packages/forms/src/model.ts
Normal file
File diff suppressed because it is too large
Load Diff
211
packages/forms/src/validators.ts
Normal file
211
packages/forms/src/validators.ts
Normal 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;
|
||||
}
|
19
packages/forms/src/version.ts
Normal file
19
packages/forms/src/version.ts
Normal 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');
|
Reference in New Issue
Block a user