Skip to content

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

@preisbeck

Description

@preisbeck

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    complexity: highExpected to require a large amont of time and effort to implement relative to other tasksnetboxstatus: backlogAwaiting selection for worktype: featureIntroduction of new functionality to the application

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions