У меня есть общий компонент, который создает форму на основе структуры SearchFilterOption
.
Вот (упрощенно):
import {
Component,
EventEmitter,
inject,
Input,
OnChanges,
Output,
SimpleChanges,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged, Subscription } from 'rxjs';
export type SearchFilterOption = {
kind: 'text' | 'search' | 'checkbox';
label: string;
key: string;
};
@Component({
selector: 'shared-search-filter',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './search-filter.component.html',
})
export class SearchFilterComponent implements OnChanges {
@Input() filter: { [key: string]: any } = {};
@Input() filterOptions: readonly SearchFilterOption[] = [];
@Output() filterChange = new EventEmitter<{ [key: string]: any }>();
form = new FormGroup({});
formChange?: Subscription;
ngOnChanges(changes: SimpleChanges): void {
if (changes['filterOptions']) {
const group: { [key: string]: FormControl } = {};
for (const filter of this.filterOptions) {
group[filter.key] = new FormControl();
}
this.form = new FormGroup(group);
this.formChange?.unsubscribe();
this.formChange = this.form.valueChanges
.pipe(
debounceTime(500),
distinctUntilChanged(
(a, b) => JSON.stringify(a) === JSON.stringify(b)
)
)
.subscribe({
next: (values) => {
this.filterChange.emit(values);
},
});
}
if (changes['filter']) {
for (const key of Object.keys(this.filter)) {
const ctrl = this.form.get(key);
if (ctrl) {
ctrl.setValue(this.filter[key]);
}
}
}
}
}
<fieldset [formGroup] = "form">
@for (option of filterOptions; track option; let i = $index) {
<label>{{ option.label }}</label>
<input [type] = "option.kind" [formControlName] = "option.key" />
}
</fieldset>
и я хотел попробовать использовать сигналы для ввода и вывода этого компонента, но, похоже, все это разваливается из-за ошибок отсутствия полей формы, когда я это делаю:
filter = model<{ [key: string]: any }>({});
filterOptions = input.required<readonly SearchFilterOption[]>();
form = new FormGroup({});
formChange?: Subscription;
constructor() {
effect(
() => {
const group: { [key: string]: FormControl } = {};
for (const filter of this.filterOptions()) {
group[filter.key] = new FormControl(
this.filter()[filter.key]
);
}
this.form = new FormGroup(group);
this.formChange?.unsubscribe();
this.formChange = this.form.valueChanges
.pipe(
debounceTime(500),
distinctUntilChanged(
(a, b) => JSON.stringify(a) === JSON.stringify(b)
)
)
.subscribe({
next: (values) => {
this.filter.set(values);
},
});
},
{ allowSignalWrites: true }
);
}
<fieldset [formGroup] = "form">
@for (option of filterOptions(); track option; let i = $index) {
<label>{{ option.label }}</label>
<input [type] = "option.kind" [formControlName] = "option.key" />
}
</fieldset>
Я также попробовал вариант, в котором подписка использует toSignal, и тот, в котором computed
используется для создания form
, и я не могу понять, что еще делать.
Есть ли работающий метод сделать это с сигналами прямо сейчас?
Примечание: на данный момент это лучший подход, который я могу придумать, но когда придут сигналы реактивной формы, этот код будет избыточным.
Ваш код работает нормально, все, что вам нужно сделать, это переместить цикл for, создающий элементы управления формы, в ngOnInit
.
import {
Component,
effect,
EventEmitter,
inject,
input,
Input,
model,
OnChanges,
Output,
SimpleChanges,
untracked,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged, Subscription, tap } from 'rxjs';
import { toSignal, outputFromObservable } from '@angular/core/rxjs-interop';
export type SearchFilterOption = {
kind: 'text' | 'search' | 'checkbox';
label: string;
key: string;
};
@Component({
selector: 'shared-search-filter',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `<fieldset [formGroup] = "form">
@for (option of filterOptions; track option; let i = $index) {
<label>{{ option.label }}</label>
<input [type] = "option.kind" [formControlName] = "option.key" />
}
</fieldset>`,
})
export class SearchFilterComponent implements OnChanges {
@Input() filter: { [key: string]: any } = {};
@Input() filterOptions: readonly SearchFilterOption[] = [];
@Output() filterChange = new EventEmitter<{ [key: string]: any }>();
form = new FormGroup({});
formChange?: Subscription;
ngOnChanges(changes: SimpleChanges): void {
if (changes['filterOptions']) {
const group: { [key: string]: FormControl } = {};
for (const filter of this.filterOptions) {
group[filter.key] = new FormControl();
}
this.form = new FormGroup(group);
this.formChange?.unsubscribe();
this.formChange = this.form.valueChanges
.pipe(
debounceTime(500),
distinctUntilChanged(
(a, b) => JSON.stringify(a) === JSON.stringify(b)
)
)
.subscribe({
next: (values) => {
this.filterChange.emit(values);
},
});
}
if (changes['filter']) {
for (const key of Object.keys(this.filter)) {
const ctrl = this.form.get(key);
if (ctrl) {
ctrl.setValue(this.filter[key]);
}
}
}
}
}
@Component({
selector: 'shared-search-filter2',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `<fieldset [formGroup] = "form">
@for (option of filterOptions(); track option; let i = $index) {
<label>{{ option.label }}</label>
<input [type] = "option.kind" [formControlName] = "option.key" />
}
</fieldset>`,
})
export class SearchFilterComponent2 {
filter = model<{ [key: string]: any }>({});
filterOptions = input.required<readonly SearchFilterOption[]>();
form = new FormGroup({});
formChange?: Subscription;
constructor() {
effect(() => {
if (this.formChange) {
this.formChange.unsubscribe();
}
this.formChange = this.form.valueChanges
.pipe(
debounceTime(500),
distinctUntilChanged(
(a, b) => JSON.stringify(a) === JSON.stringify(b)
),
tap(() => {
untracked(() => {
this.filter.set(this.form.value);
});
})
)
.subscribe();
});
}
ngOnInit() {
const group: { [key: string]: FormControl } = {};
for (const filter of this.filterOptions()) {
group[filter.key] = new FormControl(this.filter()[filter.key]);
}
this.form = new FormGroup(group);
}
}
К сожалению, это, похоже, ответ: прямо сейчас вы не можете реактивно изменять элементы управления формой с помощью сигналов, поэтому вы можете только инициализировать значения (начиная с Angular 18.0).
Не зная характера ошибок, я могу лишь предполагать решение и рекомендовать некоторые изменения, чтобы упростить компонент.
readonly filter = input<{ [key: string]: any }>({});
readonly filterOptions = input.required<readonly SearchFilterOption[]>();
readonly form = new FormGroup({});
readonly filterChange = outputFromObservable(this.form.valueChanges
.pipe(
debounceTime(500),
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
));
constructor() {
effect(() => {
this.form.reset();
Object.keys(this.form.controls).forEach(key => this.form.removeControl(key));
const filter = untracked(this.filter); // changes here won't cause an effect.
for (const opt of this.filterOptions()) {
this.form.addControl(opt.key, new FormControl(filter[opt.key]));
}
});
effect(() => {
const filter = this.filter();
for (const [key, value] of Object.entries(this.filter)) {
this.form.get(key)?.setValue(value);
}
});
}
<fieldset [formGroup] = "form">
<!-- Don't forget to call fitlerOptions since it is a function now. -->
@for (option of filterOptions(); track option; let i = $index) {
@if (form.get(option.key); as formCtrl) {
<label>{{ option.label }}</label>
<input [type] = "option.kind" [formControl] = "formCtrl" />
}
}
</fieldset>
Это несколько хороших концепций, но именно этот первый эффект по-прежнему вызывает ошибки в привязках форм: ERROR Error: There is no FormControl instance attached to form control element with name: 'test'
. (Обратите внимание, что ваш ответ не подходит .addControl
, но я изменил его на это)
@Manstie - Помимо недостающего addControl
, я обнаружил проблему: параметры входного фильтра менялись до запуска эффекта. Я обновил ответ, чтобы использовать FormGroup, но FormArray, вероятно, будет лучше, поскольку вы можете легко перебирать элементы в шаблоне. Возможно, вам нужна FormGroup для чего-то еще, поэтому я не буду ее менять. В следующий раз добавьте в пост больше информации, чтобы мы точно знали, что и где ошибка. :-)
Спасибо, вы, вероятно, можете удалить № 2 из своего ответа, поскольку для краткости я пропустил весь блок кода представления вместо того, чтобы вставить его снова с добавлением ()
, но я понимаю, как это может вызвать затруднения при ответе.
allowSignalWrites
будет удален, вместо него используйтеuntracked