We're starting to look at Angular 21's new signal-based forms, but I can't find a clean way to define default values for each form field, especially when building forms for objects where multiple keys may be undefined.
Ideally, I'd like to set a default value either:
On the whole form, so that missing properties in the model are patched with defaults (like with a schema or options parameter); or
Directly on fields, so that if a field's value is undefined, the form provides a fallback until the user provides input.
Here's what I'd like to do, but I think this isn't supported in Angular 21:
@Component({
selector: 'signal-form-default-value-example-1',
template: `<input type="number" [field]="valueForm" />`,
imports: [Field]
})
export class SignalFormDefaultValueExample1 implements FormValueControl<number | undefined> {
public readonly value = model<number>();
protected readonly valueForm = form(this.value, { defaultValue: 42 }); // << would be ideal!
}
I know that I can set the default up-front in the model (works for flat values):
public readonly value = model<number>(42);
protected readonly valueForm = form(this.value);
But for complex objects where fields may be missing, this doesn't scale:
interface Value { a?: number; b?: number }
@Component({
selector: 'signal-form-default-value-example-3',
template: `<input type="number" [field]="valueForm.a" />
<input type="number" [field]="valueForm.b" />`,
imports: [Field]
})
export class SignalFormDefaultValueExample3 implements FormValueControl<Value | undefined> {
public readonly value = model<Value>();
protected readonly valueForm = form(this.value, { defaultValue: { a: 17, b: 42 } }); // not supported
}
or per-field default:
<input type="number" [field]="valueForm.a" [fieldDefault]="17" />
I've tried:
Using effects to patch missing values, but the UI briefly shows no/default state, causing flicker. Or I get type errors when I want to pass a field to an edit component that requires a value
Separating the value input and valueChange output and a linkedSignal in between to patch the default there - but the FormValueControl expects a value model, so I can't split input and output.
Is there any "official" or ergonomic way to provide default values for individual fields in Angular 21 signal forms, so that fields always start with a meaningful value even when the data is missing or partially undefined? If not, what are recommended patterns to work around this cleanly (without UI flicker)?
My main requirements:
I come up with this solution to encapsulate the mapping between domain and form model, also allowing to provide default values for the form, see https://github.com/angular/angular/issues/65194#issuecomment-3585154425:
/**
* Create a signal form that uses a different model for data and form:
* The signal that is passed gets set from the input model and written back via effect
* This can also be used to provide default values for the form model
* @example
* ```typescript
* interface Model { someNumber?: number; option: null|string }
* interface Form { someNumber: number; option: string }
* export class SomeComponent implements FormValueControl<Model> {
* public readonly value = model<Model>();
* protected readonly valueForm = mappedForm<Model, Form>(this.value, {
* modelToForm: (modelValue) => ({
* someNumber: modelValue.someNumber ?? 42,
* option: modelValue.option ?? 'none',
* }),
* formToModel: (formValue) => ({
* someNumber: formValue.someNumber,
* option: formValue.option === 'none' ? null : formValue.option,
* }),
* })
* }
* ```
*/
export function mappedForm<Model, Form>(
signal: WritableSignal<Model>,
{
modelToForm,
formToModel,
schema,
...formOptions
}: {
modelToForm: (value: Model) => Form;
formToModel: (formValue: Form) => Model;
schema?: SchemaOrSchemaFn<Form>;
injector?: Injector;
name?: string;
},
): FieldTree<Form> {
const editSignal = linkedSignal<Form>(() => modelToForm(signal()), {
equal: isEqual,
});
// Sync form changes back from the valueEdit signal to the main model value signal
effect(() => {
const valueFromForm = formToModel(editSignal());
if (!isEqual(signal(), valueFromForm)) {
signal.set(valueFromForm);
}
});
return schema
? form<Form>(editSignal, schema, formOptions)
: form<Form>(editSignal, formOptions);
}