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

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)

关键改动说明

  1. ProductPropertyWidget:添加self.key保存传入的key,用于作为fieldset的标题。
  2. ProductPropertiesWidget:
    • 新增field_labels参数,用于接收每个子字段的label,作为对应fieldset的legend。
    • 重写render方法,判断子widget类型,如果是ProductPropertyWidget则用<fieldset>包裹,并添加<legend>;普通widget直接渲染。
    • 修正decompress方法,使其正确处理多字段的解构逻辑。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 18:49:48