我正在以类似web-master-now 的方式解决这个问题。但是,我没有编写完整的自己的ControlValueAccessor,而是将所有内容委托给内部<input>ControlValueAccessor。结果是一个更短的代码,我不必自己处理与<input> 元素的交互。
这是我的代码
@Component({
selector: 'form-field',
template: `
<label>
{{label}}
<input ngDefaultControl type="text" >
</label>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true
}]
})
export class FormFieldComponent implements ControlValueAccessor, AfterViewInit {
@Input() label: String;
@Input() formControlName: String;
@ViewChild(DefaultValueAccessor) valueAccessor: DefaultValueAccessor;
delegatedMethodCalls = new ReplaySubject<(_: ControlValueAccessor) => void>();
ngAfterViewInit(): void {
this.delegatedMethodCalls.subscribe(fn => fn(this.valueAccessor));
}
registerOnChange(fn: (_: any) => void): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.registerOnChange(fn));
}
registerOnTouched(fn: () => void): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.registerOnTouched(fn));
}
setDisabledState(isDisabled: boolean): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.setDisabledState(isDisabled));
}
writeValue(obj: any): void {
this.delegatedMethodCalls.next(valueAccessor => valueAccessor.writeValue(obj));
}
}
它是如何工作的?
通常这不起作用,因为没有formControlName-directive 的简单<input> 不会是ControlValueAccessor,由于缺少[formGroup],组件中不允许这样做,正如其他人已经指出的那样.但是,如果我们查看 Angular 的 DefaultValueAccessor 实现代码
@Directive({
selector:
'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]',
//...
})
export class DefaultValueAccessor implements ControlValueAccessor {
...我们可以看到还有一个属性选择器ngDefaultControl。它可用于不同的目的,但似乎得到官方支持。
一个小缺点是带有值访问器的@ViewChild 查询结果在调用ngAfterViewInit 处理程序之前将不可用。 (根据您的模板,它会更早提供,但官方不支持。)
这就是为什么我使用ReplaySubject 缓冲我们想要委托给内部DefaultValueAccessor 的所有调用。 ReplaySubject 是 Observable,它缓冲所有事件并在订阅时发出它们。普通的Subject 会在订阅之前将它们丢弃。
我们发出 lambda 表达式,表示可以稍后执行的实际调用。在 ngAfterViewInit 上,我们订阅了 ReplaySubject 并简单地调用接收到的 lambda 函数。
我在这里分享另外两个想法,因为它们对我自己的项目非常重要,而且我花了一些时间来解决所有问题。我看到很多人有类似的问题和用例,所以我希望这对你有用:
改进思路1:为视图提供FormControl
我在项目中将ngDefaultControl 替换为formControl,因此我们可以将FormControl 实例传递给内部<input>。这本身并没有用,但是如果您使用与FormControls 交互的其他指令,例如Angular Material 的MatInput,则它是有用的。例如。如果我们将 form-field 模板替换为...
<mat-form-field>
<input [placeholder]="label" [formControl]="formControl>
<mat-error>Error!</mat-error>
</mat-form-field>
...Angular Material 能够自动显示表单控件中设置的错误。
我必须调整组件才能通过表单控件。我从 FormControlName 指令中检索表单控件:
export class FormFieldComponent implements ControlValueAccessor, AfterContentInit {
// ... see above
@ContentChild(FormControlName) private formControlNameRef: FormControlName;
formControl: FormControl;
ngAfterContentInit(): void {
this.formControl = <FormControl>this.formControlNameRef.control;
}
// ... see above
}
您还应该调整选择器以要求 formControlName 属性:selector: 'form-field[formControlName]'。
改进想法 2:委托给更通用的值访问器
我将DefaultValueAccessor @ViewChild 查询替换为对所有ControlValueAccessor 实现的查询。这允许除 <input> 之外的其他 HTML 表单控件,例如 <select>,如果您想让表单控件类型可配置,这很有用。
@Component({
selector: 'form-field',
template: `
<label [ngSwitch]="controlType">
{{label}}
<input *ngSwitchCase="'text'" ngDefaultControl type="text" #valueAccessor>
<select *ngSwitchCase="'dropdown'" ngModel #valueAccessor>
<ng-content></ng-content>
</select>
</label>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true
}]
})
export class FormFieldComponent implements ControlValueAccessor {
// ... see above
@Input() controlType: String = 'text';
@ViewChild('valueAccessor', {read: NG_VALUE_ACCESSOR}) valueAccessor: ControlValueAccessor;
// ... see above
}
使用示例:
<form [formGroup]="form">
<form-field formControlName="firstName" label="First Name"></form-field>
<form-field formControlName="lastName" label="Last Name" controlType="dropdown">
<option>foo</option>
<option>bar</option>
</form-field>
<p>Hello "{{form.get('firstName').value}} {{form.get('lastName').value}}"</p>
</form>
上面select 的一个问题是ngModel is already deprecated together with reactive forms。不幸的是,对于 Angular 的 <select> 控制值访问器,没有像 ngDefaultControl 这样的东西。因此我建议将此与我的第一个改进想法结合起来。