Angular 12-控制ValueAccessor和PatchValue
我正在为使用 controlValueAccessor 接口而创建的影响我的自定义下拉组件的问题苦苦挣扎。
基本上,此下拉列表组件可以具有两个不同的值:
- 一个简单的字符串(属于可选字符串的数组)
- 一个复杂的对象 key :
export interface Key {
group?: string;
key?: string;
order?: number;
description?: string;
defaultQ?: string;
}
总的来说,此自定义组件正常工作如下:
- 手动输入和选择。工作正常,
- 如果该值是键对象,则仅应向用户显示 description 属性,
- 后面的CVA值正确设置为字符串(第一个方案)或键(第二场景)。
当我尝试通过从父组件中修补值的值来初始化此下拉组件时,就会发生问题:
this.formGroup.patchValue({ country: this.defaultCountry });
其中this.defaultCountry
是a key 对象,用“意大利”为描述。 事实证明,该描述未显示(而不是显示 [对象对象] ),并且也未更新下拉组件背后的CVA值( control.value control.value 解析的描述是空的)。
似乎没有由 patchValue 命令触发的更新。
这是我当前的下拉component 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>
我在这里缺少什么? 你能帮助我吗?
谢谢。
问候, 是
I am struggling with an issue affecting my custom dropdown component, created using the ControlValueAccessor interface.
Basically, this dropdown component can have two different possible values:
- a simple string (belonging to an array of choosable strings)
- a complex object Key:
export interface Key {
group?: string;
key?: string;
order?: number;
description?: string;
defaultQ?: string;
}
Overall, this custom component work correctly as follows:
- manual input and selection work fine
- if the value is a Key object, only the description attribute shall be displayed to the user
- the CVA value behind is correctly set to a string (1st scenario) or a Key (2nd scenario).
The problem occurs when I try to initialize this dropdown component by patching the value from a parent component as follows:
this.formGroup.patchValue({ country: this.defaultCountry });
where this.defaultCountry
is a Key object with "Italy" as its description.
Turns out, the description is not displayed (instead [object Object] is shown), and the CVA value behind the dropdown component is not updated either (both the control.value and the parsed description are empty).
It seems like no update is triggered by the patchValue command.
This is my current 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;
}
}
And this is the HTML code of DropdownComponent:
<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>
What am I missing here?
Can you help me?
Thank you.
Regards,
A.M.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

我设法通过在我的更改eTectorRef 中注入我的下拉component 类,并使用它如下:如下:
这样,该值已正确更新并在页面中显示。
I managed to solve my issue by injecting the ChangeDetectorRef in my DropdownComponent class and using it as follows:
Like this, the value is correctly updated and shown in page.