ARTICLE AD BOX
I'm trying to create a form with dynamic form controls.
Say I have my Signal Form setup like this:
const FormType = { A: 'A', B: 'B' } as const; type FormType = (typeof FormType)[keyof typeof FormType]; const FormTypes = { all: [FormType.A, FormType.B] } as const; interface FormData { type: FormType | null; time: Date | null; minutes: string | null; } @Component({ selector: 'app-my-form', templateUrl: '' }) export class MyFormComponent { readonly formData = signal({ type: null, time: null, minutes: null; }); readonly myForm = form(this.formData); }Each value in FormData is then mapped to a form control, but let's say that FormType.A has a different control than FormType.B. Essentially, this is what I want rendered:
<div> <div>Select Type</div> <select [formField]="form.type"> <option [value]="null">Select a type</option> @for (type of formTypes.all; track type) { <option [value]="type">{{ type }}</option> } </select> </div> <div> @switch (formData().type) { @case (formType.A) { <input [formField]="myForm.time" type="date" /> } @case (formType.B) { <input [formField]="myForm.minutes" type="text" /> } } </div>This is a simple example, but imagine a more complex one, where the form template is defined dynamically.
const FormField = { type: 'type', time: 'time', minutes: 'minutes' } as const; export type FormField = (typeof FormField)[keyof typeof FormField]; interface FormData { [FormField.type]: FormType | null; [FormField.time]: Date | null; [FormField.minutes]: string | null; } interface FormTemplate { id: string; property: FormField; type: 'date' | 'text'; } @Component({ selector: 'app-my-form', templateUrl: '' }) export class MyFormComponent { readonly formData = signal({ type: null, time: null, minutes: null; }); readonly myForm = form(this.formData); readonly fields = computed<FormTemplate[]>(() => { const formData = this.formData(); switch (formData.type) { case (FormType.A): return [{ id: 'a', property: FormField.time, type: 'date' }]; case (FormType.B): return [{ id: 'b', property: FormField.minutes, type: 'text' }]; } }); } <div> <div>Select Type</div> <select [formField]="form.type"> <option [value]="null">Select a type</option> @for (type of formTypes.all; track type) { <option [value]="type">{{ type }}</option> } </select> </div> <div> @for (field of fields(); track field.id) { <input [formField]="myForm[field.property]" [type]="field.type" /> } </div>The issue comes with the binding of formField. Since myForm.time returns a FieldTree in the simple example, but dynamically accessing the FieldTree with myForm[field.property] will cause a type error:
Type 'FieldTree<FormType | null, string> | FieldTree<Date | null, string> | MaybeFieldTree<string| null, string> | MaybeFieldTree<...> | MaybeFieldTree<...>' is not assignable to type 'FieldTree<FormType | null, string | number>'.I realize that Signal Forms are still experimental and maybe this will get worked out, but I'm curious if anyone has found a solution.
I'm currently working with Angular 21.1, though I tried Angular 21.2.0-rc.0 just to see if any changes and enhancements happened, but I'm still running into the same error.
