如何在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
相关产品推荐
相关产品推荐

