Appli de tâches
Sur cette page
Une interface interactive classique, sans framework frontend. sitelo construit la coquille de la page ; les attributs d’événement appellent import('/js/todo.js').then(…), si bien que le module ne se charge qu’au besoin. Source complète dans examples/todo.
Ce que vous obtenez
- Du HTML statique avec des gestionnaires
onsubmit/onload(et sur les éléments de la liste) src/js/todo.js— exportehydrate,handleSubmit,handleChangeethandleRemove- sitelo repère les
import('/…')littéraux dans le HTML et intègre le fichier dansdist/(voir Ressources)
Structure du projet
my-todo/
sitelo.config.js
package.json
src/
index.ht.js # coquille statique + gestionnaires import() en ligne
css/
styles.css
js/
todo.js # gestionnaires exportés (chargés à la demande)1. Imports en ligne dans la page
Pas de <script type="module" src>. Les gestionnaires sont des attributs HTML qui importent dynamiquement le module et appellent un export, en passant this (l’élément). Les modules de page restent ainsi exempts d’API navigateur (voir limites de JSX).
export default () => `
<html lang="fr">
<head>
<title>Tâches — 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>Tâches</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’y a-t-il à faire ?" required>
<button type="submit">Ajouter</button>
</form>
<ul id="todo-list"></ul>
<p id="todo-empty" class="empty" hidden>Rien ici pour l’instant.</p>
<p class="meta"><span id="todo-count">0</span> restantes</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: 'fr' },
head(
title('Tâches — 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('Tâches'),
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’y a-t-il à faire ?',
required: '',
}),
button({ type: 'submit' }, 'Ajouter'),
),
ul({ id: 'todo-list' }),
p({ id: 'todo-empty', class: 'empty', hidden: '' }, 'Rien ici pour l’instant.'),
p({ class: 'meta' }, span({ id: 'todo-count' }, '0'), ' restantes'),
),
),
)export default function Todos() {
return (
<html lang="fr">
<head>
<title>Tâches — sitelo</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body
{...{
// React transforme onLoad en événement synthétique ; le spread permet de produire un onload="" brut
onload: "import('/js/todo.js').then((m) => m.hydrate())",
}}
>
<main>
<h1>Tâches</h1>
<form
id="todo-form"
autoComplete="off"
{...{
// même raison — onsubmit="" brut, pas le 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’y a-t-il à faire ?"
required
/>
<button type="submit">Ajouter</button>
</form>
<ul id="todo-list" />
<p id="todo-empty" className="empty" hidden>
Rien ici pour l’instant.
</p>
<p className="meta">
<span id="todo-count">0</span> restantes
</p>
</main>
</body>
</html>
)
}2. Gestionnaires exportés
Le module est un fichier ES ordinaire sous src/js/. Les éléments de liste créés à l’exécution utilisent le même motif import('/js/todo.js').then(…) pour onchange et 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': `Marquer "${todo.title}" comme terminée`,
}),
span({ class: 'todo-title' }, todo.title),
),
button(
{
type: 'button',
class: 'todo-remove',
onclick: IMPORT_REMOVE,
'aria-label': `Supprimer "${todo.title}"`,
},
'Supprimer',
),
),
)
.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. Lancer
npm install
npm run devOu bien npm run build, puis hébergez dist/ partout où des fichiers statiques sont servis.
Ressources et styles · Limites de JSX · Site de base