Support description annotations in static select dropdowns (parity with DynamicModelChoiceField) #114

Open
opened 2026-04-05 16:21:48 +02:00 by MrUnknownDE · 0 comments
Owner

Originally created by @preisbeck on 3/19/2026

NetBox version

v4.5.2

Feature type

Change to existing functionality

Proposed functionality

Add support for rendering description annotations (subtitle text below each option label) in static <select> dropdowns, matching the existing behavior of DynamicModelChoiceField's AJAX-powered dropdowns.

Currently, NetBox's TomSelect integration renders descriptions only for dynamic (API-powered) selects via DynamicTomSelect in dynamic.ts. Static selects initialized by initStaticSelects() in static.ts use NetBoxTomSelect without a custom render function, so there is no way to display descriptions.

Proposed implementation:

  1. New widget or enhancement to APISelect: Add a DescriptionSelect widget (or extend the existing Select widget) that accepts a description mapping and renders data-description attributes on <option> elements:
# Example widget
class DescriptionSelect(forms.Select):
    def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
        option = super().create_option(name, value, label, selected, index, subindex, attrs)
        if description := self.descriptions.get(value, ''):
            option['attrs']['data-description'] = description
        return option
  1. Update initStaticSelects() in static.ts: Add a custom render function that checks for data-description on <option> elements and renders them as <br /><small class="text-secondary">...</small> — the same HTML pattern already used in dynamic.ts:
// In static.ts — detect and render descriptions for static selects
function renderOption(data: TomOption, escape: typeof escape_html) {
  const option = select.querySelector(`option[value="${data.value}"]`);
  const description = option?.dataset.description;
  let html = `<div>${escape(data.text)}`;
  if (description) {
    html += `<br /><small class="text-secondary">${escape(description)}</small>`;
  }
  html += `</div>`;
  return html;
}
  1. Optional: A ModelChoiceField subclass (e.g., DescriptionModelChoiceField) that automatically populates the widget's description mapping from the queryset's .description field, similar to how DynamicModelChoiceField works with the API.

Use case

Plugin developers who need filtered dropdowns with description annotations currently have no clean path to achieve this. The core issue is that DynamicModelChoiceField ignores the .queryset set in __init__() for the dropdown UI — it fetches all options from the REST API via AJAX. The queryset only affects server-side validation on form submission.

This means:

  • DynamicModelChoiceField with a filtered queryset: dropdown shows all objects (queryset ignored for UI), descriptions work
  • ModelChoiceField with a filtered queryset: dropdown correctly filters, but no description annotations

Concrete example: Our plugin restricts which DeviceRole values are available per tenant hierarchy. The filtering logic (tenant group ancestry, cross-tenant intersection) cannot be expressed via query_params on the standard API endpoint. We need a ModelChoiceField with a custom queryset — but then we lose the description subtitle styling.

Current workaround: Build a custom REST API endpoint that duplicates the filtering logic, register a URL for it, and use DynamicModelChoiceField with widget=APISelect(api_url='/api/plugins/.../custom-endpoint/'). This works but requires significant boilerplate:

  • A custom DRF APIView returning {count, results} with id, display, description fields
  • URL registration for the endpoint
  • Using query_params with $field references for dynamic dependencies
  • Redundant server-side validation in clean() as a safety net

With native static description support, all of this would be replaced by a ModelChoiceField with a DescriptionSelect widget — a single line change.

Database changes

None. This is a frontend/widget change only.

External dependencies

None. Uses existing TomSelect library already bundled with NetBox.

*Originally created by @preisbeck on 3/19/2026* ### NetBox version v4.5.2 ### Feature type Change to existing functionality ### Proposed functionality Add support for rendering **description annotations** (subtitle text below each option label) in static `<select>` dropdowns, matching the existing behavior of `DynamicModelChoiceField`'s AJAX-powered dropdowns. Currently, NetBox's TomSelect integration renders descriptions only for dynamic (API-powered) selects via `DynamicTomSelect` in `dynamic.ts`. Static selects initialized by `initStaticSelects()` in `static.ts` use `NetBoxTomSelect` without a custom render function, so there is no way to display descriptions. **Proposed implementation:** 1. **New widget or enhancement to `APISelect`**: Add a `DescriptionSelect` widget (or extend the existing `Select` widget) that accepts a description mapping and renders `data-description` attributes on `<option>` elements: ```python # Example widget class DescriptionSelect(forms.Select): def create_option(self, name, value, label, selected, index, subindex=None, attrs=None): option = super().create_option(name, value, label, selected, index, subindex, attrs) if description := self.descriptions.get(value, ''): option['attrs']['data-description'] = description return option ``` 2. **Update `initStaticSelects()` in `static.ts`**: Add a custom render function that checks for `data-description` on `<option>` elements and renders them as `<br /><small class="text-secondary">...</small>` — the same HTML pattern already used in `dynamic.ts`: ```typescript // In static.ts — detect and render descriptions for static selects function renderOption(data: TomOption, escape: typeof escape_html) { const option = select.querySelector(`option[value="${data.value}"]`); const description = option?.dataset.description; let html = `<div>${escape(data.text)}`; if (description) { html += `<br /><small class="text-secondary">${escape(description)}</small>`; } html += `</div>`; return html; } ``` 3. **Optional: A `ModelChoiceField` subclass** (e.g., `DescriptionModelChoiceField`) that automatically populates the widget's description mapping from the queryset's `.description` field, similar to how `DynamicModelChoiceField` works with the API. ### Use case Plugin developers who need **filtered dropdowns with description annotations** currently have no clean path to achieve this. The core issue is that `DynamicModelChoiceField` ignores the `.queryset` set in `__init__()` for the dropdown UI — it fetches all options from the REST API via AJAX. The queryset only affects server-side validation on form submission. This means: - `DynamicModelChoiceField` with a filtered queryset: dropdown shows **all** objects (queryset ignored for UI), descriptions work - `ModelChoiceField` with a filtered queryset: dropdown correctly filters, but **no description annotations** **Concrete example**: Our plugin restricts which `DeviceRole` values are available per tenant hierarchy. The filtering logic (tenant group ancestry, cross-tenant intersection) cannot be expressed via `query_params` on the standard API endpoint. We need a `ModelChoiceField` with a custom queryset — but then we lose the description subtitle styling. **Current workaround**: Build a custom REST API endpoint that duplicates the filtering logic, register a URL for it, and use `DynamicModelChoiceField` with `widget=APISelect(api_url='/api/plugins/.../custom-endpoint/')`. This works but requires significant boilerplate: - A custom DRF `APIView` returning `{count, results}` with `id`, `display`, `description` fields - URL registration for the endpoint - Using `query_params` with `$field` references for dynamic dependencies - Redundant server-side validation in `clean()` as a safety net With native static description support, all of this would be replaced by a `ModelChoiceField` with a `DescriptionSelect` widget — a single line change. ### Database changes None. This is a frontend/widget change only. ### External dependencies None. Uses existing TomSelect library already bundled with NetBox.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github/netbox#114