Choice-Backed Fields and Lookup Models
This guide shows how to give a field a set of options, how to build a widget that loads those options, and how to declare a Lookup model for a table of codes and names.
A Choice-Backed Field carries choices in its Model Info entry. The value is either the full list of {label, value} pairs, or true when the options are another model's rows. Choice values are always strings. Primary Key and Identifier Discipline explains why.
Make a Field Choice-Backed
Declare the options on the server.
- For a fixed set, give the model field Django's
choices. For a filter, use aChoiceFilter. - For another model's rows, add a
ForeignKeyorManyToManyFieldto that model. For a filter, use aModelChoiceFilter. To offer the values that a column already holds, use anAllValuesFilter.
- For a fixed set, give the model field Django's
Give the related model a label. Each option shows the related row's Formatted Name. Create a CRUD Surface describes how to set it.
Make the options reachable. The source that the default UI loads options from depends on how you declare the field:
Model info choicesDeclared as Default UI loads options from A list Fixed choices on a field or filter The list itself, with no request trueA form field with a PrimaryKeyRelatedField, the default for a relationThe related model's list endpoint, searched as the user types trueA form field with a SlugRelatedFieldList field choices trueA filter List filterset field choices, when the filter form opens or the URL holds a value for the filter For the list endpoint, register and route the related model like any other model, as Create a CRUD Surface describes. Users who fill the field need
readandliston the related model.For the choice endpoints, users need
readon the source model. When the options are another model's rows, they also needliston that model. The filter choices endpoint builds its options only from the rows that the registered viewset'slistview reads. For another model's rows, the user must also be able to list each related row. Filter Choices and Permission Surfaces describes that rule.ModelInfoChoicesViewSetandModelInfoFilterSetChoicesViewSetdescribe each endpoint's checks, errors, ordering, and filter narrowing.Check the model info. Request
GET /routes/vueda.info/model_info/<app_label>/<model>/(Get model info). Find the field undermodel_fieldsor the filter undermodel_filtering. Fixed choices appear as a list. Another model's rows appear as"choices": true, withapp_labelandmodelnaming the related model.
Build a Widget That Loads Choices
The default widgets cover most choice-backed fields. Write a custom widget when a field needs a presentation that they do not offer. The widget below shows the options as radio buttons.
Write the widget. For a field whose
choicesistrue, the form passes the widgetfieldApp,fieldModel, andfieldName, which name the source model and the field.useModelChoicesloads the options from the field choices endpoint:vue<script setup> import { useModelChoices } from "@vueda/use/useModelChoices.js"; import { WIDGET_EMITS, WIDGET_PROPS, useWidget } from "@vueda/use/useWidget.js"; import { computed, ref, toRef } from "vue"; const props = defineProps({ ...WIDGET_PROPS, fieldApp: { type: String, required: true }, fieldModel: { type: String, required: true }, fieldName: { type: String, required: true }, }); const emit = defineEmits([...WIDGET_EMITS]); const widget = useWidget(props, emit); // Load the options once the field holds a value or gets focus. const hasBeenFocused = ref(false); const modelChoices = useModelChoices({ [props.fieldName]: { app: toRef(props, "fieldApp"), model: toRef(props, "fieldModel"), intendToFetch: computed(() => widget.state.combinedValue != null || hasBeenFocused.value), isFilter: false, }, }); const options = computed(() => modelChoices.choices[props.fieldName]?.results ?? []); // Choice values are strings, so compare the field's value as a string. const selected = computed({ get: () => (widget.state.combinedValue == null ? null : String(widget.state.combinedValue)), set: (value) => { widget.state.combinedValue = value; }, }); </script> <template> <fieldset @focusin.once="hasBeenFocused = true"> <label v-for="option in options" :key="option.value"> <input v-model="selected" type="radio" :value="option.value" :disabled="props.readOnly" /> {{ option.label }} </label> </fieldset> </template>WIDGET_PROPSanduseWidgetconnect the widget to the form. SetisFilter: trueto load from the filter choices endpoint instead.Assign the widget to the field with
setConfig:jsimport OrderStateRadios from "./OrderStateRadios.vue"; import { storeModelConfig } from "@vueda/stores/storeModelConfig.js"; storeModelConfig().setConfig( { app: "store", model: "customerorder" }, { widgetComponents: { order_state: OrderStateRadios } }, );Customize Field and Widget Rendering describes the widget contract and the other ways to assign a widget.
Check the result. Open the create view and focus the field. The browser sends one request to
/routes/vueda.info/model_info_choices/<app_label>/<model>/<field>/.
The client asks for one page of 200 options per field. Issue #381 tracks reaching rows past the first 200. Each new fetch sends a new request, and components that ask at the same time share one. Reactive Data Flow describes the choice store.
Declare a Lookup Model
Lookup is an abstract base for tables of codes and names, such as order states. It subclasses FormattedNameBaseModel, the base that it shares with VuedaModel. It adds a unique code, a name, and a formatted_name column that copies name.
Declare the model, and point a relation at it:
pythonfrom django.db import models from vueda.core.models import BaseModelMeta, Lookup, VuedaModel class OrderState(Lookup): class Meta(BaseModelMeta): ordering = ["name"] class CustomerOrder(VuedaModel): name = models.CharField(max_length=255) order_state = models.ForeignKey(OrderState, on_delete=models.PROTECT) class Meta(BaseModelMeta): passBase each
MetaonBaseModelMeta, which gives the model VUEDA'screate,read,update,delete, andlistpermissions.Add a serializer, filterset, and viewset:
pythonfrom vueda.core.filters import VuedaFilterSet from vueda.core.serializers import VuedaLookupSerializer from vueda.core.viewsets import VuedaViewSet class OrderStateSerializer(VuedaLookupSerializer): class Meta(VuedaLookupSerializer.Meta): model = OrderState class OrderStateFilterSet(VuedaFilterSet): class Meta: model = OrderState fields = ["id", "code", "name"] class OrderStateViewSet(VuedaViewSet): queryset = OrderState.objects.all() serializer_class = OrderStateSerializer filterset_class = OrderStateFilterSet search_fields = ["name", "code"]VuedaLookupSerializerlistsid,code,name, andformatted_name. The form's relation widget sends typed text tosearch_fields. It loads several selected rows at once through theidfilter.Route the viewset under the model name, and register it in your app's
ready():pythonrouter.register(r"orderstate", OrderStateViewSet)pythondef ready(self): from vueda.info import register from .serializers import OrderStateSerializer from .viewsets import OrderStateViewSet register(OrderStateSerializer, OrderStateViewSet)Create a CRUD Surface describes the router prefix and model-info registration.
Create and apply the migration. Add the rows through a data migration or the model's CRUD views.
Grant permissions. Users who set
order_stateneedreadandliston the lookup model, for examplestore.read_orderstateandstore.list_orderstate. Users who maintain the table also needcreate,update, anddelete.
The model info for customerorder now shows "choices": true on order_state, with model set to orderstate. The create and update forms search OrderState rows and show each row's formatted_name.