You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Django中实现三层级ChoiceField?求自定义Widget示例

解决Django三级ChoiceField的自定义Widget示例

首先修正你示例代码中的语法错误(元组需用逗号分隔,而非冒号):

CHOICES = (
    ("city_group", (
        ("USA", "USA"),
        ("Canada", "Canada"),
        ("UK", (
            ("England", "England"),
            ("Scotland", "Scotland"),
        )),
    )),
)

Django默认的ChoiceField和Select组件仅支持两层选项结构(分组+选项),三级结构会触发校验报错。下面提供两种自定义解决方案:

方案一:嵌套optgroup渲染(静态三级选项)

自定义Widget来渲染嵌套的<optgroup>标签,同时自定义Field适配三级结构的校验逻辑:

1. 自定义NestedSelect Widget

from django.forms.widgets import Select
from django.utils.html import format_html

class NestedSelect(Select):
    def render_option(self, selected_choices, option_value, option_label):
        # 识别三级选项:如果显示标签是可迭代对象,渲染为内层optgroup
        if isinstance(option_label, (list, tuple)):
            sub_options = []
            for sub_val, sub_label in option_label:
                sub_options.append(self.render_option(selected_choices, sub_val, sub_label))
            return format_html(
                '<optgroup label="{}">{}</optgroup>',
                option_value,
                ''.join(sub_options)
            )
        # 渲染普通选项
        return super().render_option(selected_choices, option_value, option_label)

2. 自定义NestedChoiceField

from django.core.exceptions import ValidationError
from django.forms.fields import ChoiceField

class NestedChoiceField(ChoiceField):
    def _choices_validator(self, value):
        # 递归遍历所有层级,提取有效选项值
        def flatten_choices(choices):
            for val, label in choices:
                if isinstance(label, (list, tuple)):
                    yield from flatten_choices(label)
                else:
                    yield val
        valid_values = set(flatten_choices(self.choices))
        if value not in valid_values:
            raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')

3. 在表单中使用

from django import forms

class LocationForm(forms.Form):
    location = NestedChoiceField(
        choices=CHOICES,
        widget=NestedSelect()
    )

渲染后的HTML会生成嵌套的optgroup结构,用户可以直接选择最底层的选项。


方案二:前端联动下拉(动态三级选择)

如果需要实现“国家→地区”的联动效果,可自定义Widget并配合JS实现:

1. 自定义CascadingSelect Widget

from django.forms.widgets import Select
from django.utils.html import format_html
import json

class CascadingSelect(Select):
    class Media:
        # 引入自定义JS文件,需将文件放在静态资源目录
        js = ('js/cascading_select.js',)

    def render(self, name, value, attrs=None, renderer=None):
        base_html = super().render(name, value, attrs, renderer)
        # 将choices转为JSON格式传给前端JS
        choices_json = json.dumps(self.choices)
        return format_html(
            '{0}<script>window.{1}_choices = {2};</script>',
            base_html,
            name,
            choices_json
        )

2. 编写联动JS(cascading_select.js)

document.addEventListener('DOMContentLoaded', function() {
    const selectEl = document.getElementById('id_location');
    const rawChoices = window.location_choices[0][1]; // 取第一级分组下的选项

    // 初始化第一级选项
    function loadLevelOptions(options) {
        selectEl.innerHTML = '';
        options.forEach(item => {
            const option = document.createElement('option');
            option.value = item[0];
            option.textContent = typeof item[1] === 'string' ? item[1] : item[0];
            selectEl.appendChild(option);
        });
    }

    loadLevelOptions(rawChoices);

    // 监听选择事件,加载下一级选项
    selectEl.addEventListener('change', function() {
        const selectedVal = this.value;
        const targetItem = rawChoices.find(item => item[0] === selectedVal);
        if (targetItem && typeof targetItem[1] === 'object') {
            loadLevelOptions(targetItem[1]);
        }
    });
});

3. 表单中使用

class LocationForm(forms.Form):
    location = ChoiceField(
        choices=CHOICES,
        widget=CascadingSelect()
    )

此方案会根据用户的选择动态切换下拉选项,可根据实际需求完善初始值回显等逻辑。

内容的提问来源于stack exchange,提问作者TerrySoki

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 18:31:03