Skip to content

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

Open the page on its own

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