Each row carries a Delete button whose value is its ID; the handler removes that
item from a process-wide in-memory table and re-renders. New rows slide in via
lvt-fx:animate="slide", and the stable data-key on every <tr> lets the diff
engine drop only the deleted row instead of redrawing the table. Deletions persist
across reloads, with a Restore button to refill the demo.
Delete Row
Click Delete on any row. New rows slide in via lvt-fx:animate="slide" (entry-only). Items live in a process-wide in-memory table, so deletions persist across reloads and navigation; use Restore to reset.
ID
Name
Email
1
Item 1
item1@example.com
2
Item 2
item2@example.com
3
Item 3
item3@example.com
4
Item 4
item4@example.com
5
Item 5
item5@example.com
Template
The data-key keys each row for the diff engine, and lvt-fx:animate="slide" gives
new rows an entry animation. When the list empties, a Restore button replaces it.
{{define "content"}}
<article>
<h3>Delete Row</h3>
<p><small>Click Delete on any row. New rows slide in via <code>lvt-fx:animate="slide"</code> (entry-only). Items live in a process-wide in-memory table, so deletions persist across reloads and navigation; use Restore to reset.</small></p>
{{if gt (len .Items) 0}}
<table>
<thead><tr><th>ID</th><th>Name</th><th>Email</th><th></th></tr></thead>
<tbody>
{{range .Items}}
<tr data-key="{{.ID}}" lvt-fx:animate="slide">
<td>{{.ID}}</td>
<td>{{.Name}}</td>
<td>{{.Email}}</td>
<td>
<form method="POST" class="inline">
<button name="delete" value="{{.ID}}" class="compact contrast outline">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p><small>All items deleted.</small></p>
<form method="POST">
<button name="restore" class="secondary outline">Restore Items</button>
</form>
{{end}}
</article>
{{end}}
Delete reads the clicked button's value, removes that item under a mutex, and
copies a fresh snapshot back into session state; Restore refills the table.
// DeleteRowController holds a shared in-memory "database" protected by a
// mutex. Mount copies the DB snapshot into per-session state on every
// connect, so deletions persist across reloads and cross-handler navigation
// without needing `lvt:"persist"` struct tags. The DB lives for the life
// of the process; restarting the server resets it.
type DeleteRowController struct {
mu sync.Mutex
items []Item
}
const deleteRowInitialCount = 5
func newDeleteRowController() *DeleteRowController {
return &DeleteRowController{items: getItemPage(1, deleteRowInitialCount)}
}
// snapshot returns an independent copy of the current DB. Caller must not
// hold c.mu when invoking (this method acquires it internally).
func (c *DeleteRowController) snapshot() []Item {
c.mu.Lock()
defer c.mu.Unlock()
return slices.Clone(c.items)
}
func (c *DeleteRowController) Mount(state DeleteRowState, ctx *livetemplate.Context) (DeleteRowState, error) {
state.Items = c.snapshot()
return state, nil
}
func (c *DeleteRowController) Delete(state DeleteRowState, ctx *livetemplate.Context) (DeleteRowState, error) {
// Button sends its `value` attribute as data.value — see
// docs/references/progressive-complexity-reference.md.
id := ctx.GetString("value")
c.mu.Lock()
c.items = slices.DeleteFunc(c.items, func(item Item) bool {
return item.ID == id
})
c.mu.Unlock()
state.Items = c.snapshot()
return state, nil
}
// Restore refills the DB to its initial state. Wired to a button that
// appears after the last item is deleted, so visitors can reset the demo
// without restarting the server.
func (c *DeleteRowController) Restore(state DeleteRowState, ctx *livetemplate.Context) (DeleteRowState, error) {
c.mu.Lock()
c.items = getItemPage(1, deleteRowInitialCount)
c.mu.Unlock()
state.Items = c.snapshot()
return state, nil
}
func deleteRowHandler() http.Handler {
tmpl := newLayoutTmpl("templates/layout.tmpl", "templates/lists/delete-row.tmpl")
return tmpl.Handle(newDeleteRowController(), livetemplate.AsState(&DeleteRowState{
Title: "Delete Row",
Category: "Lists & Data",
}))
}