Customize List Column Rendering
This guide shows how to change the component that renders a column's cells in ViewList, change the props it receives, or write your own. Columns you leave alone keep their defaults.
Each cell renders through a Column Adapter. Contract-First Dynamic UI describes how model config and view props combine in general. This guide gives the order for list columns.
To link a row to its own read or update view, use a Row Link. Link List Rows to Read and Update Views describes it, including which adapters it wraps.
Before You Start
- The model has a Canonical Registration with a working
listview (Create a CRUD Surface). Check that the list renders before you add overrides. - You know the field name of each column you want to change. Overrides are keyed by field name.
How Default Columns Are Chosen
VUEDA picks each column's default adapter in utils/columnMappings.columnMappings. It looks up the field's typeSerializer, then its typeModel:
| Serializer type | Model type | Adapter | Default props |
|---|---|---|---|
BooleanField | BooleanField | ColumnBoolean | |
NullBooleanField | NullBooleanField | ColumnBoolean | |
DateField | DateField | ColumnDateTime | { showTime: false } |
DateTimeField | DateTimeField | ColumnDateTime | { showTime: true } |
TimeField | TimeField | ColumnDateTime | { format: "t", showRelative: false, showTooltip: false } |
DurationField | DurationField | ColumnDuration | |
DurationSecondsField | DurationField | ColumnDuration | |
FileField | FileField | ColumnFile | |
ImageField | ImageField | ColumnFile | |
JSONField | JSONField | ColumnJson | |
PrimaryKeyRelatedField | ForeignKey / OneToOneField | ColumnModelLink | { view: "read" } |
Every other type renders through ColumnText. That includes to-many relations and SlugRelatedField.
The adapters render as follows:
ColumnTextrenders the cell value as text. It renders nothing fornullorundefined. It renders an object or array as compactJSON, cut at 200 characters.ColumnBooleanrenders BooleanDisplay: "Yes" or "No".ColumnDateTimerenders DateTimeDisplay.ColumnDurationrenders DurationDisplay, which names the units.ColumnFilerenders FileDisplay: a link to the file, labelled with the file name.ColumnJsonrenders JsonDisplay: compactJSONon one line, cut atmaxLength(200 characters by default).ColumnModelLinkrenders a foreign key as a link to the related row's read view, through LinkModelView.
BooleanDisplay, DateTimeDisplay, DurationDisplay, FileDisplay, and JsonDisplay render a dash for an empty value.
Choose Where to Override
| To change | Use |
|---|---|
| One column of a model, in every list | Model config |
One column in a ViewList you render yourself | ViewList props |
| The markup of one column in one rendered list | A slot |
| Every column of one type, in every model | A type mapping |
For one column, the first of these that is set picks the adapter:
- A
field(<name>)slot onViewList. - The
ViewListcolumnComponentsprop. - The model config's
columnComponents. - The type mapping, else
ColumnText.
Props combine separately, as Change Only an Adapter's Props describes.
Set an Override in Model Config
- Get the store with storeModelConfig.
- Call
setConfigwhen the application starts. Register Overrides withsetConfigdescribes its arguments. - In the
listlayer, setcolumnComponentsto choose adapters. SetcolumnPropsto add props. Key both by field name.
import ColumnStatusBadge from "./ColumnStatusBadge.vue";
import { storeModelConfig } from "@vueda/stores/storeModelConfig.js";
storeModelConfig().setConfig({ app: "myapp", model: "widget" }, null, {
list: {
columnComponents: {
status: () => ColumnStatusBadge,
release_date: "ColumnDateTime",
},
columnProps: {
release_date: { showTime: false, format: "yyyy-LL-dd" },
},
},
});A columnComponents entry takes one of three values:
- A function that returns a component, such as
() => ColumnStatusBadge. VUEDA calls it with no arguments. Use this form for your own components. The store keeps its values in reactive state and merges plain objects between layers; the function keeps the component out of both. - The name of a built-in adapter in utils/columnLookups.availableColumns:
"ColumnText","ColumnBoolean","ColumnDateTime","ColumnDuration","ColumnFile","ColumnJson", or"ColumnModelLink". Use a name to give a column a built-in adapter that its type does not map to. - A component object. Use this form in the
columnComponentsprop. In model config, wrap the component in a function.
The model-wide layer also applies to the list view. When both layers set columnComponents, the list gets the entries of both. For a column that both layers name, the list layer wins. columnProps merges each column's props across the layers, key by key.
Pass Overrides as Props
When you render ViewList yourself, pass columnComponents and columnProps. They take the same values as the model config keys.
<template>
<ViewList app="myapp" model="widget" :column-components="{ category: 'ColumnText' }" />
</template>For the same column, a columnComponents prop entry wins over model config. It wins even when it names no adapter. The column then shows the error described in Troubleshooting, even if model config has a valid entry for it.
Change Only an Adapter's Props
To keep a column's adapter and change how it renders, set only columnProps. The adapter receives the type mapping's default props first. The model config's columnProps for the column come next, then the columnProps prop. A later source wins for the same key.
This hides the time on one datetime column:
storeModelConfig().setConfig({ app: "myapp", model: "widget" }, null, {
list: { columnProps: { updated_at: { showTime: false } } },
});Each built-in adapter's generated page lists its props. The ones you are most likely to set:
ColumnDateTime:format(default"absolute", or a Luxon format string),showTime,showRelative,showTooltip,tooltipFormat.ColumnBoolean:trueLabel,falseLabel.ColumnDuration:format(default"long").ColumnJson:maxLength.ColumnModelLink:view(default"read"),appandmodel,label,button.
Link a Foreign Key Column
A ForeignKey or OneToOneField column renders through ColumnModelLink with no configuration. It reads the related model from the field's appLabel and model. Model info sends both for writable and read-only relation fields.
- Related pk: a scalar cell value is the pk. An object value uses its
id, else itspk. - Link text: the
labelprop, else the cell'sformattedvalue when it is text or a number. For a scalar foreign key, the text is the pk. For an expanded foreign key, the text is the related object'sformatted_name, else itsname,id, orpk. - No link: when no related model or no pk resolves, or the value is an array, the cell renders the text without a link.
When the field carries no related model, supply one through columnProps:
storeModelConfig().setConfig({ app: "myapp", model: "widget" }, null, {
list: { columnProps: { category: { app: "myapp", model: "widgetcategory" } } },
});The link controls navigation in the UI only. When the user follows it, the target view's route guard and the server still check the user's access.
Replace a Column with a Slot
A field(<name>) slot on ViewList replaces the column's content and wins over every adapter. It receives the same props as an adapter (Write a Column Adapter).
<template>
<ViewList app="myapp" model="widget">
<template #[`field(sku)`]="{ value, obj }">
<code>{{ value }}</code>
<span v-if="obj.is_active" class="ml-2 text-green-600">active</span>
</template>
</ViewList>
</template>ObjectsGrid renders the slot in both the table cell and the card cell. If you wrap ViewList in your own component, forward the slot to ViewList.
Map a Field Type to an Adapter
To change the default for a type in every model, call mergeColumnMappings in your client entry, before the app mounts. It deep-merges your entries into columnMappings. The outer key is the typeSerializer, and the inner key is the typeModel.
This shows times as hours and minutes in every list:
import { mergeColumnMappings } from "@vueda/utils/columnMappings.js";
mergeColumnMappings({
TimeField: {
TimeField: { columnProps: { format: "HH:mm" } },
},
});An entry's column names an adapter in availableColumns, and columnProps holds its default props. The entry marked default applies when typeModel is empty. A column that names no adapter shows the column error described in Troubleshooting on every column of that type, unless an override sets the column's adapter.
Write a Column Adapter
An adapter is a Vue component. ViewList passes it the cell's slot props, with the column's resolved columnProps on top. The table cell and the card cell pass the same props:
value: the field's value in the row.formatted: in aViewListcell, the same value asvalue.obj: the row object.relatedObjandcalculatedObjhold the row's related and calculated objects.pk: the row's primary key. A foreign key's target pk is invalue.field: the column's field descriptor.rowIndex,columnIndex,rowCount,columnCount,isTableLayout, andisCardLayout: the cell's position and the current layout.pkKey,modelInfo, andmodelConfig: the model's pk field name, its Model Info, and its Model Config.
Declare only the props you use. Set inheritAttrs: false so the others do not become attributes on your root element. The built-in adapters do both.
<script setup>
defineOptions({ inheritAttrs: false });
defineProps({
value: { type: Number, default: null },
});
</script>
<template>
<span v-if="value != null" :class="value >= 0 ? 'text-green-600' : 'text-red-600'">{{ value }}</span>
</template>Register it through a function in model config, or pass it in the columnComponents prop.
Check the Result
- Each changed column renders its adapter in the table layout. Below the list's
tableBreakpoint, it renders the adapter in the card layout too. - A foreign key column links to the related row, and its URL carries the related row's pk. An empty value renders without a link.
- Columns you did not change render as before.
- No column error shows above the list.
Troubleshooting
The list shows "There was an error while rendering the list columns." A columnComponents entry or a type mapping's column does not name an adapter, or a function returned nothing. The message names the column, such as No column component named "Nope" for column "status", and adds in the type mapping when the mapping is the cause. That column's cells render empty, and the other columns render normally. Check the name against availableColumns, or pass your own component through a function. The error shows even when a field(<name>) slot fills the column. A list you build on useViewList gets these errors in columnErrors.
The override has no effect. Check that the key matches the field name exactly. A field(<name>) slot wins over both override maps, and a columnComponents prop entry wins over model config. A setConfig call made after the list has built its config does not reach that list, so call setConfig when the application starts.
A foreign key column renders text without a link. The value is empty or an array, or no related model resolved. Check the field's app_label and model in the model info response. Or supply app and model through columnProps.
A to-many relation renders as [2, 1]. A to-many relation's list value is an array of pks, and ColumnText renders it as JSON. To render linked items, expand the relation so each item carries a label, and write an adapter for it.
Attributes you did not set appear on your adapter's root element. Add defineOptions({ inheritAttrs: false }).