In a browser
import { Results } from './results.ts';
document.querySelector('#results').innerHTML = Results(items).markup;Behaviour goes on with a listener
Event handler attributes are refused, so behaviour is attached rather than written into the markup. One delegated listener on a container handles a list of any size, and keeps working after the list is re-rendered.
const list = document.querySelector('#results');
list.addEventListener('click', (e) => {
const button = e.target.closest('button[data-url]');
if (!button) return;
toggleSaved(button.dataset.url);
});This is why the example in Getting started puts data-url on the button. The markup carries the data; the listener carries the behaviour.
Custom elements for anything stateful
For a component that owns state over time, render the markup with a template and upgrade it with a custom element.
class SaveButton extends HTMLElement {
connectedCallback() {
this.addEventListener('click', () => this.toggle());
}
toggle() {
const pressed = this.getAttribute('aria-pressed') === 'true';
this.setAttribute('aria-pressed', String(!pressed));
}
}
customElements.define('save-button', SaveButton);html`<save-button ${attrs({ 'aria-pressed': saved, 'data-url': url })}>Save</save-button>`;Re-render the piece, not the page
Assigning to innerHTML throws away the nodes inside and their state — focus, scroll position, an open <details>. Ideally, render the smallest region that changed, or use a custom element that updates itself in place.
Importing less
If the browser only needs cx() and attrs() for class and style lists, @itsy/html/attrs provides them without the template scanner. See the import map.