---
url: "https://vueda.dev/v3/guides/custom-field-widget-rendering.md"
title: "Customize Field and Widget Rendering"
---

# Customize Field and Widget Rendering

This guide shows how to replace the component that VUEDA renders for a form or filter field, change the props that it receives, or write your own. Fields that you leave alone keep their defaults.

[Configuration Precedence](/v3/core-concepts/contract-first-dynamic-ui.md#configuration-precedence) describes how view props, model config, and the defaults combine, and the cases that do not follow that order.

## Before You Start

- The model has a [Canonical Registration](/v3/reference/glossary.md#canonical-registration) with working CRUD views. Check that its default forms render and submit before you add overrides.
- Each form field renders a [Form Field](/v3/reference/glossary.md#form-field) that wraps a [Widget](/v3/reference/glossary.md#widget). Replacing the field component replaces the label, help text, and messages around the input. Replacing the widget replaces only the input.

## Choose Where to Override

| To change                                          | Use                                              |
| -------------------------------------------------- | ------------------------------------------------ |
| One field in every view of a model, or in one view | [Model config](/v3/guides/custom-field-widget-rendering.md#set-an-override-in-model-config) |
| One field in a view or form you render yourself    | [View or form props](/v3/guides/custom-field-widget-rendering.md#pass-overrides-as-props)   |
| The markup around one field in one rendered form   | [A slot](/v3/guides/custom-field-widget-rendering.md#replace-one-field-with-a-slot)         |
| The layout of all fields in one rendered form      | [A `fields` slot](/v3/guides/custom-field-widget-rendering.md#lay-out-the-fields-yourself)  |
| Every field of one type, in every model            | [A type mapping](/v3/guides/custom-field-widget-rendering.md#map-a-field-type-to-a-widget)  |

## Set an Override in Model Config

1. Get the store with [storeModelConfig](/v3/reference/api/js/stores/storeModelConfig/functions/storeModelConfig.md).
2. Call [`setConfig`](/v3/reference/api/js/stores/storeModelConfig/methods/setConfig.md) with the model, a generic layer for every view, and optional per-view layers keyed by view name.
3. Key each override by the field name. Use [OverridingModelConfig.fieldComponents](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#fieldComponents) and [OverridingModelConfig.widgetComponents](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#widgetComponents) to replace components, and [OverridingModelConfig.fieldProps](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#fieldProps) and [OverridingModelConfig.widgetProps](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#widgetProps) to add props.

```js
import StatusWidget from "./StatusWidget.vue";
import { storeModelConfig } from "@vueda/stores/storeModelConfig.js";

const modelConfigStore = storeModelConfig();

modelConfigStore.setConfig(
    { app: "myapp", model: "order" },
    {
        widgetProps: { phone: { mask: "###-###-####" } },
    },
    {
        create: { widgetComponents: { status: () => StatusWidget } },
        update: { widgetComponents: { status: "WidgetSelectDropdown" } },
    },
);
```

A component entry takes one of three values:

- **A function that returns a component**, such as `() => StatusWidget`. The store keeps its values in reactive state, and the function keeps the component out of it. VUEDA calls the function with no arguments, so it can read reactive state but receives no field or view context.
- **The name of a built-in component** in [utils/formLookups.availableFields](/v3/reference/api/js/utils/formLookups.md#availableFields) or [utils/formLookups.availableWidgets](/v3/reference/api/js/utils/formLookups.md#availableWidgets), such as `"WidgetSelectDropdown"` for [WidgetSelectDropdown](/v3/reference/api/vue/WidgetSelectDropdown.md).
- **A component object.**

A name that is not in the registry makes that field render an error in place of its input: `No widget component named "X" for field "f" in app "a" model "m"`. The rest of the form still renders.

Filter forms on the list view read `fieldComponents` and `widgetComponents` from the `list` view's model config, keyed by filter name. A generic-layer entry therefore also replaces the filter input of the same name. Put form-only component overrides in the `create` and `update` layers.

## Pass Overrides as Props

When you render a form yourself, pass the same four maps as props. [FormModel](/v3/reference/api/vue/FormModel.md), [ViewCreate](/v3/reference/api/vue/ViewCreate.md), [ViewUpdate](/v3/reference/api/vue/ViewUpdate.md), and [DetailView](/v3/reference/api/vue/DetailView.md) accept [`fieldComponents`](/v3/reference/api/vue/FormModel.md#prop-fieldComponents), [`widgetComponents`](/v3/reference/api/vue/FormModel.md#prop-widgetComponents), [`fieldProps`](/v3/reference/api/vue/FormModel.md#prop-fieldProps), and [`widgetProps`](/v3/reference/api/vue/FormModel.md#prop-widgetProps).

```vue
<template>
    <ViewCreate app="myapp" model="order" :widget-components="{ status: 'WidgetSelectDropdown' }" />
</template>
```

For the same field, a prop wins over model config. A prop entry wins even when it is an unknown name: the field renders the error, although model config has a valid entry for it.

[ViewRead](/v3/reference/api/vue/ViewRead.md) takes none of these props. Set the `read` layer in model config for the read view.

Model config [OverridingModelConfig.formProps](/v3/reference/api/js/stores/storeModelConfig/types/OverridingModelConfig.md#formProps) can also carry these keys. When it and a view prop set the same key, the view prop wins and replaces the configured map whole. [Configuration Precedence](/v3/core-concepts/contract-first-dynamic-ui.md#configuration-precedence) describes the full order.

## Target Expanded Fields

An [expanded](/v3/reference/glossary.md#expand) relation renders as an [Inline](/v3/reference/glossary.md#inline), and its subfields render inside it. Key overrides for a subfield by its dotted name, `<expand>.<field>`:

```js
modelConfigStore.setConfig(
    { app: "myapp", model: "order" },
    {
        fieldComponents: { line_items: "FieldSetTabularInline" },
        widgetComponents: { "line_items.status": "WidgetSelectDropdown" },
        widgetProps: { "line_items.amount": { step: 0.01 } },
    },
    {
        update: {
            expand: ["line_items"],
            displayFields: ["line_items", "line_items.status", "line_items.amount"],
        },
    },
);
```

The expanded field itself has a field component ([FieldSetTabularInline](/v3/reference/api/vue/FieldSetTabularInline.md) here) and no widget, so a `widgetComponents` entry for `line_items` has no effect. [Render a Relation as an Inline](/v3/guides/render-relations-as-inlines.md) describes the inline setup. [Build Nested/Inlined Writes](/v3/guides/nested-writable-inlines.md) describes how the rows save.

## Replace One Field with a Slot

Use a slot when one rendered form needs different markup for one field. Pass the slot to `FormModel`, or to a view, which forwards its slots to the form. Each field renders through [FieldRenderer](/v3/reference/api/vue/FieldRenderer.md), which offers four slots per field:

- [`field(<name>)`](/v3/reference/api/vue/FieldRenderer/slots.md#slot-field-fieldName) replaces the field component and everything in it.
- [`field(<name>)default`](/v3/reference/api/vue/FieldRenderer/slots.md#slot-field-fieldName-default) replaces the content inside the field component, which holds the widget.
- [`widget(<name>)`](/v3/reference/api/vue/FieldRenderer/slots.md#slot-widget-fieldName) replaces the widget.
- [`widget(<name>)default`](/v3/reference/api/vue/FieldRenderer/slots.md#slot-widget-fieldName-default) fills the widget's default slot.

Filter forms use the same names with a `filter-` prefix, such as `filter-widget(status)`. [ViewList](/v3/reference/api/vue/ViewList.md) forwards them to its filter forms.

To change only the label, help text, or messages of a [FormField](/v3/reference/api/vue/FormField.md), fill its [`field(<name>)label`](/v3/reference/api/vue/FormField/slots.md#slot-field-fieldName-label), [`field(<name>)help`](/v3/reference/api/vue/FormField/slots.md#slot-field-fieldName-help), [`field(<name>)errors`](/v3/reference/api/vue/FormField/slots.md#slot-field-fieldName-errors), or [`field(<name>)warnings`](/v3/reference/api/vue/FormField/slots.md#slot-field-fieldName-warnings) slot.

This slot wraps the default widget in extra markup:

```vue
<template #widget(line_items.status)="slotProps">
    <div class="status-cell">
        <component :is="slotProps.widgetComponent" v-bind="slotProps.widgetProps">
            <template v-for="[slotName, slotRenderer] of slotProps.slots" #[slotName]="innerProps" :key="slotName">
                <component :is="slotRenderer" v-bind="innerProps" />
            </template>
        </component>
    </div>
</template>
```

`slotProps.slots` holds the other slots that you passed to the form, as `[name, renderer]` pairs. Pass them through, as above, when you render the default component yourself. Without the passthrough, your label, help, message, and named widget slots do not reach the component.

A slot name must match exactly, including the dotted name and the `filter-` prefix. `FormModel` checks the first segment of each `field(<path>)` and `widget(<path>)` slot against its fields. A slot whose path starts with no field of the form throws in development and logs a console warning in production. The form ignores any other misspelling, such as a misspelled `label` suffix or a wrong subfield inside a fieldset. The field then renders its default.

## Lay Out the Fields Yourself

`FormModel`'s [`fields`](/v3/reference/api/vue/FormModel/slots.md#slot-fields) slot replaces the loop that renders each field. Use it to group fields into sections or grids. Render each field with [FormModelField](/v3/reference/api/vue/FormModelField.md), which receives the per-field slots that you pass to `FormModel` or to a view:

```vue
<FormModel app="shop" model="purchaseorder">
    <template #fields="{ fieldNames, formModel }">
        <FormSection v-for="name in fieldNames" :key="name">
            <FormModelField :form-model="formModel" :form-model-name="name" />
        </FormSection>
    </template>
    <template #widget(sku)="slotProps">
        <SkuPicker v-bind="slotProps.widgetProps" />
    </template>
</FormModel>
```

`FormModelField` accepts every `FieldRenderer` prop. A slot that you pass to `FormModelField` directly wins over the form's slot with the same name.

A plain `FieldRenderer` does not receive the form's per-field slots. When a `fields` layout renders no `FormModelField` and the form has per-field slots, `FormModel` throws in development and logs a console warning in production. To use a plain `FieldRenderer`, declare the per-field slots on it instead of on the form.

The `fields` slot also binds `allFieldProps`, the merged field props for every field, and `allWidgetProps`, the same for widgets. `FormModelField` already applies both. Do not pass an `allFieldProps` entry to its `fieldProps` prop, because that prop wins over the merged entry and applies it a second time.

## Write a Custom Widget

A widget spreads [use/useWidget.WIDGET\_PROPS](/v3/reference/api/js/use/useWidget.md#WIDGET_PROPS), declares [use/useWidget.WIDGET\_EMITS](/v3/reference/api/js/use/useWidget.md#WIDGET_EMITS), and calls [useWidget](/v3/reference/api/js/use/useWidget/functions/useWidget.md). `useWidget` returns a [WidgetContext](/v3/reference/api/js/use/useWidget/types/WidgetContext.md) that connects the widget to the field around it: the value, touched and focus state, and validation flags.

```vue
<script setup>
import { WIDGET_EMITS, WIDGET_PROPS, useWidget } from "@vueda/use/useWidget.js";
import { FieldContextSymbol } from "@vueda/utils/symbols.js";
import { inject } from "vue";

defineOptions({ inheritAttrs: false });
const props = defineProps({
    ...WIDGET_PROPS,
    placeholder: { type: String, default: "" },
});
const emit = defineEmits([...WIDGET_EMITS]);
const fieldContext = inject(FieldContextSymbol, null);
const widget = useWidget(props, emit);
</script>

<template>
    <input
        :id="fieldContext?.state.fieldId"
        v-model="widget.state.adaptedValue"
        :name="widget.state.combinedName"
        :placeholder="placeholder"
        :disabled="widget.state.disabled"
        :aria-invalid="widget.state.validationState.invalid || undefined"
        :aria-required="widget.state.required || undefined"
        v-bind="$attrs"
        @blur="widget.blur"
        @focus="widget.focus"
    />
</template>
```

- Bind the value through `widget.state.adaptedValue` or `widget.state.combinedValue`. Writing to either updates the form value.
- Call `widget.blur` and `widget.focus` from the control. Blur marks the field touched.
- Use [utils/symbols.FieldContextSymbol](/v3/reference/api/js/utils/symbols.md#FieldContextSymbol) for the field's `fieldId`, so the field's label points at your control.

Assign the widget through model config, props, or a type mapping. Your own props, such as `placeholder` here, come from `widgetProps`.

## Write a Custom Field

A field component spreads [use/useField.FIELD\_PROPS](/v3/reference/api/js/use/useField.md#FIELD_PROPS), declares [use/useField.FIELD\_EMITS](/v3/reference/api/js/use/useField.md#FIELD_EMITS), and calls [useField](/v3/reference/api/js/use/useField/functions/useField.md). `useField` registers the field with the form and returns the [FieldContext](/v3/reference/api/js/use/useField/types/FieldContext.md) that the widget reads. `FieldRenderer` puts the widget in the field's default slot.

```vue
<script setup>
import { FIELD_EMITS, FIELD_PROPS, useField } from "@vueda/use/useField.js";

defineOptions({ inheritAttrs: false });
const props = defineProps({
    ...FIELD_PROPS,
    hidden: { type: Boolean, default: false },
});
const emit = defineEmits([...FIELD_EMITS]);
const field = useField(props, emit, { showsErrors: () => !props.hidden });
</script>

<template>
    <slot v-if="hidden" />
    <div v-else class="my-field">
        <label :for="field.state.fieldId">{{ field.state.label }}</label>
        <slot />
        <p v-for="(message, code) in field.state.errors" :key="code">{{ message }}</p>
    </div>
</template>
```

`FieldRenderer` passes `hidden` as `true` for fields inside an inline, which render their widget alone. Declare `hidden`, and any other `FormField` prop that your field needs, such as `validation` or `orientation`. An undeclared prop falls through as an HTML attribute. The `showsErrors` option tells the form whether this field shows its own errors; a hidden field leaves them to the form-level summary.

## Map a Field Type to a Widget

The default widget for a field comes from type mapping tables keyed by the field's [`typeSerializer`](/v3/reference/api/js/stores/storeModelInfo/types/FieldInfo.md#typeSerializer), then its [`typeModel`](/v3/reference/api/js/stores/storeModelInfo/types/FieldInfo.md#typeModel). [Field and Widget Resolution](/v3/core-concepts/contract-first-dynamic-ui.md#field-and-widget-resolution) describes the lookup.

An editable field whose type has no mapping has no widget. That field renders an error in place of its input, `No widget component found for field "f" in app "a" model "m"`, and the rest of the form still renders. Some types map to [WidgetUnmapped](/v3/reference/api/vue/WidgetUnmapped.md), which renders a notice in place of an input. By default these are IP address fields.

To give a type a widget in every model, call [mergeDefaultFieldMappings](/v3/reference/api/js/utils/fieldMappings/functions/mergeDefaultFieldMappings.md) in your client entry, before the app mounts:

```js
import { mergeDefaultFieldMappings } from "@vueda/utils/fieldMappings.js";
import WidgetTextInput from "@vueda/widgets/WidgetTextInput.vue";

mergeDefaultFieldMappings({
    IPAddressField: {
        IPAddressField: { widget: WidgetTextInput, default: true },
        GenericIPAddressField: { widget: WidgetTextInput },
    },
});
```

The outer key is the `typeSerializer`, and the inner key is the `typeModel`. The entry marked [`default`](/v3/reference/api/js/utils/fieldMappings/types/FieldMappingEntry.md#default) applies when `typeModel` is empty. An entry can also set [`readOnlyWidget`](/v3/reference/api/js/utils/fieldMappings/types/FieldMappingEntry.md#readOnlyWidget) for read-only rendering, and [`widgetProps`](/v3/reference/api/js/utils/fieldMappings/types/FieldMappingEntry.md#widgetProps) or [`fieldProps`](/v3/reference/api/js/utils/fieldMappings/types/FieldMappingEntry.md#fieldProps).

## Check the Result

- The field renders as intended in the create, update, and read views.
- A submit sends the field's value.
- Server errors for the field appear beside it.
- Leaving the field marks it touched.
- In an inline, every row's copy of the field renders and saves.
- The list view's filter of the same name renders the input that you intend.

## Troubleshooting

**The override has no effect.** Check that the key matches the field name exactly, with the dotted name for an expanded subfield. A field that renders read-only ignores `widgetComponents`, and a [Computed Field](/v3/reference/glossary.md#computed-field) ignores `fieldComponents`; [Exceptions](/v3/core-concepts/contract-first-dynamic-ui.md#exceptions) describes both and what works instead. A view prop for the same field wins over model config.

**The field shows `No widget component named "X"` or `No field component named "X"`.** The name is not in `availableWidgets` or `availableFields`. Check the spelling, or pass the component through a function.

**The field shows `No widget component found`.** The field's type has no mapping. Add one with `mergeDefaultFieldMappings`, or set a `widgetComponents` entry for the field.

**`WidgetUnmapped` renders for a field.** The field's type maps to it. Map the type to a widget, as in [Map a Field Type to a Widget](/v3/guides/custom-field-widget-rendering.md#map-a-field-type-to-a-widget).

**A slot override loses your label, help, or message slots.** The slot renders the default component without passing `slotProps.slots` through. Add the passthrough shown in [Replace One Field with a Slot](/v3/guides/custom-field-widget-rendering.md#replace-one-field-with-a-slot).

**A form with a `fields` slot throws `renders no FormModelField`.** The layout renders a plain `FieldRenderer`, so the form's per-field slots cannot reach the fields. Render each field with `FormModelField`, as in [Lay Out the Fields Yourself](/v3/guides/custom-field-widget-rendering.md#lay-out-the-fields-yourself).

**A form throws `is not a field of the form`.** A `field(<path>)` or `widget(<path>)` slot names a field that the form does not render. Check the slot name against the form's `fields`.

**The override works in the update view but not the read view.** The read view renders every field read-only, and a read-only field uses the read-only widget. Replace the field component, fill the `widget(<name>)` slot, or give the type a `readOnlyWidget`.
