Aplikasi todo
Di halaman ini
Antarmuka interaktif klasik tanpa framework frontend. sitelo membangun kerangka halamannya; atribut peristiwa memanggil import('/js/todo.js').then(…) sehingga modulnya dimuat hanya ketika dibutuhkan. Sumber lengkap di examples/todo.
Yang Anda dapat
- HTML statis dengan penangan
onsubmit/onload(dan item daftar) src/js/todo.js—hydrate,handleSubmit,handleChange,handleRemoveyang diekspor- sitelo menemukan
import('/…')harfiah di dalam HTML dan membundel berkasnya kedist/(lihat Aset)
Tata letak proyek
my-todo/
sitelo.config.js
package.json
src/
index.ht.js # cangkang statis + handler import() sebaris
css/
styles.css
js/
todo.js # handler yang diekspor (dimuat saat dibutuhkan)1. Impor sebaris di halaman
Tanpa <script type="module" src>. Penangannya adalah atribut HTML yang mengimpor modulnya secara dinamis lalu memanggil sebuah ekspor, sambil meneruskan this (elemennya). Itu menjaga modul halaman tetap bebas dari API peramban (lihat Batasan JSX).
export default () => `
<html lang="id">
<head>
<title>Daftar tugas — 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>Daftar tugas</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="Apa yang perlu dikerjakan?" required>
<button type="submit">Tambah</button>
</form>
<ul id="todo-list"></ul>
<p id="todo-empty" class="empty" hidden>Belum ada apa-apa di sini.</p>
<p class="meta"><span id="todo-count">0</span> tersisa</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: 'id' },
head(
title('Daftar tugas — 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('Daftar tugas'),
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: 'Apa yang perlu dikerjakan?',
required: '',
}),
button({ type: 'submit' }, 'Tambah'),
),
ul({ id: 'todo-list' }),
p({ id: 'todo-empty', class: 'empty', hidden: '' }, 'Belum ada apa-apa di sini.'),
p({ class: 'meta' }, span({ id: 'todo-count' }, '0'), ' tersisa'),
),
),
)export default function Todos() {
return (
<html lang="id">
<head>
<title>Daftar tugas — sitelo</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body
{...{
// React memetakan onLoad → event sintetis; spread membuat kita bisa menulis onload="" mentah
onload: "import('/js/todo.js').then((m) => m.hydrate())",
}}
>
<main>
<h1>Daftar tugas</h1>
<form
id="todo-form"
autoComplete="off"
{...{
// alasan sama — onsubmit="" mentah, bukan onSubmit milik React
onsubmit:
"event.preventDefault(); import('/js/todo.js').then((m) => m.handleSubmit(this))",
}}
>
<input
id="todo-input"
name="title"
type="text"
placeholder="Apa yang perlu dikerjakan?"
required
/>
<button type="submit">Tambah</button>
</form>
<ul id="todo-list" />
<p id="todo-empty" className="empty" hidden>
Belum ada apa-apa di sini.
</p>
<p className="meta">
<span id="todo-count">0</span> tersisa
</p>
</main>
</body>
</html>
)
}2. Penangan yang diekspor
Modulnya adalah berkas ES biasa di bawah src/js/. Item daftar yang dibuat saat jalan memakai pola import('/js/todo.js').then(…) yang sama untuk onchange / 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': `Tandai "${todo.title}" selesai`,
}),
span({ class: 'todo-title' }, todo.title),
),
button(
{
type: 'button',
class: 'todo-remove',
onclick: IMPORT_REMOVE,
'aria-label': `Hapus "${todo.title}"`,
},
'Hapus',
),
),
)
.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. Jalankan
npm install
npm run devAtau npm run build lalu hosting dist/ di mana pun berkas statis disajikan.
Aset dan gaya · Batasan JSX · Situs dasar