Skip to content

ArrayWidget collapses every item to the first value when conditional_fields is set #2117

Description

@Cloves23

Did you used AI to write this issue?

Yes — root cause analysis and wording assisted by AI; reproduction and code references verified by hand against the installed source.

What version of Unfold are you using?

0.98.0 (latest release; the offending code is also present on main).

What version of Django are you using?

6.0 (6.0.6), Python 3.14.

What browser are you using?

Browser-independent — the corruption is produced by the server-emitted Alpine.js
bindings, so it happens in any browser with Alpine running. (Observed in Firefox /
Chromium.)

Did you checked changelog/commit history, if the bug is not already fixed?

Yes. changeform_condition in src/unfold/templatetags/unfold.py on main still
ends in the generic else branch that assigns a scalar x-model.fill to any
unrecognised widget — unchanged in 0.98.0.

Did you searched other issues, if the bug is not already fixed?

Yes. #825 is a different ArrayWidget bug (comma-separated values split on
decompress, closed as not planned); #1020 is about choices/Select rendering.
Neither covers the conditional_fields interaction described here.

Did you checked documentation?

Yes — both the ArrayWidget and
Conditional fields
pages. Neither documents a restriction on combining the two.

Are you able to replicate the bug in the demo site?

Not verified on the demo site (no model there appears to combine an ArrayField
with conditional_fields). It reproduces reliably with the minimal admin below.

Repository with reproduced bug

No separate repository; the bug reproduces with the minimal model + admin below,
and the client-side cause can be shown without a browser (see "Minimal proof").

Describe your issue

Summary

When a ModelAdmin declares both conditional_fields and an ArrayField
rendered with ArrayWidget, every item input of the array is rendered with the
same Alpine x-model binding pointing to a single scalar variable. On page load
Alpine collapses all inputs to the first item's value. Removing
conditional_fields makes ArrayWidget work correctly again.

Steps to reproduce

# models.py
from django.contrib.postgres.fields import ArrayField
from django.db import models

class BibEntry(models.Model):
    type = models.CharField(max_length=20, choices=[("book", "Book"), ("paper", "Paper")])
    authors = ArrayField(models.CharField(max_length=255), default=list)
    isbn = models.CharField(max_length=20, blank=True)
# admin.py
from django.contrib import admin
from django.contrib.postgres.fields import ArrayField
from unfold.admin import ModelAdmin
from unfold.contrib.forms.widgets import ArrayWidget

@admin.register(BibEntry)
class BibEntryAdmin(ModelAdmin):
    formfield_overrides = {ArrayField: {"widget": ArrayWidget}}
    conditional_fields = {
        "isbn": "type === 'book'",  # any conditional_fields entry triggers it
    }

Create a record with several authors, e.g.
authors = ["Thomas Lewis", "Fari Amini", "Richard Lannon"], then open the
change form.

  • Expected: three inputs, one per author, each with its own value.
  • Actual: three inputs, all showing "Thomas Lewis" (the first item).

Comment out conditional_fields and the widget renders the distinct values
correctly.

Root cause

The server renders the correct, distinct values — the corruption happens
client-side via the Alpine x-model binding that conditional_fields injects.

  1. In unfold/templates/unfold/helpers/fieldset_row.html, when the admin has any
    conditional_fields, has_conditional_display is truthy and the
    changeform_condition filter is applied to every field in the form — not
    only the fields listed in conditional_fields:

    {% if has_conditional_display %}
        {% with field|changeform_condition as field %}
  2. changeform_condition (unfold/templatetags/unfold.py) has special handling
    for UnfoldAdminSplitDateTimeWidget / UnfoldAdminMoneyWidget (which use
    distinct widgets_names suffixes), but ArrayWidget falls into the generic
    else branch:

    else:
        field.field.field.widget.attrs["x-model.fill"] = field.field.name

    So ArrayWidget.attrs["x-model.fill"] = "authors".

  3. ArrayWidget is a MultiWidget whose widgets_names are all "" (every item
    shares the field name). Django's MultiWidget.get_context copies the parent
    attrs onto every subwidget:

    widget_attrs = final_attrs.copy()  # carries x-model.fill="authors"

    Result: every author <input> gets name="authors" and
    x-model.fill="authors".

  4. changeform_data seeds the Alpine scope with authors: null. With N inputs
    two-way-bound to the same scalar via x-model, Alpine synchronises them all to
    one value on init — the first item — visually wiping the rest.

Minimal proof (no browser needed)

from unfold.contrib.forms.widgets import ArrayWidget
w = ArrayWidget()
w.attrs["x-model.fill"] = "authors"  # what changeform_condition injects
ctx = w.get_context("authors", ["A", "B", "C"], {"id": "id_authors"})
for s in ctx["widget"]["subwidgets"]:
    print(s["name"], s["attrs"].get("x-model.fill"))
# authors authors
# authors authors
# authors authors  -> all three inputs bound to the same scalar

Suggested fix

changeform_condition should not assign a scalar x-model.fill to widgets whose
subwidgets share a name (i.e. a MultiWidget with empty widgets_names, like
ArrayWidget). Either skip ArrayWidget entirely (an array field can't act as a
boolean condition source and rarely needs to drive visibility), or give it a
per-item binding instead of a shared scalar.

Workaround

Subclass ArrayWidget and strip the injected x-model attributes before render:

class ConditionalSafeArrayWidget(ArrayWidget):
    def get_context(self, name, value, attrs):
        self.attrs = {k: v for k, v in self.attrs.items() if not k.startswith("x-model")}
        if attrs:
            attrs = {k: v for k, v in attrs.items() if not k.startswith("x-model")}
        return super().get_context(name, value, attrs)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions