Recipe: Todo list
The “hello world” of Civility apps — a single Collection is the whole data model. Assume the singleton app store, light-DOM routes, and a worker.ts from the README around this.
Create with set(id, value) (there’s no add), toggle with update, remove with delete.
// www/store/app.ts
import { Store } from '@civility/store'
import { IDBStorage } from '@civility/store/idb'
type Todo = { text: string; done: boolean; createdAt: string }
const store = new Store(new IDBStorage({ dbName: 'me.example.todo' }), {
collections: ['todos'],
versions: [{
version: globalThis.__CIVILITY__?.version ?? '0.0.0',
schema: { type: 'object', properties: { todos: { type: 'object' } } },
}],
})
export class App {
todos = store.collection<Todo>('todos')
#subs = new Set<() => void>()
constructor() {
store.subscribe(() => this.#notify())
this.#init()
}
async #init() {
await this.todos.preload()
this.#notify()
}
#notify() {
for (const cb of this.#subs) cb()
}
addEventListener(fn: () => void) {
this.#subs.add(fn)
}
removeEventListener(fn: () => void) {
this.#subs.delete(fn)
}
add(text: string) {
return this.todos.set(crypto.randomUUID(), {
text,
done: false,
createdAt: new Date().toISOString(),
})
}
toggle(id: string, done: boolean) {
return this.todos.update(id, { done })
}
remove(id: string) {
return this.todos.delete(id)
}
}
export default new App()
// www/routes/home.ts
import { html, LitElement } from 'lit'
import app from '../store/app.ts'
export class HomePage extends LitElement {
protected override createRenderRoot() {
return this
}
#onUpdate = () => this.requestUpdate()
override connectedCallback() {
super.connectedCallback()
app.addEventListener(this.#onUpdate)
}
override disconnectedCallback() {
app.removeEventListener(this.#onUpdate)
super.disconnectedCallback()
}
#submit(e: SubmitEvent) {
e.preventDefault()
const input = (e.target as HTMLFormElement).elements.namedItem(
'text',
) as HTMLInputElement
if (input.value.trim()) {
app.add(input.value.trim())
input.value = ''
}
}
override render() {
const todos = [...(app.todos.value ?? [])] // Map<string, Todo> → [id, todo][]
return html`
<form
@submit=${this
.#submit}><input name="text" placeholder="New todo" autofocus></form>
<ul>${todos.map(([id, t]) =>
html`
<li>
<input type="checkbox" .checked=${t.done} @change=${(e: Event) =>
app.toggle(id, (e.target as HTMLInputElement).checked)}>
<span style=${t.done ? 'text-decoration:line-through' : ''}>${t
.text}</span>
<button @click=${() => app.remove(id)}>×</button>
</li>
`
)}</ul>
`
}
}
customElements.define('home-page', HomePage)
Add sync and two devices share the list; because each toggle is a CRDT change, editing different todos offline on each device merges cleanly.
Next: Journal adds a settings document alongside the collection.