我有这个组件实现
ControlValueAccessor
并将内部状态保持在信号中 value
:
export class InputComponent implements ControlValueAccessor {
// internal state
value = signal('');
// implement writeValue()
writeValue(value: string): void { this.value.set(value); } // <-- writeValue is updating the signal
// other methods
}
InputComponent
用于这样的模板中:
<!-- ClientComponent template -->
<app-input [formControl]="fcUrl"/>
其中
fcUrl
是 FormControl
:
export class ClientComponent {
url = input.required<string>();
fcUrl = new FormControl<string>('');
constructor() {
effect(() => {
// update the FormControl when new value for 'url' arrives:
this.fcUrl.setValue(this.url());
}, { allowSignalWrites: true });
}
}
这里,
{ allowSignalWrites: true }
是必要的,因为writeValue()
的InputComponent
正在设置信号的值。否则会抛出运行时错误:
NG0600: Writing to signals is not allowed in a `computed` or an `effect` by default. Use `allowSignalWrites` in the `CreateEffectOptions` to enable this inside effects.
在这种情况下,使用
FormControl
而不使用 effect()
标志来更新 allowSignalWrites
的正确方法是什么(而不是用 effect()
替换 rxjs
,例如 toObservable(this.url).pipe(...)
)?
如果您按照以下步骤实现
ControlValueAccessor
,则无需传递 formControl 作为输入,然后只需使用 [formControl]
或 formControlName
即可进行绑定。
然后您可以简单地使用
patchValue
根据更新的 url
信号来更新 formControl。
...
export class Child {
signUpForm = new FormGroup({
name: new FormControl(''),
});
url = input.required<string>();
constructor() {
effect(
() => {
// update the FormControl when new value for 'url' arrives:
this.signUpForm.patchValue({ name: this.url() });
});
}
}
@Component({
selector: 'app-input',
standalone: true,
imports: [FormsModule, ReactiveFormsModule, NgClass],
template: `
<input
[type]="type()"
#val
(focus)="markAsTouched()"
(input)="valueChanged(val.value)"
[disabled]="disabled"
[value]="_value"
/>
<label [for]="label()">
{{ label() }}
</label>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: InputComponent,
},
],
})
export class InputComponent implements ControlValueAccessor {
public readonly label = input.required<InputProps['label']>();
public readonly checkValidation = input<InputProps['checkValidation']>(false);
public readonly placeholder = input<InputProps['placeholder']>();
public type = input<InputProps['type']>('text');
onChange = (value: any) => {};
_value!: any;
onTouched = () => {};
touched = false;
disabled = false;
writeValue(value: number) {
this._value = value;
}
valueChanged(val: string) {
this.onChange(val);
}
registerOnChange(onChange: any) {
this.onChange = onChange;
}
registerOnTouched(onTouched: any) {
this.onTouched = onTouched;
}
markAsTouched() {
if (!this.touched) {
this.onTouched();
this.touched = true;
}
}
setDisabledState(disabled: boolean) {
this.disabled = disabled;
}
}
import { Component, effect, input } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import {
ReactiveFormsModule,
FormGroup,
FormsModule,
FormControl,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
NgControl,
} from '@angular/forms';
import { CommonModule, NgClass } from '@angular/common';
type InputProps = {
formGroup: FormGroup;
type: 'text' | 'password' | 'number';
value: string;
label: string;
formControlName: string;
placeholder: string;
checkValidation: boolean;
};
@Component({
selector: 'app-input',
standalone: true,
imports: [FormsModule, ReactiveFormsModule, NgClass],
template: `
<input
[type]="type()"
#val
(focus)="markAsTouched()"
(input)="valueChanged(val.value)"
[disabled]="disabled"
[value]="_value"
/>
<label [for]="label()">
{{ label() }}
</label>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: InputComponent,
},
],
})
export class InputComponent implements ControlValueAccessor {
public readonly label = input.required<InputProps['label']>();
public readonly checkValidation = input<InputProps['checkValidation']>(false);
public readonly placeholder = input<InputProps['placeholder']>();
public type = input<InputProps['type']>('text');
onChange = (value: any) => {};
_value!: any;
onTouched = () => {};
touched = false;
disabled = false;
writeValue(value: number) {
this._value = value;
}
valueChanged(val: string) {
this.onChange(val);
}
registerOnChange(onChange: any) {
this.onChange = onChange;
}
registerOnTouched(onTouched: any) {
this.onTouched = onTouched;
}
markAsTouched() {
if (!this.touched) {
this.onTouched();
this.touched = true;
}
}
setDisabledState(disabled: boolean) {
this.disabled = disabled;
}
}
@Component({
selector: 'app-child',
standalone: true,
imports: [ReactiveFormsModule, InputComponent, CommonModule],
template: `
<form [formGroup]="signUpForm">
<app-input
label="Name"
formControlName="name"
></app-input>
</form>
{{signUpForm.value | json}}
`,
})
export class Child {
signUpForm = new FormGroup({
name: new FormControl(''),
});
url = input.required<string>();
constructor() {
effect(
() => {
// update the FormControl when new value for 'url' arrives:
this.signUpForm.patchValue({ name: this.url() });
},
{ allowSignalWrites: true }
);
}
}
@Component({
selector: 'app-root',
standalone: true,
imports: [Child],
template: `
<app-child [url]="url"/>
`,
})
export class App {
url = Math.random().toString();
ngOnInit() {
setInterval(() => {
this.url = Math.random().toString();
}, 2000);
}
}
bootstrapApplication(App);