angulartypescriptangular-formsangular-signals

How to provide default values for individual fields in Angular 21 Signal Forms (without UI flicker) when implementing a FormValueControl?


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:

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:

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:


Solution

  • 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);
    }