Lazy dashboard
Widgets that load when they are needed, each behind its own fallback.
Each widget is a lazy() component. A real app would pass
() => import('./widget.js'), here a delayed promise stands
in for the network. Every widget has its own Suspense
boundary, so a slow one never holds back the others.
Preview
Code
Save it as an .html file and open it in a browser. It loads
Signy and Iris CSS from the CDN, so there is nothing to install or build.
Show the full page
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Lazy dashboard - Signy example</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/iriscss@1/dist/iris.min.css">
<script src="https://cdn.jsdelivr.net/npm/signy@1/dist/signy.global.min.js"></script>
</head>
<body>
<main class="container" id="app"></main>
<script>
const { Component, Signal, application, html, lazy, Suspense, Show, For, preloadAll } = Signy;
class Weather extends Component {
render() {
return html`<p>☀️ 21°C in ${this.props.city}, light wind from the west.</p>`;
}
}
class Sales extends Component {
render() {
const days = [["Mon", 40], ["Tue", 65], ["Wed", 50], ["Thu", 80], ["Fri", 95]];
return html`
${days.map(([day, value]) => html`
<label>${day}<progress max="100" value=${value}></progress></label>
`)}
`;
}
}
class Team extends Component {
render() {
return html`
<ul>
<li>Ada, in a meeting</li>
<li>Grace, available</li>
<li>Alan, away until 3pm</li>
</ul>
`;
}
}
class Tasks extends Component {
render() {
return html`
<label><input type="checkbox" checked /> Send the invoices</label>
<label><input type="checkbox" /> Review the roadmap</label>
`;
}
}
// A real app would write lazy(() => import("./weather.js")), with the class as
// the module's default export. Here a delayed promise plays the network.
const pretendImport = (Widget, ms) => () =>
new Promise((resolve) => setTimeout(() => resolve({ default: Widget }), ms));
const WIDGETS = [
{ id: "weather", title: "Weather", component: lazy(pretendImport(Weather, 400)), props: { city: "Turin" } },
{ id: "sales", title: "Sales this week", component: lazy(pretendImport(Sales, 1500)) },
{ id: "team", title: "Team", component: lazy(pretendImport(Team, 900)) },
{ id: "tasks", title: "Tasks", component: lazy(pretendImport(Tasks, 1200)) },
];
class Dashboard extends Component {
visible = new Signal(["weather", "sales"]);
preloaded = new Signal(false);
add(id) {
this.visible.set((ids) => [...ids, id]);
}
async preload() {
await preloadAll(WIDGETS.map((widget) => widget.component));
this.preloaded.value = true;
}
// Each widget has its own Suspense boundary, so a slow one never holds back the others
widget(widget) {
const Widget = widget.component;
return html`
<article class="card">
<header><strong>${widget.title}</strong></header>
${Suspense({
fallback: html`<p aria-busy="true">Loading</p>`,
children: () => html`<${Widget} ...${widget.props ?? {}} />`,
})}
</article>
`;
}
render() {
return html`
<h1>Dashboard</h1>
<div class="cluster">
${WIDGETS.map((widget) => Show({
when: () => !this.visible.value.includes(widget.id),
children: html`<button class="small outlined" onClick=${() => this.add(widget.id)}>Add ${widget.title}</button>`,
}))}
<button class="small secondary" disabled=${() => this.preloaded.value} onClick=${() => this.preload()}>
${() => (this.preloaded.value ? "All widgets loaded" : "Preload all widgets")}
</button>
</div>
<div class="grid">
${For({
each: () => this.visible.value.map((id) => WIDGETS.find((widget) => widget.id === id)),
keyBy: "id",
children: (widget) => this.widget(widget),
})}
</div>
`;
}
}
application(Dashboard).mount("#app");
</script>
</body>
</html>
What it uses
- lazy(), Suspense and preloadAll()
- Show to add widgets on demand