Skip to content

Signal Forms Documentation: Enhance Guidance for Large, Nested, and Variant-Based Signal Form Architectures #67407

@ptandler

Description

@ptandler

Describe the problem that you experienced

When building large, deeply nested Signal Forms with domain ↔ form model separation, there are several architectural questions where it would be helpful to have more detailed recommendations in the documentation.

Enter the URL of the topic with the problem

https://angular.dev/guide/forms/signals/model-design

Describe what you were looking for in the documentation

I try to summarize some points we have experienced so far to be helpful. In case some aspects are in the docs in the meantime already, please apologize. 😊

Encapsulation & Mapping Domain and Form Models

When you have a large and nested domain model that requires adjusted form models at all levels, our first try was to do an independent encapsulated mapping for each part. However, this causes issues and overhead so for now we switched to an approach to map the domain models once to form model at initialization and this seem to work well. The drawback is that we need to describe the domain and form model composition several times: for the edit components, for the mapping and for the validation. Not sure if there is a cleaner way.

  • use encapsulated mapping functions for each part: for a large, deeply nested domain model we introduced mapping functions not only for the top level model, but also for each sub model. This ensures some kind of encapsulation for each form model.
    • e.g. when we have interface User { name: string; address: Address } we also have a mapToAddressForm() & mapFromAddressForm() that is called by the user mappings.
  • the docs mention the naming MyDomainModel / MyFormModel and domainModelToFormModel() / formModelToDomainModel()
  • I used the naming like User & UserForm (without ...Model) and for the functions mapToUserForm() and mapFromUserForm() as this does not repeat the User part. Maybe not that important, but I think it's helpful to establish a clear naming standard here as well

Avoid circular synchronization of domain and form model

Currently we have good experiences with a clear one-way chain for the data flow:

originalDomainModel -> mappedFormModel -> form -> mappedBackFormValue

  • use a linkedSignal for mapped form value as we can get updates from the backend and need to re-init the edit value
  • include the previous edit state in the mapping of the form model if there is edit state not overwritten by the domain model update
  • the docs give an example to use an effect to sync back the edited form model to the domain model. In our case with a large, nested structure this caused issues we could avoid by a clear one-way data flow.
  • if you want live preview or something, it's much more robust to map the edit form value back to a edit domain model and don't replace the original domain model; just avoid signal update cycles
  • BTW: when I have class X { value = signal<Y>(...); valueForm = form(this.value) } Does accessing this.value() and this.form().value() yield the same result in all cases? Or are there situations where I should prefer one way?

Form Models

  • the form models should have default values applied to all optional parts, so it's a complete model. The form model definition must reflect this. In the docs the handling of undefined models is shown as part of the computation. I think it's cleaner to do the handling of undefined parts within the mapTo functions, as we there can handle not only the case when the whole model is undefined, but also when parts are undefined. So the example would be simply formModel = linkedSignal({ source: this.domainModel; computation: mapToFormModel })
  • be very strict with null vs undefined: null mean that there is no value but undefined should be mapped to the default (wich might be null) to ensure that the field tree is complete and no fields are missing: undefined values will be excluded from the field tree
  • use null in form value that e.g. initially or during edit have no value, use validation to ensure that a required field is set
  • when you have variant union types in you domain model: my experience is that it works well to keep the edit state for all variants next to each other in the form model, e.g. type V = A | B | C will have interface TForm { type: 'a'|'b'|'c'; a: AForm; b : BForm; c: CForm } in case the linked signal is updates, it's good for UX to keep the values that are not overwritten
  • if the domain model (a variant union) uses a common base interface / base class, in some places it worked well to extract the common base in the form model during mapping (and re-assemble when mapping back to domain model) to a separate base prop like this type V = A | B | C // all extend Base and base defined type: 'a'|'b'|'c' and interface TForm { type: 'a'|'b'|'c'; base: BaseForm; a: AForm; b : BForm; c: CForm } This way, it's simple to have an edit component for the base parts that can be re-used for editing a/b/c
  • This also nicely avoids the current limitation that variant unions are not yet supported in the template

Validation

Similar to the mapping, we currently do the validation in one large part for the whole form.

  • Encapsulated validation: it's handy to define schema() for all part of the domain model and assemble them similar to the mapping functions and have a single top-level validation attached to the form. for type TForm we call the schema TFormSchema.
  • Is there a official recommendation how to assemble validation parts? options are: having a validation function for a sub part that is called by the parent, or use a schema and apply / applyWhen / applyWhenValue.
  • I think the question of how to assemble large form models is important. I first tried to encapsulate the mapping to the form model for each part of our model. While is have really nice encapsulation, it has drawbacks that we need a lot of additional signals and forms and things get complicated. so my question is what is the best way to encapsulate sub-edit components, mappings, validation?

Edit Components for Child Models

  • For the sub form model edit components, we started that the get a public readonly field = input.required<FieldTree<T>>() input that passes the FieldTree for this part. This way, it avoids mapping form fields to some component internal signals and assemble them again (as it would happen when using the formField directive). I saw this approach somewhere in an article ... Is this a recommended approach? Our experience is pretty positive until now.

Describe the actions that led you to experience the problem

No response

Describe what you want to experience that would fix the problem

No response

Add a screenshot if that helps illustrate the problem

No response

If this problem caused an exception or error, please paste it here


If the problem is browser-specific, please specify the device, OS, browser, and version


Provide any additional information here in as much as detail as you can


Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions