Todo-App
Auf dieser Seite
Eine klassische interaktive Oberfläche ohne Frontend-Framework. sitelo baut die Seitenhülle; die Event-Attribute rufen import('/js/todo.js').then(…) auf, sodass das Modul erst bei Bedarf lädt. Vollständiger Quelltext in examples/todo.
Was du bekommst
- Statisches HTML mit
onsubmit- /onload-Handlern (und solchen an den Listeneinträgen) src/js/todo.js— exportierthydrate,handleSubmit,handleChangeundhandleRemove- sitelo erkennt wörtliche
import('/…')im HTML und bündelt die Datei nachdist/(siehe Assets)
Projektstruktur
my-todo/
sitelo.config.js
package.json
src/
index.ht.js # statische Hülle + inline import()-Handler
css/
styles.css
js/
todo.js # exportierte Handler (bei Bedarf geladen)1. Inline-Importe in der Seite
Kein <script type="module" src>. Die Handler sind HTML-Attribute, die das Modul dynamisch importieren und einen Export aufrufen — mit this (dem Element) als Argument. So bleiben Seitenmodule frei von Browser-APIs (siehe JSX-Einschränkungen).
export default () => `
<html lang="de">
<head>
<title>Aufgaben — sitelo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body onload="import('/js/todo.js').then((m) => m.hydrate())">
<main>
<h1>Aufgaben</h1>
<form
id="todo-form"
autocomplete="off"
onsubmit="event.preventDefault(); import('/js/todo.js').then((m) => m.handleSubmit(this))"
>
<input id="todo-input" name="title" type="text" placeholder="Was ist zu tun?" required>
<button type="submit">Hinzufügen</button>
</form>
<ul id="todo-list"></ul>
<p id="todo-empty" class="empty" hidden>Hier ist noch nichts.</p>
<p class="meta"><span id="todo-count">0</span> offen</p>
</main>
</body>
</html>
`import {
html, head, title, meta, link, body, main, h1, form, input, button, ul, p, span,
} from 'javascript-to-html'
export default () =>
html({ lang: 'de' },
head(
title('Aufgaben — sitelo'),
meta({ name: 'viewport', content: 'width=device-width, initial-scale=1' }),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
{ onload: "import('/js/todo.js').then((m) => m.hydrate())" },
main(
h1('Aufgaben'),
form(
{
id: 'todo-form',
autocomplete: 'off',
onsubmit:
"event.preventDefault(); import('/js/todo.js').then((m) => m.handleSubmit(this))",
},
input({
id: 'todo-input',
name: 'title',
type: 'text',
placeholder: 'Was ist zu tun?',
required: '',
}),
button({ type: 'submit' }, 'Hinzufügen'),
),
ul({ id: 'todo-list' }),
p({ id: 'todo-empty', class: 'empty', hidden: '' }, 'Hier ist noch nichts.'),
p({ class: 'meta' }, span({ id: 'todo-count' }, '0'), ' offen'),
),
),
)export default function Todos() {
return (
<html lang="de">
<head>
<title>Aufgaben — sitelo</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body
{...{
// React macht aus onLoad ein synthetisches Event; per Spread geben wir ein rohes onload="" aus
onload: "import('/js/todo.js').then((m) => m.hydrate())",
}}
>
<main>
<h1>Aufgaben</h1>
<form
id="todo-form"
autoComplete="off"
{...{
// aus demselben Grund — rohes onsubmit="", nicht Reacts onSubmit
onsubmit:
"event.preventDefault(); import('/js/todo.js').then((m) => m.handleSubmit(this))",
}}
>
<input
id="todo-input"
name="title"
type="text"
placeholder="Was ist zu tun?"
required
/>
<button type="submit">Hinzufügen</button>
</form>
<ul id="todo-list" />
<p id="todo-empty" className="empty" hidden>
Hier ist noch nichts.
</p>
<p className="meta">
<span id="todo-count">0</span> offen
</p>
</main>
</body>
</html>
)
}2. Exportierte Handler
Das Modul ist eine ganz normale ES-Datei unter src/js/. Zur Laufzeit erzeugte Listeneinträge nutzen dasselbe Muster import('/js/todo.js').then(…) für onchange und onclick.
import { button, input, label, li, span } from 'javascript-to-html'
const STORAGE_KEY = 'sitelo-todo-example'
function loadTodos() {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return []
const parsed = JSON.parse(raw)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
function saveTodos(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}
function createId() {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
}
const IMPORT_CHANGE = "import('/js/todo.js').then((m) => m.handleChange(this))"
const IMPORT_REMOVE = "import('/js/todo.js').then((m) => m.handleRemove(this))"
function render() {
const list = document.querySelector('#todo-list')
const empty = document.querySelector('#todo-empty')
const count = document.querySelector('#todo-count')
if (!list || !empty || !count) return
const todos = loadTodos()
list.innerHTML = todos
.map((todo) =>
li(
{
class: todo.done ? 'todo is-done' : 'todo',
'data-id': todo.id,
},
label(
input({
type: 'checkbox',
onchange: IMPORT_CHANGE,
'aria-label': `Markiere "${todo.title}" als erledigt`,
}),
span({ class: 'todo-title' }, todo.title),
),
button(
{
type: 'button',
class: 'todo-remove',
onclick: IMPORT_REMOVE,
'aria-label': `Entfernen "${todo.title}"`,
},
'Entfernen',
),
),
)
.join('')
for (const cb of list.querySelectorAll('input[type="checkbox"]')) {
const item = cb.closest('[data-id]')
const todo = todos.find((t) => t.id === item?.dataset.id)
if (todo) cb.checked = todo.done
}
count.textContent = String(todos.filter((t) => !t.done).length)
empty.hidden = todos.length > 0
}
export function hydrate() {
render()
}
export function handleSubmit(form) {
const input = form.elements.namedItem('title')
if (!(input instanceof HTMLInputElement)) return
const title = input.value.trim()
if (!title) return
saveTodos([{ id: createId(), title, done: false }, ...loadTodos()])
input.value = ''
input.focus()
render()
}
export function handleChange(checkbox) {
const item = checkbox.closest('[data-id]')
if (!(item instanceof HTMLElement) || !item.dataset.id) return
saveTodos(
loadTodos().map((todo) =>
todo.id === item.dataset.id ? { ...todo, done: checkbox.checked } : todo,
),
)
render()
}
export function handleRemove(button) {
const item = button.closest('[data-id]')
if (!(item instanceof HTMLElement) || !item.dataset.id) return
saveTodos(loadTodos().filter((todo) => todo.id !== item.dataset.id))
render()
}3. Ausführen
npm install
npm run devOder npm run build und dist/ überall dort hosten, wo statische Dateien ausgeliefert werden.
Assets und Styling · JSX-Einschränkungen · Basis-Website