Skip to content

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 ​

  1. Declare the options on the server.

  2. Give the related model a label. Each option shows the related row's Formatted Name. Create a CRUD Surface describes how to set it.

  3. Make the options reachable. The source that the default UI loads options from depends on how you declare the field:

    Model info choicesDeclared asDefault UI loads options from
    A listFixed choices on a field or filterThe 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 filterList 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 read and list on the related model.

    For the choice endpoints, users need read on the source model. When the options are another model's rows, they also need list on that model. The filter choices endpoint builds its options only from the rows that the registered viewset's list view 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. ModelInfoChoicesViewSet and ModelInfoFilterSetChoicesViewSet describe each endpoint's checks, errors, ordering, and filter narrowing.

  4. Check the model info. Request GET /routes/vueda.info/model_info/<app_label>/<model>/ (Get model info). Find the field under model_fields or the filter under model_filtering. Fixed choices appear as a list. Another model's rows appear as "choices": true, with app_label and model naming 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.

  1. Write the widget. For a field whose choices is true, the form passes the widget fieldApp, fieldModel, and fieldName, which name the source model and the field. useModelChoices loads 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_PROPS and useWidget connect the widget to the form. Set isFilter: true to load from the filter choices endpoint instead.

  2. Assign the widget to the field with setConfig:

    js
    import 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.

  3. 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.

  1. Declare the model, and point a relation at it:

    python
    from 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):
            pass

    Base each Meta on BaseModelMeta, which gives the model VUEDA's create, read, update, delete, and list permissions.

  2. Add a serializer, filterset, and viewset:

    python
    from 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"]

    VuedaLookupSerializer lists id, code, name, and formatted_name. The form's relation widget sends typed text to search_fields. It loads several selected rows at once through the id filter.

  3. Route the viewset under the model name, and register it in your app's ready():

    python
    router.register(r"orderstate", OrderStateViewSet)
    python
    def 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.

  4. Create and apply the migration. Add the rows through a data migration or the model's CRUD views.

  5. Grant permissions. Users who set order_state need read and list on the lookup model, for example store.read_orderstate and store.list_orderstate. Users who maintain the table also need create, update, and delete.

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.