<import-element>

Loads an HTML file and defines every <custom-element> in it. Works in a page and inside component markup. realm.css hides it.

<import-element from="URL" [loading="lazy" for="tag-a tag-b"]></import-element>
<import-element from="components/todo-app.html"></import-element>
<todo-app></todo-app>

Attributes

Attribute Type Default Meaning
from URL required File to load, resolved against the element's base URL (the page). Without it REALM warns and does nothing
loading lazy eager With lazy, wait until one of the tags in for appears in the page or in a component's markup
for space-separated tag names none The tags the file defines. Required for loading="lazy". Without it the import starts at once with a warning

The attributes are read once, when the element first connects. Reconnecting does not import again.

Events

Fired on the <import-element>. They do not bubble. See Import events.

Event detail When
load { url: string } Every definition in the file is defined
error { url: string, status: number } The file could not be loaded. status is 0 for network errors

Neither fires if the element was removed before the file arrived. Handle them with onload / onerror attributes or addEventListener.

Behaviour

Example: lazy loading and the load event

Previewindex.htmltodo-app.htmltodos.js
Show a todo-app

todo-app.html is not loaded yet.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>import-element demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <import-element from="./todo-app.html" loading="lazy" for="todo-app" onload="document.getElementById('lazy-status').value = 'Loaded ' + event.detail.url"></import-element>

  <ui-button onclick="document.getElementById('lazy-target').replaceChildren(document.createElement('todo-app'))">Show a todo-app</ui-button>
  <p><output id="lazy-status">todo-app.html is not loaded yet.</output></p>
  <div id="lazy-target"></div>
</body>
</html>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/checkbox.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>

<custom-element name="todo-app">
  <script type="module">
    import { todos } from "./todos.js";

    const props = defineProps({ heading: prop.string().default("Todos") });
    const draft = useState({ text: "" });
    const filters = [
      { id: "all", label: "All" },
      { id: "active", label: "Active" },
      { id: "done", label: "Completed" },
    ];
    const view = useState({
      get filters() {
        return filters.map((filter) => ({
          ...filter,
          current: filter.id === todos.filter,
          variant: filter.id === todos.filter ? "secondary" : "ghost",
        }));
      },
    });

    function add(event) {
      event.preventDefault();
      const text = draft.text.trim();
      if (!text) return;
      todos.items.push({ id: crypto.randomUUID(), text, done: false, editing: false });
      draft.text = "";
    }

    function updateDraft(field) {
      draft.text = field.value;
    }

    function toggle(item) {
      item.done = !item.done;
    }

    function toggleAll() {
      const done = !todos.allDone;
      for (const item of todos.items) item.done = done;
    }

    function remove(item) {
      todos.items.splice(todos.items.indexOf(item), 1);
    }

    async function edit(item, text) {
      const row = text.closest("li");
      for (const other of todos.items) other.editing = false;
      item.editing = true;
      await tick();
      row.querySelector("input.edit").focus();
    }

    function save(item, field) {
      const text = field.value.trim();
      if (!item.editing) return;
      item.editing = false;
      if (text) item.text = text;
      else remove(item);
    }

    function editKeys(item, event) {
      if (event.key === "Enter") event.target.blur();
      if (event.key === "Escape") {
        event.target.value = item.text;
        item.editing = false;
        event.target.blur();
      }
    }

    function show(filter) {
      todos.filter = filter.id;
    }

    function clearDone() {
      todos.items = todos.items.filter((item) => !item.done);
    }
  </script>

  <style>
    :host {
      display: block;
      width: min(calc(var(--spacing) * 136), 100%);
    }
    h1 {
      margin: 0 0 calc(var(--spacing) * 6);
      font-size: var(--text-2xl);
      letter-spacing: -0.02em;
    }
    form {
      display: flex;
      gap: calc(var(--spacing) * 2);
    }
    form[data-empty] ui-checkbox {
      visibility: hidden;
    }
    form ui-input {
      flex: 1;
    }
    ul {
      margin: calc(var(--spacing) * 4) 0 0;
      padding: 0;
      list-style: none;
      border: 1px solid var(--border);
      border-radius: var(--radius-lg);
    }
    li {
      display: flex;
      align-items: center;
      gap: calc(var(--spacing) * 3);
      min-height: calc(var(--spacing) * 12);
      padding: 0 calc(var(--spacing) * 3);
      border-bottom: 1px solid var(--border);
    }
    li:last-child {
      border-bottom: 0;
    }
    .text {
      flex: 1;
      cursor: text;
      overflow-wrap: anywhere;
    }
    li[data-done] .text {
      color: var(--muted-foreground);
      text-decoration: line-through;
    }
    input.edit {
      flex: 1;
      height: calc(var(--spacing) * 8);
      padding: 0 calc(var(--spacing) * 2);
      border: 1px solid var(--ring);
      border-radius: var(--radius-md);
      background: transparent;
      color: inherit;
      font: inherit;
    }
    .remove {
      opacity: 0;
    }
    li:hover .remove,
    li:focus-within .remove {
      opacity: 1;
    }
    footer {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      justify-content: space-between;
      gap: calc(var(--spacing) * 2);
      margin-top: calc(var(--spacing) * 4);
      font-size: var(--text-sm);
      color: var(--muted-foreground);
    }
    .count,
    .actions {
      flex: 1 1 0;
      white-space: nowrap;
    }
    .actions {
      display: flex;
      justify-content: flex-end;
    }
    footer:not([data-has-done]) .clear {
      visibility: hidden;
    }
    .filters {
      display: flex;
      gap: calc(var(--spacing) * 1);
    }
    .empty {
      padding: calc(var(--spacing) * 8);
      text-align: center;
      color: var(--muted-foreground);
    }
  </style>

  <h1><slot name="@heading"></slot></h1>

  <form onsubmit="add(event)" _data-empty="#todos.isEmpty">
    <ui-checkbox aria-label="Mark all as complete" _checked="#todos.allDone" onchange="toggleAll()"></ui-checkbox>
    <ui-input name="todo" aria-label="New todo" placeholder="What needs to be done?" _value="#draft.text" oninput="updateDraft(this)"></ui-input>
    <ui-button type="submit">Add</ui-button>
  </form>

  <is-hidden when="#todos.isEmpty">
    <ul>
      <li repeat="#todos.visible" as="item" key="id" _data-done="#item.done">
        <ui-checkbox _checked="#item.done" onchange="toggle(item)"></ui-checkbox>
        <is-hidden when="#item.editing">
          <span class="text" ondblclick="edit(item, this)"><slot name="#item.text"></slot></span>
        </is-hidden>
        <is-visible when="#item.editing">
          <input class="edit" aria-label="Edit todo" _value="#item.text" onblur="save(item, this)" onkeydown="editKeys(item, event)">
        </is-visible>
        <ui-button class="remove" variant="ghost" size="icon-sm" aria-label="Delete" onclick="remove(item)">✕</ui-button>
      </li>
    </ul>

    <footer _data-has-done="#todos.hasDone">
      <span class="count"><slot name="#todos.summary"></slot></span>
      <div class="filters" role="group" aria-label="Filter">
        <repeat-list of="#view.filters" as="filter" key="id">
          <ui-button size="sm" _variant="#filter.variant" _aria-pressed="#filter.current" onclick="show(filter)"><slot name="#filter.label"></slot></ui-button>
        </repeat-list>
      </div>
      <span class="actions">
        <ui-button class="clear" size="sm" variant="ghost" onclick="clearDone()">Clear completed</ui-button>
      </span>
    </footer>
  </is-hidden>

  <is-visible when="#todos.isEmpty">
    <p class="empty">Nothing to do yet. Add your first task above.</p>
  </is-visible>
</custom-element>
import { defineStore } from "https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js";

export const todos = defineStore(
  "example-todos",
  {
    items: [],
    filter: "all",
    get visible() {
      if (this.filter === "active") return this.items.filter((item) => !item.done);
      if (this.filter === "done") return this.items.filter((item) => item.done);
      return this.items;
    },
    get remaining() {
      return this.items.filter((item) => !item.done).length;
    },
    get summary() {
      return `${this.remaining} ${this.remaining === 1 ? "item" : "items"} left`;
    },
    get hasDone() {
      return this.items.some((item) => item.done);
    },
    get isEmpty() {
      return this.items.length === 0;
    },
    get allDone() {
      return this.items.length > 0 && this.remaining === 0;
    },
  },
  { persist: "local" },
);

todo-app.html is fetched only when the button adds a <todo-app> to the page. onload is a native handler attribute, so it works on a plain page.

Edit this page on GitHub · View as Markdown