Skip to content

Components drop every attribute they do not declare, in both engines #70

Description

@fsecada01

Summary

No component in either engine forwards attributes it does not declare. Every theme partial renders a fixed attribute list, so anything the caller adds beyond the declared props — hx-*, @click, x-*, id, data-*, aria-*, name, form — is dropped.

Like #65, this fails silently. The component renders, styles correctly, and reads correctly at the call site. What it does not do is behave. On an interactive component that is the whole point of the element.

Scope

Measured on 0.3.1: 210 templates reference attrs zero times — 105 cotton theme partials and 105 JinjaX components, five themes each, across all 21 components.

So this is not a gap in one component. It is the current contract everywhere.

Reproduction

{% load cotton %}
<c-cf.button variant="primary" hx-post="/enrich/" hx-target="#panel" hx-swap="innerHTML">
  Enrich
</c-cf.button>

Rendered (0.3.1, CF_UI_THEME = "bulma"):

<button class="button is-primary" type="button">Enrich</button>

Correct classes, correct element, correct label. The HTMX wiring is gone, so the button does nothing when clicked. Same for <c-cf.icon id="x" data-state="ok">, which loses both attributes.

Why the fix is small

django-cotton already collects undeclared attributes into attrs, and that context survives the wrapper's {% include %} into the theme partial. I verified this by reproducing cf-ui's own wrapper/partial structure locally rather than reading it off the source:

{# probe_widget.html — mimics cf/button.html #}
<c-vars variant="neutral" class="" />
{% include "probe_partial.html" %}

{# probe_partial.html — mimics _themes/bulma/button.html, plus attrs #}
<button class="btn {{ variant }} {{ class }}" {{ attrs }}>{{ slot }}</button>

Called as <c-probe_widget variant="primary" hx-post="/x/" hx-target="#t" @click="go()">Go</c-probe_widget>, that renders:

<button class="btn primary " hx-post="/x/" hx-target="#t" @click="go()">Go</button>

Declared props (variant) are correctly excluded from attrs; everything else comes through. So on the cotton side the change is one {{ attrs }} per partial — the plumbing already works and is simply not being used.

On the JinjaX side the analogous mechanism is the implicit attrs object rather than a declared param. I could not verify that half locally (the consuming repo is Django-only, no JinjaX installed), so treat the Jinja API as needing your confirmation rather than as asserted.

Impact in a real consumer

RankedJobs (fsecada01/Ranked-Jobs) is the only repo declaring cf-ui. Its cotton layer has 29 button-classed tags, and 15 of them — 52% — carry hx-post/hx-target/hx-swap/hx-vals driving the HTMX panel refresh cycle. Repo-wide across all cf-ui-mappable constructs it is 24 of 217 tags.

This is not hypothetical. That repo just merged the first slice of its cf-ui adoption (Ranked-Jobs#247) and had to stop at notification because of this. The adoption ticket's premise was that swapping raw markup for cf-ui components on the bulma theme is visually inert, since the bulma partials emit bulma classes. That premise holds for classes and says nothing about behaviour, which is exactly what this issue is.

The result is that roughly 107 of ~121 mappable constructs in that layer stayed raw — button 27, icon 34, tag 21, table 9, box 8, heading 6 — not because the components are wrong, but because converting them would ship inert markup. That PR pinned the constraint in a test (test_cf_ui_attribute_passthrough.py) so a release that fixes this shows up there as a failing assertion.

Worth noting the failure mode is the same one as #65 and lands in the same place: markup that looks right in review, renders right in the browser, and silently does nothing. #65 cost that repo three months of empty notification boxes. This one is currently costing it the entire rest of its migration.

Proposed fix

Add {{ attrs }} to the outermost element of each cotton theme partial, and the JinjaX equivalent to each .jinja component.

Three design questions I would not want to answer for you:

Which components? Every one is defensible and consistent. A narrower pass covering the interactive set first — button, select, textarea, form-field, checkbox-group, icon, badge, box — would unblock essentially all real usage. The data-driven ones (table, pagination, breadcrumb, progress) benefit less, though id and aria-* still apply to them.

Where multiple elements exist. form-field and checkbox-group render a wrapper plus a control. Caller attributes almost always mean the control, not the wrapper, which is a real decision rather than a mechanical edit — and an argument for handling those separately from the single-element components.

Collision with declared props. <c-cf.button class="x" hx-post="/y/"> must not emit class twice. Cotton excludes declared props from attrs, which the probe above confirms, but it is worth an explicit test per component given class is declared on nearly all of them.

Escaping

Given 0.2.0's autoescape change and the hardening in #36, this deserves care rather than assumption. Attribute values here come from the template author, not from end users — but templates interpolate user data into them freely (hx-vals with a rendered id is the common case in the consumer above), so attrs must escape values on the way out. Cotton's attrs handles this; the JinjaX side should be asserted rather than inferred, since that is precisely the path #36 was about.

Suggested tests

  • Per component, per engine: an undeclared attribute reaches the rendered element.
  • Per component: a declared prop passed alongside undeclared ones is not duplicated — specifically class.
  • Attribute values containing quotes and < are escaped, on both engines.
  • Boolean/valueless attributes (disabled, required, x-cloak) survive as bare attributes rather than becoming disabled="" — or, if normalising them is intended, that is pinned deliberately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions