Modal Dialog

Open a native <dialog> declaratively with the Invoker Commands API (command="show-modal" + commandfor) or a plain #id hash link — no inline JavaScript. A valid submit closes the dialog and shows a success flash; an invalid submit keeps it open with field errors rendered inside, because the form posts through LiveTemplate and re-renders the same region.

Modal Dialog

Native <dialog> opened via the Invoker Commands API (command="show-modal" + commandfor) or directly via a hash link (<a href="#edit-dialog">, client v0.8.30+). Submitting the form with valid input closes the dialog and shows a success flash; invalid input keeps it open with field errors rendered inside (client v0.8.33+).

Profile
Ada Lovelace · ada@analytical.engine
Open via URL hash

Edit profile

Template

The trigger button carries command/commandfor; the Cancel button closes via command="close". BindAndValidate populates AriaInvalid and ErrorTag so a failed save renders errors next to each field without closing the dialog.

{{define "content"}}
<article>
    <h3>Modal Dialog</h3>
    <p><small>Native <code>&lt;dialog&gt;</code> opened via the Invoker Commands API (<code>command="show-modal"</code> + <code>commandfor</code>) or directly via a hash link (<code>&lt;a href="#edit-dialog"&gt;</code>, client v0.8.30+). Submitting the form with valid input closes the dialog and shows a success flash; invalid input keeps it open with field errors rendered inside (client v0.8.33+).</small></p>

    <dl>
        <dt><strong>Profile</strong></dt>
        <dd>{{.Name}} &middot; <small>{{.Email}}</small>{{if .SavedAt}} &middot; <small>Saved at {{.SavedAt}}</small>{{end}}</dd>
    </dl>

    <fieldset role="group">
        <button command="show-modal" commandfor="edit-dialog">Edit profile</button>
        <a href="#edit-dialog" role="button" class="secondary outline">Open via URL hash</a>
    </fieldset>

    {{.lvt.FlashTag "success"}}

    <dialog id="edit-dialog">
        <article>
            <header>
                <h4>Edit profile</h4>
            </header>
            <form method="POST">
                <label>
                    Name
                    <input name="name" value="{{.Name}}" required minlength="3" {{.lvt.AriaInvalid "name"}}>
                    {{.lvt.ErrorTag "name"}}
                </label>
                <label>
                    Email
                    <input type="email" name="email" value="{{.Email}}" required {{.lvt.AriaInvalid "email"}}>
                    {{.lvt.ErrorTag "email"}}
                </label>
                <footer>
                    <fieldset role="group">
                        <button name="save" type="submit">Save</button>
                        <button type="button" command="close" commandfor="edit-dialog" class="secondary">Cancel</button>
                    </fieldset>
                </footer>
            </form>
        </article>
    </dialog>
</article>
{{end}}

modal-dialog.tmpl

Handler & state

One Save action binds and validates the form; on error it returns the validation error and the still-open dialog re-renders with the messages.

// On invalid submit, field errors must render inside the still-open dialog.
type ModalDialogController struct{}

type modalDialogInput struct {
	Name  string `json:"name"  validate:"required,min=3"`
	Email string `json:"email" validate:"required,email"`
}

func (c *ModalDialogController) Save(state ModalDialogState, ctx *livetemplate.Context) (ModalDialogState, error) {
	var in modalDialogInput
	if err := ctx.BindAndValidate(&in, validateNav); err != nil {
		return state, err
	}
	state.Name = in.Name
	state.Email = in.Email
	state.SavedAt = time.Now().Format("15:04:05")
	ctx.SetFlash("success", "Profile saved", livetemplate.FlashExpiry(5*time.Second))
	return state, nil
}

func modalDialogHandler() http.Handler {
	tmpl := newLayoutTmpl("templates/layout.tmpl", "templates/navigation/modal-dialog.tmpl")
	return tmpl.Handle(&ModalDialogController{}, livetemplate.AsState(&ModalDialogState{
		Title:    "Modal Dialog",
		Category: "Dialogs, Tabs & Navigation",
		Name:     "Ada Lovelace",
		Email:    "ada@analytical.engine",
	}))
}

handlers_navigation.go:21-50

type ModalDialogState struct {
	Title    string
	Category string
	Name     string
	Email    string
	SavedAt  string
}

state_navigation.go:4-11

When to use

Reach for Confirm Dialog when the dialog only needs a yes/no confirmation for a destructive action.

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