在 Angular 4.x 中对于使用 Template-Driven 表单场景,如果需要实现表单数据绑定。我们就需要引入 ngModel
指令。该指令用于基于 domain 模型,创建 FormControl
实例,并将创建的实例绑定到表单控件元素上。
ngModel 使用示例
ngModel
app.component.ts
@Component({ selector: 'exe-app',template: ` <form novalidate #f="ngForm"> Name: <input type="text" name="username" ngModel> </form> {{ f.value | json }} `,}) export class AppComponent implements OnInit { }
在 <form>
表单中使用 ngModel
时,我们需要设置一个 name
属性,以便该控件可以使用该名称在表单中进行注册。
单向绑定 - [ngModel]
app.component.ts
@Component({ selector: 'exe-app',template: ` <form novalidate #f="ngForm"> Name: <input type="text" name="username" [ngModel]="user.username"> </form> {{ user | json }} `,}) export class AppComponent implements OnInit { user: { username: string }; ngOnInit() { this.user = { username: 'Semlinker' }; } }
双向绑定 - [(ngModel)]
表单中应用
app.component.ts
@Component({ selector: 'exe-app',template: ` <form novalidate #f="ngForm"> Name: <input type="text" name="username" [(ngModel)]="user.username"> </form> {{ user | json }} `,}) export class AppComponent implements OnInit { user: { username: string }; ngOnInit() { this.user = { username: 'Semlinker' }; } }
单独应用
import { Component } from '@angular/core'; @Component({ selector: 'exe-app',template: ` <input name="username" [(ngModel)]="username"> {{username}} `,}) export class AppComponent { username: string; }
ngModelOptions - [ngModelOptions]
当你在使用 ngModel 时未设置 name 属性,如下所示:
<form novalidate #f="ngForm"> Name: <input type="text" [(ngModel)]="user.username"> </form>
当你运行时,浏览器控制台将会抛出以下异常信息:
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.
以上异常信息告诉我们,如果在表单标签中使用 ngModel,则必须设置 name 属性,或者在 ngModelOptions 中必须将表单控件定义为 "standalone"。依据上述异常信息,我们做如下调整:
<form novalidate #f="ngForm"> Name: <input type="text" [(ngModel)]="user.username" [ngModelOptions]="{standalone: true}"> </form>
接下来我们看一下 ngModelOptions 支持的对象类型:
@Input('ngModelOptions') options: {name?: string,standalone?: boolean};
禁用控件 - disabled
<form novalidate #f="ngForm"> Name: <input type="text" name="username" [(ngModel)]="user.username" disabled="true"> </form>
监听 ngModelChange 事件 - (ngModelChange)
app.component.ts
@Component({ selector: 'exe-app',template: ` <form novalidate #f="ngForm"> Name: <input type="text" name="username" (ngModelChange)="userNameChange($event)" [(ngModel)]="user.username"> </form> {{ user | json }} `,}) export class AppComponent implements OnInit { user: { username: string }; ngOnInit() { this.user = { username: 'Semlinker' }; } userNameChange(name: string) { console.log(name); } }
获取关联的 NgModel 对象
app.component.ts
@Component({ selector: 'exe-app',template: ` <form novalidate #f="ngForm"> Name: <input type="text" name="username" #userName="ngModel" [(ngModel)]="user.username"> </form> {{ userName.control | json }} `,}) export class AppComponent implements OnInit { user: { username: string }; ngOnInit() { this.user = { username: 'Semlinker' }; } }
通过使用 userName="ngModel"
方式,我们可以获取表单控件关联的 NgModel 对象,进而获取控件当前控件的相关信息,如控件的当前的状态或控件验证信息等。
完整示例
import { Component } from '@angular/core'; import { NgForm } from '@angular/forms'; @Component({ selector: 'exe-app',template: ` <form #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate> <input name="first" ngModel required #first="ngModel"> <input name="last" ngModel> <button>Submit</button> </form> <p>First name value: {{ first.value }}</p> <p>First name valid: {{ first.valid }}</p> <p>Form value: {{ f.value | json }}</p> <p>Form valid: {{ f.valid }}</p> `,}) export class AppComponent { onSubmit(f: NgForm) { console.log(f.value); // { first: '',last: '' } console.log(f.valid); // false } }
ngModel 指令详解
ngModel 指令定义
@Directive({ selector: '[ngModel]:not([formControlName]):not([formControl])',providers: [formControlBinding],exportAs: 'ngModel' })
formControlBinding 定义
export const formControlBinding: any = { provide: NgControl,useExisting: forwardRef(() => NgModel) };
相关说明
selector 中
[ngModel]:not([formControlName]):not([formControl])
表示该指令只应用于 Template-Driven 表单中。exportAs - 表示可以使用
first="ngModel"
语法获取 NgModel 对象
ngModel 指令输入与输出属性
输入属性
@Input() name: string; @Input('disabled') isDisabled: boolean; @Input('ngModel') model: any; @Input('ngModelOptions') options: {name?: string,standalone?: boolean};
输出属性
@Output('ngModelChange') update = new EventEmitter();
NgModel 类
// angular2/packages/forms/src/directives/ng_model.ts export class NgModel extends NgControl implements OnChanges,OnDestroy { /** @internal */ _control = new FormControl(); // 创建FormControl对象 /** @internal */ _registered = false; // 用于标识控件是否已注册 viewmodel: any; // 用于保存前一次model的值 ... }
NgModel 构造函数
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); }
相关说明
@Optional() - 表示该依赖对象是可选的
@Host() - 表示从宿主元素注入器获取依赖对象
@Self() - 表示从当前注入器获取依赖对象
@Inject() - 用于注入 Token (new InjectionToken) 对应的非 Type 类型依赖对象
-
构造函数执行的操作:
NgModel 生命周期钩子
ngOnChanges
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; } }
_checkForErrors()
private _checkForErrors(): void { if (!this._isStandalone()) { this._checkParentType(); } this._checkName(); } // 判断是否设置standalone属性 private _isStandalone(): boolean { return !this._parent || (this.options && this.options.standalone); } /** * 1.ngModel指令不能与formGroupName或formArrayName指令一起使用,需改用 * formControlName或调整ngModel的父控件使用的指令为ngModelGroup。 * * 2.ngModel不能被注册到使用formGroup指令的表单中,需改用formControlName或设置 * ngModelOptions对象中的standalone属性,避免注册该控件。 */ 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(); } } /** * 验证是否设置name属性 * * 如果在表单标签中使用 ngModel,则必须设置 name 属性,或者在ngModelOptions中必须将 * 表单控件定义为"standalone"。 * * <input [(ngModel)]="person.firstName" [ngModelOptions]="{standalone: * true}"> */ private _checkName(): void { if (this.options && this.options.name) this.name = this.options.name; if (!this._isStandalone() && !this.name) { TemplateDrivenErrors.missingNameException(); } }
_setUpControl()
// 初始化控件 private _setUpControl(): void { this._isStandalone() ? this._setUpStandalone() : // 在ControlContainer所属的form中注册该控件 this.formDirective.addControl(this); this._registered = true; // 标识已注册 } // 若设置standalone属性,则初始化该控件,并更新控件的值和验证状态 private _setUpStandalone(): void { setUpControl(this._control,this); this._control.updateValueAndValidity({emitEvent: false}); } // 获取ControlContainer所属的form get formDirective(): any { return this._parent ? this._parent.formDirective : null; }
_updateDisabled()
若设置 isDisabled
输入属性,则更新控件的 disabled 属性:
// 更新控件的disabled状态 private _updateDisabled(changes: SimpleChanges) { // 获取disabled输入属性的当前值 const disabledValue = changes['isDisabled'].currentValue; // 判断是否设置为disabled 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(); // 启用控件 } }); }
isPropertyUpdated()
// 判断属性是否更新 export function isPropertyUpdated(changes: {[key: string]: any},viewmodel: any): boolean { if (!changes.hasOwnProperty('model')) return false; // @Input('ngModel') model: any; const change = changes['model']; if (change.isFirstChange()) return true; // 判断是否首次改变 return !looseIdentical(viewmodel,change.currentValue); } // JS has NaN !== NaN export function looseIdentical(a: any,b: any): boolean { return a === b || typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b); }
_updateValue()
// 更新控件的值 private _updateValue(value: any): void { resolvedPromise.then( () => { this.control.setValue(value,{emitViewToModelChange: false}); }); } const resolvedPromise = Promise.resolve(null);
ngOnDestroy()
// 指令销毁时,从formDirective中移除该控件 ngOnDestroy(): void { this.formDirective && this.formDirective.removeControl(this); }
NgModel 方法
get control(): FormControl
// 获取控件 get control(): FormControl { return this._control; } /** @internal */ _control = new FormControl();
get path(): string[]
// 获取控件的访问路径 get path(): string[] { return this._parent ? controlPath(this.name,this._parent) : [this.name]; }
get validator(): ValidatorFn
// 获取同步验证器 get validator(): ValidatorFn { return composeValidators(this._rawValidators); } export interface ValidatorFn { (c: AbstractControl): ValidationErrors|null; }
get asyncValidator(): AsyncValidatorFn
// 获取异步验证器 get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._rawAsyncValidators); } export interface AsyncValidatorFn { (c: AbstractControl): Promise<ValidationErrors|null>|Observable<ValidationErrors|null>; }
viewToModelUpdate(newValue: any): void
// 触发ngModelChange事件 viewToModelUpdate(newValue: any): void { this.viewmodel = newValue; // @Output('ngModelChange') update = new EventEmitter(); this.update.emit(newValue); }
NgControl 抽象类
// angular2/packages/forms/src/directives/ng_control.ts // 所有控件指令都需继承的基类,绑定FormControl对象至DOM元素 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; }
AbstractControlDirective 抽象类
// angular2/packages/forms/src/directives/abstract_control_directive.ts export abstract class AbstractControlDirective { // 获取控件 get control(): AbstractControl { throw new Error('unimplemented'); } // 获取控件的值 get value(): any { return this.control ? this.control.value : null; } // 控件控件的验证状态 - valid、invalid、pending 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 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 errors(): ValidationErrors|null { return this.control ? this.control.errors : null; } // 获取statusChanges对象 get statusChanges(): Observable<any> { return this.control ? this.control.statusChanges : null; } // 获取valueChanges对象 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); } // 判断是否path路径对应的控件,是否存在errorCode对应的错误 hasError(errorCode: string,path: string[] = null): boolean { return this.control ? this.control.hasError(errorCode,path) : false; } // 获取path路径对应的控件,参数errorCode对应的错误 getError(errorCode: string,path: string[] = null): any { return this.control ? this.control.getError(errorCode,path) : null; } }
input 指令
input 指令定义
@Directive({ selector:` input:not([type=checkBox])[formControlName],textarea[formControlName],input:not([type=checkBox])[formControl],textarea[formControl],input:not([type=checkBox])[ngModel],textarea[ngModel],[ngDefaultControl] `,host: { '(input)': '_handleInput($event.target.value)','(blur)': 'onTouched()','(compositionstart)': '_compositionStart()','(compositionend)': '_compositionEnd($event.target.value)' },providers: [DEFAULT_VALUE_ACCESSOR] })
相关说明
compositionstart - 事件触发于一段文字的输入之前 (类似于
keydown
事件,但是该事件仅在若干可见字符的输入之前,而这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)。compositionend - 事件触发于完成文本段落输入或取消输入
compositionstart、compositionend 的实际应用,请参考 - 应对中文输入法的字符串截断方案
DEFAULT_VALUE_ACCESSOR
export const DEFAULT_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR,useExisting: forwardRef(() => DefaultValueAccessor),multi: true };
DefaultValueAccessor
export class DefaultValueAccessor implements ControlValueAccessor { onChange = (_: any) => {}; onTouched = () => {}; /** Whether the user is creating a composition string (IME events). */ private _composing = false; constructor( private _renderer: Renderer,// 注入Renderer对象 private _elementRef: ElementRef,@Optional() @Inject(COMPOSITION_BUFFER_MODE) private _compositionMode: boolean) { if (this._compositionMode == null) { this._compositionMode = !_isAndroid(); } } // 将模型中的新值写入视图或DOM元素属性中 writeValue(value: any): void { const normalizedValue = value == null ? '' : value; this._renderer.setElementProperty(this._elementRef.nativeElement,'value',normalizedValue); } // 设置当控件接收到change事件后,调用的函数 registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } // 设置当控件接收到touched事件后,调用的函数 registerOnTouched(fn: () => void): void { this.onTouched = fn; } // 设置控件的Disabled状态 setDisabledState(isDisabled: boolean): void { this._renderer.setElementProperty(this._elementRef.nativeElement,'disabled',isDisabled); } // 处理input事件 _handleInput(value: any): void { if (!this._compositionMode || (this._compositionMode && !this._composing)) { this.onChange(value); } } // 处理compositionstart事件 _compositionStart(): void { this._composing = true; } // 处理compositionend事件 _compositionEnd(value: any): void { this._composing = false; this._compositionMode && this.onChange(value); } } export const COMPOSITION_BUFFER_MODE = new InjectionToken<boolean> ('CompositionEventMode'); // 用于判断是否处于安卓平台,composition事件在iOS和Android存在兼容性 function _isAndroid(): boolean { const userAgent = getDOM() ? getDOM().getUserAgent() : ''; return /android (\d+)/.test(userAgent.toLowerCase()); }
相关说明
为了能够支持跨平台,Angular 通过抽象层封装了不同平台的差异,统一了 API 接口。如定义了抽象类 Renderer 、抽象类 RootRenderer 等。此外还定义了以下引用类型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。
了解详细的信息,请查看 - Angular 2 ElementRef
另外看完上面的代码,不知道读者有没有以下的疑问:
为了解开这些疑惑我们就需要分析一下,一个很重要的方法 - setUpControl()。我们先来看一下 setUpControl() 的调用的时机点:
NgModel ngOnChanges 生命周期钩子
ngOnChanges(changes: SimpleChanges) { ... if (!this._registered) this._setUpControl(); ... }
_setUpControl() 方法
private _setUpControl(): void { this._isStandalone() ? this._setUpStandalone() : // 在ControlContainer所属的form中注册该控件 this.formDirective.addControl(this); this._registered = true; // 标识已注册 }
_setUpControl() 方法内部,先判断控件有设置 standalone 属性,如果有的话,则调用 _setUpStandalone() 方法:
// 若设置standalone属性,则初始化该控件,并更新控件的值和验证状态 private _setUpStandalone(): void { setUpControl(this._control,this); // 调用时机点一 this._control.updateValueAndValidity({emitEvent: false}); }
如果没有设置 standalone 属性,则调用 this.formDirective.addControl(this)
,这个方法存在于我们的 form
指令中,我们直接看一下具体实现:
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}); }); }
搞清楚 setUpControl() 调用的时机点,是时候分析一下 setUpControl() 方法的具体实现了。
setUpControl()
// angular2/packages/forms/src/directives/shared.ts export function setUpControl(control: FormControl,dir: NgControl): void { if (!control) _throwError(dir,'Cannot find control with'); /** * NgModel构造函数 * @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[] * this.valueAccessor = selectValueAccessor(this,valueAccessors); */ // 判断控件是否实现ControlValueAccessor接口 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]); // 该方法用于将模型中的新值写入视图或 DOM 属性中 dir.valueAccessor.writeValue(control.value); // view -> model /** * @Directive({ * selector: 'input:not([type=checkBox])[formControlName],...',* host: { * '(input)': '_handleInput($event.target.value)' * },* providers: [DEFAULT_VALUE_ACCESSOR] * }) * export class DefaultValueAccessor implements ControlValueAccessor { * // 下面就是调用该方法 * registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } * * // input事件触发后,调用该方法 * _handleInput(value: any): void { * if (!this._compositionMode || (this._compositionMode && !this._composing)) { * this.onChange(value); //调用下面注册的onChange函数 * } * } * } * */ dir.valueAccessor.registerOnChange((newValue: any) => { /** * ngModel指令 - viewToModelUpdate() 方法 * * viewToModelUpdate(newValue: any): void { * this.viewmodel = newValue; // 更新viewmodel * // @Output('ngModelChange') update = new EventEmitter(); * this.update.emit(newValue); // 触发ngModelChange事件 * } */ dir.viewToModelUpdate(newValue); control.markAsDirty(); /* * setValue(value: any,{onlySelf,emitEvent,emitModelToViewChange,* emitViewToModelChange}: { * onlySelf?: boolean,* emitEvent?: boolean,* emitModelToViewChange?: boolean,* emitViewToModelChange?: boolean * } = {}): void { * this._value = value; * if (this._onChange.length && emitModelToViewChange !== false) { * this._onChange.forEach((changeFn) => changeFn(this._value,* emitViewToModelChange !== false)); * } * this.updateValueAndValidity({onlySelf,emitEvent}); * } */ control.setValue(newValue,{emitModelToViewChange: false}); // 更新控件的值 }); // touched dir.valueAccessor.registerOnTouched(() => control.markAsTouched()); /** * control = new FormControl(); * * control - _onChange 属性 * _onChange: Function[] = []; * * control - registerOnChange() 方法 * registerOnChange(fn: Function): void { this._onChange.push(fn); } */ control.registerOnChange((newValue: any,emitModelEvent: boolean) => { // control -> view /* * writeValue(value: any): void { * const normalizedValue = value == null ? '' : value; * this._renderer.setElementProperty(this._elementRef.nativeElement,* normalizedValue); * } */ dir.valueAccessor.writeValue(newValue); // control -> ngModel /** * ngModel指令 - viewToModelUpdate() 方法 * * viewToModelUpdate(newValue: any): void { * this.viewmodel = newValue; // 更新viewmodel * // @Output('ngModelChange') update = new EventEmitter(); * this.update.emit(newValue); // 触发ngModelChange事件 * } */ if (emitModelEvent) dir.viewToModelUpdate(newValue); }); // 当控件状态变成 DISABLED 或从 DISABLED 状态变化成 ENABLE 状态时,会调用该函数。该函数会根据参数 // 值,启用或禁用指定的 DOM 元素 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()); }); }
最后我们再看一下 ControlValueAccessor 接口:
ControlValueAccessor
// angular2/packages/forms/src/directives/control_value_accessor.ts export interface ControlValueAccessor { writeValue(obj: any): void; registerOnChange(fn: any): void; registerOnTouched(fn: any): void; setDisabledState?(isDisabled: boolean): void; }
setDisabledState?(isDisabled: boolean):当控件状态变成
DISABLED
或从DISABLED
状态变化成ENABLE
状态时,会调用该函数。该函数会根据参数值,启用或禁用指定的 DOM 元素
了解 ControlValueAccessor 的详细信息,可以参考 - Understanding ControlValueAccessor
明天补充图示说明哈,能够理解的同学请直接略过。