Skip to content

Passthrough: a name-only attribute renders bare, as written #96

Open

Description

@hugithordarson

An attribute written without a value in a template should render without one:

<wo:script filename="app.js" defer />   →   <script src="…" defer></script>

Today it renders defer="true". This issue is parked: see Why this waits below.

What the template says is what gets rendered

A name-only attribute and a Boolean value are different things, and each should render as written:

Template Should render Renders today
defer defer defer="true"
defer="$true" defer="true" defer="true"
aria-expanded="$open" (a Boolean) aria-expanded="true" / "false" same
a binding that resolves to null attribute omitted attribute omitted

The parser keeps the distinction (NGBindingValue.BooleanPresence). NGAssociationFactory then turns it into the same constant Boolean.TRUE association that $true produces, so the elements can no longer tell them apart.

Bound Booleans must keep rendering their string value. They are not given HTML boolean semantics (true renders bare, false omits the attribute), because many attributes need the strings:

  • Boolean attributes (defer, async, disabled, required, checked, multiple, …): only presence counts. defer="false" still means on. The only valid forms are defer, defer="" and defer="defer".
  • Enumerated attributes (contenteditable, draggable, spellcheck): "true" and "false" are real values, so spellcheck="false" turns spellcheck off.
  • aria-*: string values. aria-expanded="false" means collapsed; leaving it out means "unknown".
  • data-*: whatever the page's script reads.

There's no rule based on the value's type that works for all of them, so the rendering follows what the template wrote.

Why this waits

Every element renders its attributes through two helpers in NGHTMLUtilities, addAssociationValuesToAttributes and createElementStringWithAttributes. They were written as temporary helpers:

  • They pass attributes as Map<String, String>, where a bare attribute has no value to store.
  • null already means "omit".

Representing a bare attribute means changing those helpers and their 16 callers. The options considered:

  1. A typed marker value: the maps become Map<String, Object>, and a bare attribute's value is a marker (e.g. NGHTMLUtilities.BARE).
  2. Bare names tracked separately: addAssociationValuesToAttributes returns the set of bare names, and createElementStringWithAttributes takes it as an extra argument.
  3. A small attributes type in place of the map: put( name, value ), putBare( name ), render( tag, close ).

Each one reworks a temporary helper instead of replacing it. The fix belongs in whatever properly replaces attribute rendering (probably the render redesign's output work), where name-only attributes can be part of the design from the start. Until then, this stays open.

Until then

  • A name-only attribute renders with the value "true". defer renders defer="true", which browsers treat the same as a bare defer. The output doesn't match the template, but it behaves correctly.
  • To leave an attribute out, bind a value that resolves to null, not false. A Boolean false renders defer="false", which still counts as present, so the script is still deferred. For a conditional boolean attribute, bind something like defer="$deferOrNull" that returns the attribute's name or null. WebObjects and Wonder have the same trap.

Possibly later

  • A way to drop a boolean attribute when its bound value is false, without writing a method that returns null. That's a separate feature on top of this one: either a binding-level opt-in or a rule per attribute name.
  • Documentation: once this lands, describe the "rendered as written" rule wherever passthrough is documented.
  • Release note: templates that use name-only attributes on passthrough elements (e.g. <wo:textfield required />) will render required instead of required="true".

Activity

  1. hugithordarson commented on Oct 17, 2026

    @hugithordarson
    CollaboratorAuthor

    Parked: this waits on replacing the attribute helpers, not changing them. NGHTMLUtilities.addAssociationValuesToAttributes and createElementStringWithAttributes are temporary helpers built on Map<String, String>, where a bare attribute has no value to store. When attribute rendering is replaced properly, name-only attributes and bare rendering should be part of the new design from the start.

    Until then:

    • A name-only attribute renders with the value "true". <wo:script filename="app.js" defer /> renders defer="true". Browsers treat this the same as a bare defer: for HTML boolean attributes only presence counts.
    • To leave an attribute out, bind a value that resolves to null, not false. A null value is omitted. A Boolean false renders defer="false", which still counts as present, so the script is still deferred. For a conditional boolean attribute, bind something like defer="$deferOrNull" that returns the attribute's name or null.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions