<shark-select [(ngModel)]="channelId" [data]="channelList">shark-select>
在表单中直接这么写会报错
ChannelListComponent.html:13 ERROR 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:
Example 2:
再看下ngModelOptions
的定义,它其实是ngmodel
指定的一个input属性,当standalone值为true时,代表着此 ngModel 不会把自己注册进它的父表单中,其行为就像没在表单中一样。
我们项目中使用ngModel其实是属于Template-driven Form,但shark中其实并没有用到ng的form表单校验。而Angular 会在 标签上自动创建并附加一个 NgForm 指令,NgForm 指令为 form 增补了一些额外特性。 它会控制那些带有 ngModel 指令和 name 属性的元素,监听他们的属性(包括其有效性)。 它还有自己的 valid 属性,这个属性只有在它包含的每个控件都有效时才是真。
forms/src/directives/ng_model.ts:
/**
* @description
* Tracks the name bound to the directive. The parent form
* uses this name as a key to retrieve this control's value.
*/
// TODO(issue/24571): remove '!'.
@Input() name !: string;
forms/src/directives/ng_form.ts:
/**
* @description
* Method that sets up the control directive in this group, re-calculates its value
* and validity, and adds the instance to the internal list of directives.
*
* @param dir The `NgModel` directive instance.
*/
addControl(dir: NgModel): void {
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
(dir as{control: FormControl}).control =
container.registerControl(dir.name, dir.control);
setUpControl(dir.control, dir);
dir.control.updateValueAndValidity({emitEvent: false});
this._directives.push(dir);
});
}
可以看到Angular 创建了一些 FormControl,并把它们注册到 Angular 附加到 标签上的 NgForm 指令。 注册每个 FormControl 时,使用
name
属性值作为键值。
所以每个form中的input元素的name值必须是唯一的,或者使用[ngModelOptions]="{standalone: true}"
将其排除在表单之外。
那么ng表单和表单验证是什样的?
ng中一个重要的概念就是表单,ng提供了响应式表单(Reactive Form)和模板驱动表单(Template-driven form)两种形式。这两种形式的表单都基于几个共同的底层概念:
class Validators {
static min(min: number): ValidatorFn
static max(max: number): ValidatorFn
static required(control: AbstractControl): ValidationErrors | null
static requiredTrue(control: AbstractControl): ValidationErrors | null
static email(control: AbstractControl): ValidationErrors | null
static minLength(minLength: number): ValidatorFn
static maxLength(maxLength: number): ValidatorFn
static pattern(pattern: string | RegExp): ValidatorFn
static nullValidator(control: AbstractControl): ValidationErrors | null
static compose(validators: ValidatorFn[]): ValidatorFn | null
static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn | null
}
添加至响应式表单
this.signupForm = this.fb.group({
userName: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.pattern('[a-z0-9._%+_]+@[a-z0-9.-]+')]]
});
添加至模板驱动式表单
shared/forbidden-name.directive.ts (forbiddenNameValidator):
/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? {'forbiddenName': {value: control.value}} : null;
};
}
@Directive({
selector: '[appForbiddenName]',
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator {
@Input('appForbiddenName') forbiddenName: string;
validate(control: AbstractControl): {[key: string]: any} | null {
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
: null;
}
}
添加至响应式表单
this.heroForm = new FormGroup({
'name': new FormControl(this.hero.name, [
Validators.required,
Validators.minLength(4),
forbiddenNameValidator(/bob/i) // <--传入自定义验证器
])
})
添加至模板驱动式表单
表单控件有以下6种状态
可以通过formControl来获取这些属性,这些属性从AbstractControl继承而来。
forms/src/directives/abstract_control_directive.ts
export abstract class AbstractControlDirective {
get valid(): boolean { return this.control ? this.control.valid : null; }
get invalid(): boolean { return this.control ? this.control.invalid : null; }
get errors(): ValidationErrors | null { 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 valueChanges(): Observable { return this.control ?
this.control.valueChanges : null; }
hasError(errorCode: string, path: string[] = null): boolean {
return this.control ? this.control.hasError(errorCode, path) : false;
}
如下,可以在html中获取状态从而显示对应的错误信息
<input type="text" formControlName="foo">
<div *ngIf="form.get('foo').hasError('required') && form.get('foo').touched">
Field is required
div>
<div *ngIf="form.get('foo').hasError('minlength') && form.get('foo').dirty">
Min length is 5
div>
附:写这错误显示逻辑未免有些繁琐,可以把其封装成组件,ToddMotto的ngx-error可供参考,更加声明式的表达。
<input type="text" formControlName="foo">
<div ngxErrors="foo">
<div ngxError="required" when="touched">
Field is required
div>
<div ngxError="minlength" when="dirty">
Min length is 5
div>
div>
写自定义表单控件的时候,都需要实现ControlValueAccessor接口,然而仅仅是知道如何实现,却并不知其中含义。
ControlValueAccessor是原生元素和Angular表单之间的桥梁,将组件或者指令继承ControlValueAccessor的接口就能当作Angular表单元素使用了。
ControlValueAccessor接口可以帮助我们完成数据和视图的双向转换,它的作用是:
Angular 之所以提供这个接口是因为不同的输入控件数据更新方式是不一样的。例如,对于我们常用的文本输入框来说,我们是设置它的 value 值,而对于复选框 (checkbox) 我们是设置它的 checked 属性。实际上,不同类型的输入控件都有一个 ControlValueAccessor,用来更新视图。
Angular中这些表单元素都实现了ControlValueAccessor接口:
ControlValueAccessor有这些方法:
forms/src/directives/control_value_accessor.ts
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
举个例子,angular源码中default_value_accessor对writeValue的实现:
writeValue
forms/src/directives/default_value_accessor.ts#L104
/**
* Sets the "value" property on the input element.
*
* @param value The checked value
*/
writeValue(value: any): void {
const normalizedValue = value == null ? '' : value;
this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue);
// <-----这里就是将模型中的新值写入视图或 DOM 属性中
}
//forms/src/directives/shared.ts#L41
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]);
// **将formControl的值初始化给view**
dir.valueAccessor !.writeValue(control.value);
// **监听view的变化,将值同步给formControl和ngModel**
setUpViewChangePipeline(control, dir);
//监听formControl,将值同步给view和ngModel
setUpModelChangePipeline(control, dir);
setUpBlurPipeline(control, dir);
//...
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnChange((newValue: any) => {
control._pendingValue = newValue;
control._pendingChange = true;
control._pendingDirty = true;
if (control.updateOn === 'change') updateControl(control, dir);
});
}
function updateControl(control: FormControl, dir: NgControl): void {
if (control._pendingDirty) control.markAsDirty();
control.setValue(control._pendingValue, {emitModelToViewChange: false});
dir.viewToModelUpdate(control._pendingValue);
control._pendingChange = false;
}
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
// control -> view
dir.valueAccessor !.writeValue(newValue);
// control -> ngModel
// **控件值访问器 ControlValueAccessory 还会调用 NgModel.viewToModelUpdate() 方法,它会发出一个 ngModelChange 事件。**
if (emitModelEvent) dir.viewToModelUpdate(newValue);
});
}
// .....
如上,在formControl的ngOnChanges中去调用setUpControl,setUpControl又去调用controlValueAccessor的方法。
下图是formControl如何借助于ControlValueAccessor与原生或自定义表单元素交互图
总而言之就是,自定义表单元素通过实现ControlValueAccessor中的writeValue()、registerOnChange()等方法,来实现formControl和DOM之前的双向交互。
需要两步:
实现ControlValueAccessor接口上面已经提到过,下面介绍如何注册 NG_VALUE_ACCESSOR提供商。
NG_VALUE_ACCESSOR 提供商用来指定实现了 ControlValueAccessor 接口的类,并且被 Angular 用来和 formControl进行同步。通常是使用该组件类或指令本身来注册。所有表单指令都是使用NG_VALUE_ACCESSOR 作为令牌来注入控件值访问器,然后通过selectValueAccessor方法选择合适的访问器。
export const DEFAULT_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DefaultValueAccessor),
multi: true
};
})
@Directive({
...
providers: [DEFAULT_VALUE_ACCESSOR]
})
export class DefaultValueAccessor implements ControlValueAccessor {...}
接下来解读下上述代码的含义。
我们使用内置的token NG_VALUE_ACCESSOR来注册 DefaultValueAccessor。
在 TypeScript 里面,类声明的顺序是很重要的。如果一个类尚未定义,就不能引用它。forwardRef() 函数建立一个间接地引用DefaultValueAccessor,使我们在Angular实例化它之前就可以使用它。
useExisting()用来保证只有一个DefaultValueAccessor的实例。
该provider对象有第三个属性 multi: true,把它和DI Tokens一起使用用来为特定事件注册多个处理器,这在当我们想给一个NG_VALUE_ACCESSOR token注册自己的ControlValueAccessors时是非常有用的,把一个token和多个provider关联起来。
参考链接: