Gate a destructive action behind a per-row native <dialog> confirmation with no
inline JavaScript, so it works under a strict Content-Security-Policy. Each row owns
its own dialog id (confirm-{{.ID}}), opened with command="show-modal"; confirming
posts the clicked button's value (the item id) to a Delete action that removes the
item and re-renders the table.
Confirm Dialog
Per-item confirmation gated by a native <dialog> with no inline JS. Each row owns its own dialog id (confirm-{{.ID}}) so URL hashes can deep-link to a specific row's confirmation.
Name
Email
Item 1
item1@example.com
Item 2
item2@example.com
Item 3
item3@example.com
Item 4
item4@example.com
Item 5
item5@example.com
Template
Dialogs render outside the <table> (a <dialog> inside <tbody> is invalid HTML).
The Delete button carries value="{{.ID}}" so the server learns which row to remove
without a hidden input.
{{define "content"}}
<article>
<h3>Confirm Dialog</h3>
<p><small>Per-item confirmation gated by a native <code><dialog></code> with no inline JS. Each row owns its own dialog id (<code>confirm-{{`{{.ID}}`}}</code>) so URL hashes can deep-link to a specific row's confirmation.</small></p>
{{if .Items}}
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Items}}
<tr data-key="{{.ID}}">
<td>{{.Name}}</td>
<td><small>{{.Email}}</small></td>
<td>
<button command="show-modal" commandfor="confirm-{{.ID}}" class="compact secondary outline">Delete</button>
</td>
</tr>
{{end}}
</tbody>
</table>
{{/* Dialogs render outside the table — <dialog> inside <tbody> is invalid HTML. */}}
{{range .Items}}
<dialog id="confirm-{{.ID}}">
<article>
<header>
<h4>Delete “{{.Name}}”?</h4>
</header>
<p>This cannot be undone.</p>
<form method="POST" name="delete">
<footer>
<fieldset role="group">
<button type="button" command="close" commandfor="confirm-{{.ID}}" class="secondary">Cancel</button>
<button name="delete" value="{{.ID}}" class="contrast">Delete</button>
</fieldset>
</footer>
</form>
</article>
</dialog>
{{end}}
{{else}}
<p><small>All items deleted. Reload the page or navigate away and back to reseed the list.</small></p>
{{end}}
</article>
{{end}}
Delete reads the clicked button's value and drops that item; unknown ids are a
tolerated no-op so client and server lists reconcile silently.
// The Delete action reads the item id from the submit button's value attribute
// (the canonical Tier-1 row-action shape), not a hidden input.
type ConfirmDialogController struct{}
const confirmDialogItemCount = 5
func (c *ConfirmDialogController) Mount(state ConfirmDialogState, ctx *livetemplate.Context) (ConfirmDialogState, error) {
if len(state.Items) == 0 && ctx.Action() == "" {
state.Items = getItemPage(1, confirmDialogItemCount)
}
return state, nil
}
func (c *ConfirmDialogController) Delete(state ConfirmDialogState, ctx *livetemplate.Context) (ConfirmDialogState, error) {
// "value" is the framework key for the clicked submit button's value
// attribute (independent of the button's name). Same idiom as dialog-patterns.
// id is a server-rendered Item.ID echoed back via the form, NOT free-form
// user input — no escaping/allowlist check is needed.
id := ctx.GetString("value")
// Unknown ids are tolerated as a no-op — the next render reconciles any
// drift between client and server item lists without surfacing a flash.
state.Items = slices.DeleteFunc(state.Items, func(it Item) bool { return it.ID == id })
return state, nil
}
func confirmDialogHandler() http.Handler {
tmpl := newLayoutTmpl("templates/layout.tmpl", "templates/navigation/confirm-dialog.tmpl")
return tmpl.Handle(&ConfirmDialogController{}, livetemplate.AsState(&ConfirmDialogState{
Title: "Confirm Dialog",
Category: "Dialogs, Tabs & Navigation",
}))
}