Confirm Dialog

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

Delete “Item 1”?

This cannot be undone.

Delete “Item 2”?

This cannot be undone.

Delete “Item 3”?

This cannot be undone.

Delete “Item 4”?

This cannot be undone.

Delete “Item 5”?

This cannot be undone.

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>&lt;dialog&gt;</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 &ldquo;{{.Name}}&rdquo;?</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}}

confirm-dialog.tmpl

Handler & state

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",
	}))
}

handlers_navigation.go:56-88

type ConfirmDialogState struct {
	Title    string
	Category string
	Items    []Item
}

state_navigation.go:15-20

When to use

Reach for Modal Dialog when the overlay holds a full form rather than a single confirm/cancel choice.

source: livetemplate/docs · path: examples/patterns/templates/navigation/confirm-dialog.tmpl