Список задач
На этой странице
Классический интерактивный интерфейс без фронтенд-фреймворка. sitelo собирает оболочку страницы; атрибуты событий вызывают import('/js/todo.js').then(…), так что модуль загружается только когда нужен. Полный исходник в examples/todo.
Что вы получаете
- Статический HTML с обработчиками
onsubmit/onload(и на элементах списка) src/js/todo.js— экспортируетhydrate,handleSubmit,handleChangeиhandleRemove- sitelo находит буквальные
import('/…')в HTML и собирает файл вdist/(см. Ресурсы)
Структура проекта
my-todo/
sitelo.config.js
package.json
src/
index.ht.js # статическая оболочка + встроенные обработчики import()
css/
styles.css
js/
todo.js # экспортируемые обработчики (загружаются по требованию)1. Встроенные импорты в странице
Никакого <script type="module" src>. Обработчики — это HTML-атрибуты, которые динамически импортируют модуль и вызывают экспорт, передавая this (элемент). Так модули страниц остаются свободны от браузерных API (см. ограничения JSX).
export default () => `
<html lang="ru">
<head>
<title>Задачи — 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>Задачи</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="Что нужно сделать?" required>
<button type="submit">Добавить</button>
</form>
<ul id="todo-list"></ul>
<p id="todo-empty" class="empty" hidden>Здесь пока ничего нет.</p>
<p class="meta"><span id="todo-count">0</span> осталось</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: 'ru' },
head(
title('Задачи — 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('Задачи'),
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: 'Что нужно сделать?',
required: '',
}),
button({ type: 'submit' }, 'Добавить'),
),
ul({ id: 'todo-list' }),
p({ id: 'todo-empty', class: 'empty', hidden: '' }, 'Здесь пока ничего нет.'),
p({ class: 'meta' }, span({ id: 'todo-count' }, '0'), ' осталось'),
),
),
)export default function Todos() {
return (
<html lang="ru">
<head>
<title>Задачи — sitelo</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body
{...{
// React превращает onLoad в синтетическое событие; spread позволяет вывести сырой onload=""
onload: "import('/js/todo.js').then((m) => m.hydrate())",
}}
>
<main>
<h1>Задачи</h1>
<form
id="todo-form"
autoComplete="off"
{...{
// по той же причине — сырой onsubmit="", а не onSubmit из React
onsubmit:
"event.preventDefault(); import('/js/todo.js').then((m) => m.handleSubmit(this))",
}}
>
<input
id="todo-input"
name="title"
type="text"
placeholder="Что нужно сделать?"
required
/>
<button type="submit">Добавить</button>
</form>
<ul id="todo-list" />
<p id="todo-empty" className="empty" hidden>
Здесь пока ничего нет.
</p>
<p className="meta">
<span id="todo-count">0</span> осталось
</p>
</main>
</body>
</html>
)
}2. Экспортируемые обработчики
Модуль — обычный ES-файл в src/js/. Элементы списка, созданные во время выполнения, используют тот же приём import('/js/todo.js').then(…) для onchange и 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': `Отметить "${todo.title}" как выполненную`,
}),
span({ class: 'todo-title' }, todo.title),
),
button(
{
type: 'button',
class: 'todo-remove',
onclick: IMPORT_REMOVE,
'aria-label': `Удалить "${todo.title}"`,
},
'Удалить',
),
),
)
.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. Запуск
npm install
npm run devЛибо npm run build и разместите dist/ где угодно, где отдают статические файлы.
Ресурсы и стили · Ограничения JSX · Базовый сайт