App de tareas
En esta página
Una interfaz interactiva clásica sin framework de frontend. sitelo construye la estructura de la página; los atributos de evento llaman a import('/js/todo.js').then(…), así que el módulo se carga solo cuando hace falta. Código completo en examples/todo.
Qué obtienes
- HTML estático con manejadores
onsubmit/onload(y también en los elementos de la lista) src/js/todo.js— exportahydrate,handleSubmit,handleChangeyhandleRemove- sitelo detecta los
import('/…')literales del HTML y empaqueta el archivo endist/(ver Recursos)
Estructura del proyecto
my-todo/
sitelo.config.js
package.json
src/
index.ht.js # estructura estática + manejadores import() en línea
css/
styles.css
js/
todo.js # manejadores exportados (se cargan bajo demanda)1. Imports en línea en la página
Sin <script type="module" src>. Los manejadores son atributos HTML que importan el módulo dinámicamente y llaman a un export, pasando this (el elemento). Así los módulos de página se mantienen libres de APIs del navegador (ver limitaciones de JSX).
export default () => `
<html lang="es">
<head>
<title>Tareas — 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>Tareas</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="¿Qué hay que hacer?" required>
<button type="submit">Añadir</button>
</form>
<ul id="todo-list"></ul>
<p id="todo-empty" class="empty" hidden>Aquí no hay nada todavía.</p>
<p class="meta"><span id="todo-count">0</span> pendientes</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: 'es' },
head(
title('Tareas — 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('Tareas'),
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: '¿Qué hay que hacer?',
required: '',
}),
button({ type: 'submit' }, 'Añadir'),
),
ul({ id: 'todo-list' }),
p({ id: 'todo-empty', class: 'empty', hidden: '' }, 'Aquí no hay nada todavía.'),
p({ class: 'meta' }, span({ id: 'todo-count' }, '0'), ' pendientes'),
),
),
)export default function Todos() {
return (
<html lang="es">
<head>
<title>Tareas — sitelo</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body
{...{
// React convierte onLoad en un evento sintético; el spread nos deja emitir onload="" tal cual
onload: "import('/js/todo.js').then((m) => m.hydrate())",
}}
>
<main>
<h1>Tareas</h1>
<form
id="todo-form"
autoComplete="off"
{...{
// por lo mismo — onsubmit="" tal cual, no el onSubmit de React
onsubmit:
"event.preventDefault(); import('/js/todo.js').then((m) => m.handleSubmit(this))",
}}
>
<input
id="todo-input"
name="title"
type="text"
placeholder="¿Qué hay que hacer?"
required
/>
<button type="submit">Añadir</button>
</form>
<ul id="todo-list" />
<p id="todo-empty" className="empty" hidden>
Aquí no hay nada todavía.
</p>
<p className="meta">
<span id="todo-count">0</span> pendientes
</p>
</main>
</body>
</html>
)
}2. Manejadores exportados
El módulo es un archivo ES normal dentro de src/js/. Los elementos de lista creados en tiempo de ejecución usan el mismo patrón import('/js/todo.js').then(…) para onchange y 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': `Marcar "${todo.title}" como completada`,
}),
span({ class: 'todo-title' }, todo.title),
),
button(
{
type: 'button',
class: 'todo-remove',
onclick: IMPORT_REMOVE,
'aria-label': `Eliminar "${todo.title}"`,
},
'Eliminar',
),
),
)
.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. Ejecutar
npm install
npm run devO bien npm run build y aloja dist/ en cualquier sitio que sirva archivos estáticos.
Recursos y estilos · Limitaciones de JSX · Sitio básico