Async panels
Load panel content with promises, fetch, or heavy computation
Live demo
const panel = new Panel('#async-panel-recipe');
panel.onBeforeOpen((el, signal) => {
return slowFetch('https://dummyjson.com/recipes/1', { signal })
.then(r => r.json())
.then(data => {
el.querySelector('.panel-wrapper').innerHTML = renderRecipe(data);
});
}, { once: true });
The panel below fetches a random recipe from a public API when opened. A simulated delay makes the spinner visible. With { once: true } the content is cached, so reopening the panel skips the fetch.
The onBeforeOpen() callback
Use onBeforeOpen() to add async loading:
const panel = new Panel('#my-panel');
panel.onBeforeOpen((el, signal) => {
// Return a promise to delay opening
return fetch(`/api/content`, { signal })
.then(response => response.text())
.then(html => {
el.querySelector('.panel-wrapper').innerHTML = html;
});
}, { once: true }); // Load once, cache forever
Parameters
- el
- The panel
HTMLElementbeing opened - signal
AbortSignalfor cancelling the operation if the user closes the panel- options.once
trueloads once and caches;falsealways reloads. Default:false
Using the event directly
For more control, listen to panel:beforeopen on the element:
document.querySelector('#my-panel').addEventListener('panel:beforeopen', e => {
// panel:beforeopen bubbles, so read the panel from e.target (the dispatching panel). e.currentTarget breaks under event delegation; e.target does not.
const panel = e.target;
const { signal } = e.detail;
if (panel.dataset.loaded === 'true') return; // skip if already loaded
e.detail.waitUntil(
fetch('/api/content', { signal })
.then(r => r.text())
.then(html => {
panel.querySelector('.panel-wrapper').innerHTML = html;
panel.dataset.loaded = 'true';
})
);
});
Event detail properties
- signal
AbortSignal: pass tofetch()so in-flight requests cancel when the user closes the panel before it opens.- waitUntil(promise)
- Recommended. Delay opening until
promiseresolves. Safe to call more than once: the open waits for all of them. - promise
Promise | null: the underlying propertywaitUntil()sets. You can still assign it directly, butwaitUntil()is preferred. Leave asnullto open immediately.- trigger
HTMLElement | null: the element that initiated the open (e.g. the button).
Heavy computation
Works for CPU-heavy operations too, not just network requests. Two flavours:
Simulated slow load (demo only)
This one does no real work. It only simulates latency with a timer, so the spinner is easy to see. The signal cancels the timer if you close the panel first.
panel.onBeforeOpen((el, signal) => new Promise((resolve, reject) => {
// Demo only: no real work happens here, the timer just simulates latency.
const t = setTimeout(() => {
el.querySelector('.panel-wrapper').innerHTML = '<h3>Done</h3><p>Simulated 2s load.</p>';
resolve();
}, 2000);
signal.addEventListener('abort', () => {
clearTimeout(t);
reject(new DOMException('Aborted', 'AbortError'));
});
}), { once: true });
Genuine CPU-heavy work
Real heavy work has to leave the main thread. Doing it synchronously inside the promise would still freeze the UI (and the spinner); a Web Worker is what keeps the page responsive. The signal terminates the worker on close.
panel.onBeforeOpen((el, signal) => new Promise((resolve, reject) => {
const worker = new Worker('/assets/scripts/fib-worker.js');
worker.postMessage(42);
worker.onmessage = (e) => {
el.querySelector('.panel-wrapper').innerHTML =
`<h3>Calculation complete</h3><p>fibonacci(42) = ${e.data}</p>`;
worker.terminate();
resolve();
};
signal.addEventListener('abort', () => {
worker.terminate();
reject(new DOMException('Aborted', 'AbortError'));
});
}), { once: false });
The worker file (fib-worker.js) does the calculation off the main thread:
// fib-worker.js
onmessage = (e) => {
const fib = (n) => (n <= 1 ? n : fib(n - 1) + fib(n - 2));
postMessage(fib(e.data));
};
Loading states
Loading spinner
During async operations, Panel shows a spinner. It only appears if loading takes longer than loadingDelay ms. Fast loads open directly without a spinner:
const panel = new Panel('#my-panel', {
loadingDelay: 320 // Wait 320ms before showing spinner
});
Or via data attribute:
<div data-panel data-panel-loading-delay="320">...</div>
Loading height
Reserve vertical space for the spinner while content loads:
const panel = new Panel('#my-panel', {
loadingHeight: 200 // Example increase from default 150 during load
});
<div data-panel data-panel-loading-height="200">...</div>
Abort handling
When the user closes the panel during loading, the operation is automatically cancelled via the AbortSignal:
panel.onBeforeOpen((el, signal) => {
// Pass signal to fetch (automatically cancels on close)
return fetch('/api/data', { signal })
.then(/* ... */);
});
If you need to detect the abort (e.g. to clean up state), check the signal directly:
panel.onBeforeOpen((el, signal) => {
signal.addEventListener('abort', () => {
console.log('Load cancelled');
});
return fetch('/api/data', { signal });
});