docs(cabana): widget element contract and payload fixture (quick-261006-eyj)

- acme-demo-lookup fixture is a reorder widget: a click reverses its items,
  sends the new id order as detail.payload and renders the returned items from
  the data attribute and the summer-result event with textContent, no HTTP
- partials-and-widgets gains a Widget element contract subsection listing the
  attributes the SPA sets and the summer-action / summer-result events
- cabana README names both events in the form widgets bullet
This commit is contained in:
Jakub Zych
2026-10-06 11:18:03 +02:00
parent c6f68e4639
commit e723c391fb
3 changed files with 66 additions and 6 deletions

View File

@@ -1,16 +1,60 @@
// A plain custom element: one light-DOM button that asks the admin SPA to
// run the field's action. It makes no request and reads no cookie or
// storage; the SPA owns HTTP.
// A plain custom element that demonstrates a reorder widget: one light-DOM
// button and an ordered list. The SPA owns HTTP; this element only speaks the
// summer-action / summer-result events. It makes no request and reads no
// cookie or storage. A click reverses the current items (the simplest stand-in
// for a drag reorder) and sends the new id order as the action's payload; the
// returned data (its items array) arrives both as the `data` attribute and as
// the summer-result event's detail.data, and is rendered with textContent.
class AcmeDemoLookup extends HTMLElement {
static get observedAttributes() {
return ['data']
}
constructor() {
super()
this.items = []
this.list = null
this.addEventListener('summer-result', (event) => this.receive(event.detail && event.detail.data))
}
connectedCallback() {
if (this.firstChild) return
const button = document.createElement('button')
button.type = 'button'
button.textContent = this.getAttribute('label') || ''
button.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('summer-action', { bubbles: true, composed: true }))
this.items = this.items.slice().reverse()
this.render()
const payload = { order: this.items.map((item) => item.id) }
this.dispatchEvent(new CustomEvent('summer-action', { bubbles: true, composed: true, detail: { payload } }))
})
this.append(button)
this.list = document.createElement('ol')
this.append(button, this.list)
this.render()
}
attributeChangedCallback(name, _old, value) {
if (name !== 'data') return
try {
this.receive(value === null ? null : JSON.parse(value))
} catch {
this.receive(null)
}
}
receive(data) {
this.items = data && Array.isArray(data.items) ? data.items.filter((item) => item && typeof item === 'object') : []
this.render()
}
render() {
if (!this.list) return
this.list.textContent = ''
for (const item of this.items) {
const li = document.createElement('li')
li.textContent = String(item.title == null ? item.id : item.title)
this.list.append(li)
}
}
}
if (!customElements.get('acme-demo-lookup')) {