A keyed list
Add and remove todos. Entries are keyed, so existing nodes are moved rather than rebuilt. The compiled-markup version of this is Keyed list.
0 remaining
How it works
State
type Item struct {
ID string
Text string
}
items := reactive.NewSignal([]Item{
{ID: "1", Text: "Read the docs"},
{ID: "2", Text: "Build something"},
})
draft := reactive.NewSignal("")The model is a slice of items; the DOM is a projection of it. draft holds the input while it's being typed.
The keyed list
reactive.BindList(list, items.Get,
func(it Item) string { return it.ID },
func(it Item) wasmwrap.Element {
row := wasmwrap.Create("li")
remove := wasmwrap.Create("button").SetText("remove")
reactive.On(remove, "click", func(wasmwrap.Event) {
items.Update(func(all []Item) []Item {
return slices.DeleteFunc(all, func(o Item) bool {
return o.ID == it.ID
})
})
})
return row.Append(wasmwrap.Create("span").SetText(it.Text), remove)
})BindList takes the parent, a getter, a key function and a render function. The key is what makes it patch instead of re-render: add a row and only a new <li> is created; remove one and only that node is detached. The render function runs once per key, so wiring the remove handler inside it is safe — it closes over its own row.
Adding a todo
reactive.On(form, "submit", func(e wasmwrap.Event) {
e.PreventDefault()
text := strings.TrimSpace(draft.Get())
if text == "" {
return
}
items.Update(func(all []Item) []Item {
return append(all, Item{ID: fmt.Sprintf("%d", len(all)+1), Text: text})
})
draft.Set("")
})The field is two-way bound to draft. Submit — Enter works — trims, ignores empty text, appends a new item and clears the field. items.Update replaces the slice, and BindList diffs the keys and patches the DOM to match.
The count
reactive.BindText(remaining, func() string {
return strconv.Itoa(len(items.Get()))
})The remaining count is a text binding over the same slice — reading items.Get() re-runs it on every change.
Full source
type Item struct {
ID string
Text string
}
items := reactive.NewSignal([]Item{
{ID: "1", Text: "Read the docs"},
{ID: "2", Text: "Build something"},
})
draft := reactive.NewSignal("")
reactive.Hydrate("#todo", func(root wasmwrap.Element) {
field := root.Find("[data-new]")
reactive.BindValue(field, draft)
reactive.On(root.Find("[data-form]"), "submit", func(e wasmwrap.Event) {
e.PreventDefault()
text := strings.TrimSpace(draft.Get())
if text == "" {
return
}
items.Update(func(all []Item) []Item {
return append(all, Item{ID: fmt.Sprintf("%d", len(all)+1), Text: text})
})
draft.Set("")
})
reactive.BindText(root.Find("[data-remaining]"), func() string {
return strconv.Itoa(len(items.Get()))
})
reactive.BindList(root.Find("[data-list]"), items.Get,
func(it Item) string { return it.ID },
func(it Item) wasmwrap.Element {
row := wasmwrap.Create("li")
remove := wasmwrap.Create("button").SetText("remove")
reactive.On(remove, "click", func(wasmwrap.Event) {
items.Update(func(all []Item) []Item {
return slices.DeleteFunc(all, func(o Item) bool {
return o.ID == it.ID
})
})
})
return row.Append(wasmwrap.Create("span").SetText(it.Text), remove)
})
})How this demo runs
scripts/demo.go is compiled to demo.wasm by CompileFrom and loaded by WasmScript — see Scripts & WebAssembly. Without WebAssembly the page still renders; only the rows stay put.