Appli de tâches

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

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).

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'),
      ),
    ),
  )

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 dev

Ou bien npm run build, puis hébergez dist/ partout où des fichiers statiques sont servis.

Ressources et styles · Limites de JSX · Site de base