Question
Using signals with dynamic angular forms?
I have this shared component which builds a form based on the SearchFilterOption
structure.
Here it is (simplified):
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>
and I wanted to try using signals for the input and output of this component, but it all seems to fall apart with errors of missing form fields when I do:
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 }
);
}
I've also tried a variation where the subscription uses toSignal, and one that uses computed
to build the form
and can't really figure out what else to do.
Is there a working method of doing this with signals right now?