Repository navigation
Description
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 aredefer,defer=""anddefer="defer". - Enumerated attributes (
contenteditable,draggable,spellcheck):"true"and"false"are real values, sospellcheck="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. nullalready means "omit".
Representing a bare attribute means changing those helpers and their 16 callers. The options considered:
- A typed marker value: the maps become
Map<String, Object>, and a bare attribute's value is a marker (e.g.NGHTMLUtilities.BARE). - Bare names tracked separately:
addAssociationValuesToAttributesreturns the set of bare names, andcreateElementStringWithAttributestakes it as an extra argument. - 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".deferrendersdefer="true", which browsers treat the same as a baredefer. The output doesn't match the template, but it behaves correctly. - To leave an attribute out, bind a value that resolves to
null, notfalse. A Booleanfalserendersdefer="false", which still counts as present, so the script is still deferred. For a conditional boolean attribute, bind something likedefer="$deferOrNull"that returns the attribute's name ornull. 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 returnsnull. 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 renderrequiredinstead ofrequired="true".
Activity
hugithordarson commentedon Oct 17, 2026
Parked: this waits on replacing the attribute helpers, not changing them.
NGHTMLUtilities.addAssociationValuesToAttributesandcreateElementStringWithAttributesare temporary helpers built onMap<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:
"true".<wo:script filename="app.js" defer />rendersdefer="true". Browsers treat this the same as a baredefer: for HTML boolean attributes only presence counts.null, notfalse. A null value is omitted. A Booleanfalserendersdefer="false", which still counts as present, so the script is still deferred. For a conditional boolean attribute, bind something likedefer="$deferOrNull"that returns the attribute's name ornull.