App de tarefas

Uma interface interativa clássica sem framework de frontend. O sitelo constrói a estrutura da página; os atributos de evento chamam import('/js/todo.js').then(…), por isso o módulo só carrega quando é preciso. Código completo em examples/todo.

O que obténs

Estrutura do projeto

my-todo/
  sitelo.config.js
  package.json
  src/
    index.ht.js          # estrutura estática + handlers import() em linha
    css/
      styles.css
    js/
      todo.js            # handlers exportados (carregados a pedido)

1. Imports em linha na página

Sem <script type="module" src>. Os handlers são atributos HTML que importam o módulo dinamicamente e chamam um export, passando this (o elemento). Assim os módulos de página ficam livres de APIs do navegador (vê limitações 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: 'pt' },
    head(
      title('Tarefas — 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('Tarefas'),
        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: 'O que há para fazer?',
            required: '',
          }),
          button({ type: 'submit' }, 'Adicionar'),
        ),
        ul({ id: 'todo-list' }),
        p({ id: 'todo-empty', class: 'empty', hidden: '' }, 'Ainda não há nada aqui.'),
        p({ class: 'meta' }, span({ id: 'todo-count' }, '0'), ' por fazer'),
      ),
    ),
  )

2. Handlers exportados

O módulo é um ficheiro ES normal dentro de src/js/. Os itens de lista criados em tempo de execução usam o mesmo padrão import('/js/todo.js').then(…) para onchange e 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 concluída`,
          }),
          span({ class: 'todo-title' }, todo.title),
        ),
        button(
          {
            type: 'button',
            class: 'todo-remove',
            onclick: IMPORT_REMOVE,
            'aria-label': `Remover "${todo.title}"`,
          },
          'Remover',
        ),
      ),
    )
    .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. Executar

npm install
npm run dev

Ou npm run build e aloja dist/ em qualquer sítio que sirva ficheiros estáticos.

Recursos e estilos · Limitações de JSX · Site básico