待办应用

不用前端框架的经典交互界面。sitelo 负责构建页面外壳;事件属性调用 import('/js/todo.js').then(…),因此模块只在需要时才加载。完整源码见 examples/todo

你会得到什么

项目结构

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 的限制)。

import {
  html, head, title, meta, link, body, main, h1, form, input, button, ul, p, span,
} from 'javascript-to-html'

export default () =>
  html({ lang: 'zh' },
    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'), ' 项未完成'),
      ),
    ),
  )

2. 导出的处理函数

这个模块就是 src/js/ 下一个普通的 ES 文件。运行时创建的列表项,对 onchangeonclick 使用同样的 import('/js/todo.js').then(…) 写法。

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 的限制 · 基础站点