Angular 12 - ControlValueAccessor 和 patchValue
Angular 12 - ControlValueAccessor and patchValue
我正在努力解决影响我的自定义下拉组件的问题,该组件是使用 ControlValueAccessor 界面创建的。
基本上,这个下拉组件可以有两个不同的可能值:
- 一个简单的字符串(属于可选字符串数组)
- 一个复杂的对象Key:
export interface Key {
group?: string;
key?: string;
order?: number;
description?: string;
defaultQ?: string;
}
总体而言,此自定义组件按如下方式正常工作:
- 手动输入和选择工作正常
- 如果值是一个 Key 对象,只有 description 属性应该显示给用户
- 后面的 CVA 值正确设置为字符串(第一种情况)或 Key(第二种情况)。
当我尝试通过修补父组件的值来初始化此下拉组件时出现问题,如下所示:
this.formGroup.patchValue({ country: this.defaultCountry });
其中 this.defaultCountry
是一个 Key 对象,其描述为“意大利”。
事实证明,描述没有显示(而是显示 [object Object]),下拉组件后面的 CVA 值也没有更新(control.value 解析后的描述为空)。
似乎 patchValue 命令没有触发更新。
这是我当前的 DropdownComponent class:
@Component({
selector: 'app-dropdown',
templateUrl: './dropdown.component.html',
styleUrls: ['./dropdown.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: DropdownComponent,
multi: true
}
]
})
export class DropdownComponent implements ControlValueAccessor, OnDestroy {
private _destroyed: Subject<boolean> = new Subject();
@Input()
label = '';
@Input()
values: string[] | Key[] = [];
@Input()
readOnly = false;
@Input()
uppercase = false;
@Input()
filterResults = false;
@Input()
showErrors = true;
@Input()
position: DdPosition = 'bottom';
@Input()
keyGroup!: KeyGroup;
@Input()
formControl!: FormControl;
@Input()
formControlName!: string;
@Output()
selected: EventEmitter<unknown> = new EventEmitter<unknown>();
@Output()
valid: EventEmitter<boolean> = new EventEmitter<boolean>();
onTouched = (): void => {};
onChange = () => {};
@ViewChild(FormControlDirective, { static: true })
formControlDirective!: FormControlDirective;
@ViewChild('valueSearch', { static: false })
valueSearch: ElementRef<HTMLElement> | undefined;
constructor(private controlContainer: ControlContainer, private keyService: KeyService) {
this.keyService.keys$
.pipe(
takeUntil(this._destroyed),
tap(keys => (this.values = keys.filter(k => k.group == this.keyGroup)))
)
.subscribe();
}
ngOnDestroy(): void {
this._destroyed.next();
this._destroyed.complete();
}
get control(): any {
return this.formControl || this.controlContainer.control?.get(this.formControlName) || new FormControl();
}
get value(): any {
if (!this.control.value) {
return null;
}
if (this.keyGroup) {
// Dropdown of keys
return this.control.value[0] as Key;
}
return this.control.value;
}
getDescription(value: any): string { // this is the real value displayed by the HTML code
if (!value) {
return '';
}
if (typeof value === 'string') {
return value;
}
// Dropdown of keys
return (value as Key)?.description || '';
}
get stringsToFilter(): string[] {
if (this.keyGroup) {
// Dropdown of keys
return (this.values as Key[]).map(k => k.description || '');
}
return this.values as string[];
}
clearInput(): void {
if (this.control.disabled) {
return;
}
this.control.setValue('');
this.onChange();
this.selected.emit(this.value);
this.valueSearch?.nativeElement.blur();
}
onSelectChange(selected: string): void {
if (this.control.disabled) {
return;
}
if (this.keyGroup) {
this.control.setValue((this.values as Key[]).filter(v => v.description === selected));
} else {
this.control.setValue(selected);
}
this.onInputChange();
}
onInputChange(): void {
if (this.control.disabled) {
return;
}
this.onChange();
this.selected.emit(this.value);
}
onBlur(): void {
this.onTouched();
}
registerOnTouched(fn: any): void {
this.formControlDirective.valueAccessor?.registerOnTouched(fn);
}
registerOnChange(fn: any): void {
this.formControlDirective.valueAccessor?.registerOnChange(fn);
}
writeValue(obj: any): void {
this.formControlDirective.valueAccessor?.writeValue(this.getDescription(obj));
}
setDisabledState(isDisabled: boolean): void {
this.formControlDirective.valueAccessor?.setDisabledState?.(isDisabled);
}
get isValueInList(): boolean {
if (!this.getDescription(this.value) || this.getDescription(this.value) == '') {
return true;
}
return this.values
.map(v => (this.keyGroup ? (v as Key).description : (v as string)))
.includes(this.getDescription(this.value));
}
get invalid(): boolean {
return (this.control ? this.control.invalid : false) || !this.isValueInList;
}
get hasErrors(): boolean {
if (!this.control) {
return false;
}
const { dirty, touched } = this.control;
return this.invalid ? dirty || touched : false;
}
}
这是 DropdownComponent 的 HTML 代码:
<div class="text-xs dropdown">
[...]
<!-- Selected value -->
<input
name="select"
id="select"
class="px-4 appearance-none outline-none text-gray-800 w-full"
autocomplete="off"
[ngClass]="{
'uppercase': uppercase,
'cursor-pointer': readOnly
}"
[value]="getDescription(value)"
[formControl]="control"
[readOnly]="readOnly"
(blur)="onBlur()"
(change)="onInputChange()"
#valueSearch
/>
[...]
</div>
我在这里错过了什么?
你能帮帮我吗?
谢谢。
此致,
A.M.
我设法通过在我的 DropdownComponent class 中注入 ChangeDetectorRef 并按如下方式使用它来解决我的问题:
writeValue(obj: any): void {
this.formControlDirective.valueAccessor?.writeValue(obj);
this.cdr.detectChanges();
}
像这样,值被正确更新并显示在页面中。
我正在努力解决影响我的自定义下拉组件的问题,该组件是使用 ControlValueAccessor 界面创建的。
基本上,这个下拉组件可以有两个不同的可能值:
- 一个简单的字符串(属于可选字符串数组)
- 一个复杂的对象Key:
export interface Key {
group?: string;
key?: string;
order?: number;
description?: string;
defaultQ?: string;
}
总体而言,此自定义组件按如下方式正常工作:
- 手动输入和选择工作正常
- 如果值是一个 Key 对象,只有 description 属性应该显示给用户
- 后面的 CVA 值正确设置为字符串(第一种情况)或 Key(第二种情况)。
当我尝试通过修补父组件的值来初始化此下拉组件时出现问题,如下所示:
this.formGroup.patchValue({ country: this.defaultCountry });
其中 this.defaultCountry
是一个 Key 对象,其描述为“意大利”。
事实证明,描述没有显示(而是显示 [object Object]),下拉组件后面的 CVA 值也没有更新(control.value 解析后的描述为空)。
似乎 patchValue 命令没有触发更新。
这是我当前的 DropdownComponent class:
@Component({
selector: 'app-dropdown',
templateUrl: './dropdown.component.html',
styleUrls: ['./dropdown.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: DropdownComponent,
multi: true
}
]
})
export class DropdownComponent implements ControlValueAccessor, OnDestroy {
private _destroyed: Subject<boolean> = new Subject();
@Input()
label = '';
@Input()
values: string[] | Key[] = [];
@Input()
readOnly = false;
@Input()
uppercase = false;
@Input()
filterResults = false;
@Input()
showErrors = true;
@Input()
position: DdPosition = 'bottom';
@Input()
keyGroup!: KeyGroup;
@Input()
formControl!: FormControl;
@Input()
formControlName!: string;
@Output()
selected: EventEmitter<unknown> = new EventEmitter<unknown>();
@Output()
valid: EventEmitter<boolean> = new EventEmitter<boolean>();
onTouched = (): void => {};
onChange = () => {};
@ViewChild(FormControlDirective, { static: true })
formControlDirective!: FormControlDirective;
@ViewChild('valueSearch', { static: false })
valueSearch: ElementRef<HTMLElement> | undefined;
constructor(private controlContainer: ControlContainer, private keyService: KeyService) {
this.keyService.keys$
.pipe(
takeUntil(this._destroyed),
tap(keys => (this.values = keys.filter(k => k.group == this.keyGroup)))
)
.subscribe();
}
ngOnDestroy(): void {
this._destroyed.next();
this._destroyed.complete();
}
get control(): any {
return this.formControl || this.controlContainer.control?.get(this.formControlName) || new FormControl();
}
get value(): any {
if (!this.control.value) {
return null;
}
if (this.keyGroup) {
// Dropdown of keys
return this.control.value[0] as Key;
}
return this.control.value;
}
getDescription(value: any): string { // this is the real value displayed by the HTML code
if (!value) {
return '';
}
if (typeof value === 'string') {
return value;
}
// Dropdown of keys
return (value as Key)?.description || '';
}
get stringsToFilter(): string[] {
if (this.keyGroup) {
// Dropdown of keys
return (this.values as Key[]).map(k => k.description || '');
}
return this.values as string[];
}
clearInput(): void {
if (this.control.disabled) {
return;
}
this.control.setValue('');
this.onChange();
this.selected.emit(this.value);
this.valueSearch?.nativeElement.blur();
}
onSelectChange(selected: string): void {
if (this.control.disabled) {
return;
}
if (this.keyGroup) {
this.control.setValue((this.values as Key[]).filter(v => v.description === selected));
} else {
this.control.setValue(selected);
}
this.onInputChange();
}
onInputChange(): void {
if (this.control.disabled) {
return;
}
this.onChange();
this.selected.emit(this.value);
}
onBlur(): void {
this.onTouched();
}
registerOnTouched(fn: any): void {
this.formControlDirective.valueAccessor?.registerOnTouched(fn);
}
registerOnChange(fn: any): void {
this.formControlDirective.valueAccessor?.registerOnChange(fn);
}
writeValue(obj: any): void {
this.formControlDirective.valueAccessor?.writeValue(this.getDescription(obj));
}
setDisabledState(isDisabled: boolean): void {
this.formControlDirective.valueAccessor?.setDisabledState?.(isDisabled);
}
get isValueInList(): boolean {
if (!this.getDescription(this.value) || this.getDescription(this.value) == '') {
return true;
}
return this.values
.map(v => (this.keyGroup ? (v as Key).description : (v as string)))
.includes(this.getDescription(this.value));
}
get invalid(): boolean {
return (this.control ? this.control.invalid : false) || !this.isValueInList;
}
get hasErrors(): boolean {
if (!this.control) {
return false;
}
const { dirty, touched } = this.control;
return this.invalid ? dirty || touched : false;
}
}
这是 DropdownComponent 的 HTML 代码:
<div class="text-xs dropdown">
[...]
<!-- Selected value -->
<input
name="select"
id="select"
class="px-4 appearance-none outline-none text-gray-800 w-full"
autocomplete="off"
[ngClass]="{
'uppercase': uppercase,
'cursor-pointer': readOnly
}"
[value]="getDescription(value)"
[formControl]="control"
[readOnly]="readOnly"
(blur)="onBlur()"
(change)="onInputChange()"
#valueSearch
/>
[...]
</div>
我在这里错过了什么? 你能帮帮我吗?
谢谢。
此致, A.M.
我设法通过在我的 DropdownComponent class 中注入 ChangeDetectorRef 并按如下方式使用它来解决我的问题:
writeValue(obj: any): void {
this.formControlDirective.valueAccessor?.writeValue(obj);
this.cdr.detectChanges();
}
像这样,值被正确更新并显示在页面中。