Django中嵌套MultiValue表单字段渲染问题:ProductPropertyField
Django多值表单字段:为每个ProductPropertyField实例添加独立fieldset
问题定位
当前所有ProductPropertyField实例被渲染在同一个<fieldset>里,原因是外层的ProductPropertiesWidget(继承自MultiWidget)默认会将所有子widget直接平铺渲染,不会为每个子ProductPropertyWidget单独生成fieldset,即便你给ProductPropertyWidget设置了use_fieldset=True,也会被外层widget的渲染逻辑覆盖。
解决方案
核心是重写ProductPropertiesWidget的render方法,手动为每个ProductPropertyWidget类型的子widget包裹独立fieldset,并保留最后一个新增属性输入框的原有渲染逻辑。同时修正ProductPropertiesWidget的decompress方法,使其匹配多字段的解构逻辑。
修改后的完整代码
from django import forms from django.utils.safestring import mark_safe from .models import ProductProperty class ProductPropertyWidget(forms.MultiWidget): use_fieldset = True def __init__(self, attrs={}, choices=[], key=None): key_widget = forms.TextInput(attrs=attrs.pop("key_widget", {})) value_widget = forms.Select( attrs=attrs.pop("value_widget", {}), choices=choices ) new_property_widget = forms.TextInput( attrs=attrs.pop("new_property_widget", {}) ) widgets = [ key_widget, value_widget, new_property_widget, ] super().__init__(widgets, attrs) self.key = key # 保存key作为fieldset的标题 def decompress(self, value): if value: key, value, new_property = value.split(",") return [key, value, new_property] return [None, None, None] class ProductPropertiesWidget(forms.MultiWidget): use_fieldset = True def __init__(self, widgets=[], attrs={}, field_labels=[]): super().__init__(widgets=widgets, attrs=attrs) self.field_labels = field_labels # 保存每个子字段的label def decompress(self, value): # 修正解构逻辑:每个子字段对应一个值,返回对应长度的列表 if value: return value.split(",") return [None for _ in self.widgets] def render(self, name, value, attrs=None): decompressed_value = self.decompress(value) output = [] final_attrs = self.build_attrs(attrs) # 遍历每个子widget,为ProductPropertyWidget类型添加fieldset for i, widget in enumerate(self.widgets): widget_value = decompressed_value[i] if i < len(decompressed_value) else None if isinstance(widget, ProductPropertyWidget): # 生成fieldset包裹当前widget fieldset_attrs = final_attrs.copy() fieldset_attrs['id'] = f"{name}_{i}" legend = self.field_labels[i] if i < len(self.field_labels) else widget.key output.append(f'<fieldset {self.flatten_attrs(fieldset_attrs)}>') output.append(f'<legend>{legend}</legend>') # 渲染当前widget的内容 widget_name = f"{name}_{i}" output.append(widget.render(widget_name, widget_value, attrs)) output.append('</fieldset>') else: # 渲染最后一个新增属性输入框 widget_name = f"{name}_{i}" if len(self.widgets) > 1 else name output.append(widget.render(widget_name, widget_value, attrs)) return mark_safe('\n'.join(output)) class ProductPropertyField(forms.MultiValueField): widget = ProductPropertyWidget def __init__(self, *args, **kwargs): key_field = forms.CharField() value_field = forms.ChoiceField(choices=kwargs.pop("choices", [])) new_property_field = forms.CharField() super().__init__( fields=[key_field, value_field, new_property_field], require_all_fields=False, *args, **kwargs, ) def compress(self, data_list): return ",".join(data_list) class ProductPropertiesField(forms.MultiValueField): def __init__(self, *args, **kwargs): widgets = [] fields = [] field_labels = [] # 收集每个子字段的label key_value = ProductProperty.objects.values("feature__key", "feature__value") values = {} for property in key_value: key = values.get(property["feature__key"], None) if not key: values[property["feature__key"]] = set() values[property["feature__key"]].add(property["feature__value"]) for key, values_set in values.items(): value_choices = [(v, v) for v in sorted(values_set)] widget = ProductPropertyWidget( attrs={"placeholder": "Some placeholder"}, choices=value_choices, key=key, ) field = ProductPropertyField(label=key, choices=value_choices) fields.append(field) widgets.append(widget) field_labels.append(key) # 保存当前字段的label # 添加新增属性字段 new_prop_field = forms.CharField( help_text="New property should be like property_key=value", label="New property", ) fields.append(new_prop_field) widgets.append(forms.TextInput(attrs={"placeholder": "Add new property"})) field_labels.append(new_prop_field.label) # 初始化外层widget时传入字段label列表 widget = ProductPropertiesWidget(widgets=widgets, field_labels=field_labels) super().__init__(fields=fields, widget=widget, *args, **kwargs) def compress(self, data_list): return ",".join(data_list)
关键改动说明
ProductPropertyWidget:添加self.key保存传入的key,用于作为fieldset的标题。ProductPropertiesWidget:- 新增
field_labels参数,用于接收每个子字段的label,作为对应fieldset的legend。 - 重写
render方法,判断子widget类型,如果是ProductPropertyWidget则用<fieldset>包裹,并添加<legend>;普通widget直接渲染。 - 修正
decompress方法,使其正确处理多字段的解构逻辑。
- 新增
ProductPropertiesField:新增field_labels列表收集每个子字段的label,初始化外层widget时传入该列表。
预期渲染效果
修改后每个ProductPropertyField实例会被包裹在独立的<fieldset>中,示例HTML输出如下:
<fieldset id="properties_0"> <legend>Material</legend> <input name="properties_0_0" required="" type="text" /> <select name="properties_0_1" required=""> <option value="brass">brass</option> ... </select> <input name="properties_0_2" required="" type="text" /> </fieldset> <fieldset id="properties_1"> <legend>Color</legend> <input name="properties_1_0" required="" type="text" /> <select name="properties_1_1" required=""> <option value="black">black</option> ... </select> <input name="properties_1_2" required="" type="text" /> </fieldset> <input name="properties_2" required="" type="text" placeholder="Add new property" />
内容的提问来源于stack exchange,提问作者get_into_jinto
相关产品推荐
相关产品推荐

