Contract-First Dynamic UI
VUEDA's client ships without knowledge of your models: which exist, what fields they have, or which actions they offer. It builds routes, forms, field and widget components, and action views at runtime from the server's metadata, called Model Info. It reads that metadata through stores/storeModelInfo.
This page explains how the client turns that metadata into a default UI, and how you customize the parts that need it. It describes configuration precedence (with its exceptions) and field and widget resolution in full. Server-Client Metadata Contract describes what the metadata contains, and Canonical Registration and Model Discovery describes how a model gets metadata.
The Default UI and Customization
A model registered with a serializer and a viewset gets a complete CRUD surface with no client code. When a field changes on the server, the form changes with it. When the server stops offering an action, its route goes away.
The default UI is a starting point. A view that needs tweaking, or does not fit the default conventions, takes overrides: for a field, for a widget, or for the whole view. Everything an override leaves alone still follows the metadata. Configuration Precedence describes how overrides combine with the defaults.
Model info is the only source the client uses for a model's shape: its fields, expands, filters, ordering, and model-level actions. A user's actions can also come from the workflow and from each object's own availability. Action Contract and Availability describes where each action comes from. When the client does something unexpected, start with the metadata it received.
Registration decides what the client can build. A registered model gets metadata, routes, forms, and permission gating. A model or endpoint outside registration gets none of these, and the client cannot add an action that the server does not offer.
Where the Metadata Comes From
The server derives model info from the model, serializer, viewset, and filterset definitions. These are the same definitions that the server enforces. A field that is read-only in the serializer arrives read-only, and an action that the viewset does not implement does not appear.
Client Derivation Layer
The client renames model info's keys when it stores them; Server-Client Metadata Contract lists the names. It then derives each part of the UI from that metadata, with no per-model client code.
Route Availability
Every model shares the same CRUD Routes. Route Admission opens a route only for one of the user's permitted transitions or an action that the server lists for the user. Routing and View Resolution Model describes the guard chain and what happens when a guard rejects a route.
Action View Resolution
For each admitted action, ViewActionRouter picks one view through Action View Resolution. setCrudComponents changes the built-in view registry. The order and the transition cases are in View Component Resolution Order.
Field and Widget Resolution
Each form field renders as a field component that wraps a widget component. The field component handles label, layout, help text, and error display. The widget is the input itself, such as a text box, select, or date picker.
The default field component is FormField for every field type. The one type-driven exception is an expand in the form's field list, which renders as an inline field set: FieldSetStackedInline for a many relation, FieldSetSingularStackedInline otherwise.
The default widget comes from type mapping tables keyed on the field's typeSerializer, then its typeModel. When typeModel is empty, the entry marked default for that serializer type applies. Choice fields and many-valued fields have their own tables, checked before the base table. typeDb only selects props for a GeneratedField. mergeDefaultFieldMappings adds or replaces entries in the base table at runtime.
A read-only field takes its type's read-only widget. When the type has none, it takes WidgetReadOnly. An editable field whose type has no mapping has no widget. That field renders an error in place of its input, and the rest of the form still renders. Custom Field and Widget Rendering describes that fallback and the steps to add a mapping.
Form Generation
The form builder turns each field's metadata into a field component, a widget component, props for each, and choice-loading behaviour. Fields with static choices get their options inline in the metadata. Fields marked choices: true load their options from the server: a relation's widget searches the related model's list endpoint, and a slug relation's widget fetches from the choices endpoint when it holds a value or first gets focus. Choice-Backed Fields and Lookup Models lists where each kind of field loads its options.
The client flattens expanded relations into dotted field names (expandName.fieldName). An override for a nested field uses the same keys as one for a base field. Composables and symbol-based provide and inject share form state: values, errors, touched state, and modification tracking. Nested field sets join the same form without passing props down.
Configuration Precedence
Through two layers above the server's defaults, the client can reshape what it derives. Model Config is a Pinia store, stores/storeModelConfig, filled with setConfig. It has a generic layer for all views of a model and a view layer keyed by view name (list, create, update, read, and others). For the same key, the view layer wins over the generic layer. Component props are the props that a view or form receives from the template that renders it. Reactive Data Flow describes how the store builds and caches each view's config.
The Rule
For fields and widgets, precedence is: component props > model config > server-derived defaults. How a layer wins depends on the kind of key:
- Component maps (OverridingModelConfig.fieldComponents, OverridingModelConfig.widgetComponents): the first layer with an entry for the field wins. An entry can be a component, the name of a registered component, or a function that returns a component. An unknown name raises an error for that field.
- Prop maps (OverridingModelConfig.fieldProps, OverridingModelConfig.widgetProps): the layers merge key by key. Defaults come first (the type mapping's props, plus the server's field entry for field props), then model config, then component props.
- Field lists (
fields, OverridingModelConfig.expand,computedFields): a component prop replaces the model config list whole. - Field metadata (OverridingModelConfig.fieldDetails): entries merge over the server's field entries, per property, in the same order.
The resolver sets a few props after every layer, so no layer can override them. On the widget these are readOnly and the choice source (options, or the endpoint coordinates for choices: true). On the field they are name and readOnly.
Exceptions
Three cases do not follow the rule:
- Read-only fields take the read-only widget. When a field renders read-only, the resolver ignores
widgetComponentsfrom both layers and renders the type's read-only widget orWidgetReadOnly. A field renders read-only when the server marks it read-only, when the view isread, when it is a computed field, or when either layer setsreadOnly: true. To customize it, replace the field component withfieldComponents, fill the field'swidget(<name>)slot, or make the field editable. - Computed fields always use
FormField. A computed field ignoresfieldComponentsfrom both layers. readOnly: falsewins from either layer. An explicitreadOnly: falsein thefieldPropsof either layer makes the field editable in the UI. It beats a server read-only marker, areadview, and areadOnly: truein the other layer, so a model configfalsebeats a component proptrue.
formProps in model config are extra props for the form model. Every form view applies three layers in order. Model config formProps comes first, then the view's own formProps prop. The view's form props, such as fieldProps, widgetProps, and formModelVariant, come last. A form prop that the template does not pass leaves the configured value in place. A later layer replaces a value whole, so a fieldProps map passed to the view replaces the configured map instead of merging with it.
The view always sets app, model, and view, because they select the configuration that the form reads. The view ignores those keys in a formProps layer and logs a console warning. The read view, ViewRead, takes no form props, so its form receives model config formProps alone. A custom view shell gets the same order from use/useFormModelProps.
List columns resolve through their own chain, described in Customize List Column Rendering.
UI Overrides and Server Enforcement
Overrides change only what the client renders. Field props can replace required, label, or readOnly for the UI, and fieldDetails can replace any property of a field's metadata. The server applies its own validation and read-only rules on every write, whatever the form sent.
Overrides cannot add actions. The route guard admits only workflow transition codes and actions that the server lists, so an action name that only model config knows gets no route and no view. routeActions can only narrow the server's list. Hiding an action in the client does not remove its endpoint; Authorization vs UI Semantics describes that boundary.
Resolution Determinism
The same metadata, configuration, and registered mappings always produce the same UI. The type mapping tables and the view registry are module-level state, so mergeDefaultFieldMappings and setCrudComponents change the result for every model at once. Precedence has fixed layers, the resolver applies the three exceptions the same way in every form, and view resolution ends at exactly one component.
This is what makes a UI problem traceable. Debugging it means inspecting the metadata that the client received and the configuration it applied.
Failure Modes
Client code names a field that the server no longer sends. A configured field list that names a field missing from the metadata throws Unknown field ... specified when the form builds. Project overrides and custom views reintroduce this risk whenever they reference contract details by name.
Premature overrides hide defaults. The default mapping is often correct. An override added before you understand the default adds maintenance and becomes one more variable when something breaks.
Client-side restriction mistaken for server-side removal. Removing an action from routeActions hides it in the client, but the endpoint still accepts requests from any user the server permits.
Dynamic choices assumed to be loaded. A field with choices: true has no options until its widget fetches them. Code that reads its options early sees an empty list, and keeps seeing one if the fetch fails.
Widget overrides on read-only fields. A widgetComponents entry for a field that renders read-only has no effect, as described under Exceptions.
Nested object paths for expanded fields. Overrides for expanded fields use the flattened dotted key. A nested object path does not match any field, and the override is ignored.
A failed model-info fetch blocks the model. The client caches the failure, so the model stays unavailable until the cache clears. Reactive Data Flow describes when cached errors clear.