<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
- Each URL is fetched once per page. Several
<import-element>tags, orimportDefinitionscalls, for the same file share one request. - At most six imports per origin load at once. The rest wait in order.
- The file is parsed as an HTML document, never executed. Only its
<custom-element>elements are used. Other content is ignored. - Relative URLs inside definitions (
<link rel="stylesheet">,importin scripts) resolve against the imported file, not the page. - Files must be served over HTTP.
file://pages cannot fetch them.
Example: lazy loading and the load event
<!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.