wcb/demo/examples/boolean-props/index.js
Ayo 1b3e251a9d
Some checks are pending
Tests / Unit (push) Waiting to run
Tests / E2E (push) Waiting to run
feat!: reflect boolean props as bare attributes
Boolean props follow the HTML presence/absence semantics.
- We stick close to standard HTML behavior: flag="false" is treated as
true in most cases. Standard bare attributes disabled and required for
form fields are interpreted as true with non-existence as false.
- The use-cases for aria-*="false" and contenteditable="false" are also
supported by typing them in the JS class as string ("true" | "false").
This "true" | "false" opt-in is types-only. At runtime it's just a
string, and strings already serialize literally and are never removed —
so the enumerated/aria path needs no runtime code.
2026-07-20 19:57:23 +02:00

113 lines
3.5 KiB
JavaScript

import { WebComponent, html } from 'web-component-base'
/**
* Boolean props reflect as **bare attributes**: `true` sets the attribute with
* no value, `false` removes it entirely — exactly how native `disabled` and
* `required` behave.
*
* Two things that silently broke under the old string reflection now work:
*
* - `el.toggleAttribute(name, force)` only ever adds/removes an attribute,
* it never rewrites a value. With `flag="false"` sitting there, forcing
* `true` changed nothing — no attributeChangedCallback, no re-render.
* - `[flag]` CSS selectors match on *presence*, so a stamped `flag="false"`
* matched too and painted the "on" style onto an element whose prop was
* false.
*/
class FlagBox extends WebComponent {
static props = { flag: false }
static shadowRootInit = { mode: 'open' }
static styles = `
:host {
display: block;
padding: 0.8em 1em;
border: 2px solid #8884;
border-radius: 8px;
font-family: system-ui, sans-serif;
}
/* presence selector: matches only while the prop is actually true */
:host([flag]) {
border-color: #c2410c;
background: #c2410c22;
}
.state { font-weight: 600; }
`
get template() {
return html`
<div>flag is <span class="state">${String(this.props.flag)}</span></div>
`
}
}
customElements.define('flag-box', FlagBox)
/**
* The panel wires the three ways host code drives a boolean prop, and prints
* what the DOM actually looks like after each one.
*/
class BooleanDemo extends WebComponent {
static props = { log: '' }
onInit() {
this.box = null
}
afterViewInit() {
this.box = this.querySelector('flag-box')
this.#report('initial mount')
}
#report(action) {
const el = this.box
if (!el) return
const attr = el.hasAttribute('flag')
? `flag="${el.getAttribute('flag')}"`
: '(absent)'
this.props.log = `${action} → prop: ${el.props.flag} · attribute: ${attr}`
}
#toggle() {
// the platform API — a silent no-op back when flag="false" was stamped
this.box.toggleAttribute('flag', !this.box.props.flag)
this.#report('toggleAttribute()')
}
#write(value) {
this.box.props.flag = value
this.#report(`props.flag = ${value}`)
}
#stringly() {
// the pre-v6 idiom: any present value is true, so this turns the flag ON
// even though it reads as "off". wcb warns in the console when it sees it.
this.box.setAttribute('flag', 'false')
this.#report('setAttribute("flag", "false")')
}
get template() {
return html`
<flag-box></flag-box>
<p class="log"><code>${this.props.log}</code></p>
<div class="row">
<button onclick=${() => this.#toggle()}>toggleAttribute()</button>
<button onclick=${() => this.#write(true)}>props.flag = true</button>
<button onclick=${() => this.#write(false)}>props.flag = false</button>
<button onclick=${() => this.#stringly()}>
setAttribute("flag", "false") ⚠️
</button>
</div>
<p class="hint">
The last button is the migration trap:
<strong>any present value is true</strong>, so writing the string
<code>"false"</code> turns the flag <em>on</em> — just like native
<code>disabled="false"</code> is still disabled. Check the console for
the warning wcb logs. Use
<code>toggleAttribute(name, bool)</code> instead.
</p>
`
}
}
customElements.define('boolean-demo', BooleanDemo)