Components
Edit In Place
A read-only record with an Edit affordance that swaps in a pre-filled form. Save issues a PUT; Cancel re-fetches the view. The canonical htmx editable primitive — built entirely on outerHTML swaps over REST, no modal, no custom JS.
Installation
1. Install via the shadcn CLI
npx shadcn@latest add http://localhost/r/edit-in-place.json2. Use it
import { EditInPlace, EditInPlaceForm } from "@/components/ui/edit-in-place"
// View — GET /users/1 returns this. Edit fetches the form below.
<EditInPlace id="user" editHref="/users/1/edit" fields={[
{ label: "Name", value: user.name },
{ label: "Email", value: user.email, type: "email" },
]} />
// Editor — GET /users/1/edit returns this. Save PUTs; Cancel re-GETs the view.
<EditInPlaceForm id="user" putHref="/users/1" cancelHref="/users/1" fields={[
{ label: "Name", value: user.name },
{ label: "Email", value: user.email, type: "email" },
]} />Or copy the source manually
/** @jsxImportSource hono/jsx */
import type { Child, PropsWithChildren } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"
import { buttonClasses } from "@/registry/ui/button"
import { inputClasses } from "@/registry/ui/input"
import { labelClasses } from "@/registry/ui/label"
// Edit In Place — shadcn-htmx, htmx v4 + Tailwind v4.
//
// The canonical htmx editable record: a read-only view with an "Edit"
// affordance that swaps in a pre-filled form. Save = PUT; Cancel re-GETs
// the view. No modal, no custom JS — the whole thing rides on outerHTML
// swaps over REST.
//
// Built on:
// repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md
// (GET /users/1 → view, GET /users/1/edit → form, PUT /users/1 → view)
// repos/htmx/www/src/content/reference/01-attributes/08-hx-target.md:12-17
// hx-target="this" targets the element making the request.
// repos/htmx/www/src/content/reference/01-attributes/07-hx-swap.md:35-41
// hx-swap="outerHTML" replaces the whole element with the response.
// repos/htmx/www/src/content/reference/01-attributes/03-hx-put.md
// hx-put issues the REST PUT on Save.
//
// Native semantics:
// - The view is a <dl> description list — the right element for
// name/value record pairs.
// repos/mdn/files/en-us/web/html/reference/elements/dl/index.md
// - The edit affordance is a real <button> and the editor is a real
// <form> with native <input>s, so constraint validation, Enter-to-
// submit, and focus all come from the platform.
// repos/mdn/files/en-us/web/html/reference/elements/form/index.md
//
// Class strings mirror Card (rounded bordered container) + Input + Button,
// so the view and the editor occupy the same visual footprint and the swap
// looks like an in-place toggle rather than a layout jump.
//
// Style analogues: registry/ui/card.tsx, registry/ui/input.tsx,
// registry/ui/button.tsx, registry/ui/label.tsx.
// Shared container so the read-only view and the editor render at the same
// size — the outerHTML swap then reads as a true in-place toggle.
const container =
"flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm"
// One field's label, styled as the small uppercase eyebrow shadcn uses for
// record metadata.
const fieldTermClass =
"text-xs font-medium tracking-wide text-muted-foreground uppercase"
const fieldValueClass = "mt-0.5 text-sm font-medium text-foreground"
export type EditInPlaceField = {
// Visible label for the field, e.g. "Email".
label: string
// The current, read-only value shown in the view.
value: Child
// Form field name used by the editor's <input name>. Defaults to a
// lowercased label, but pass it explicitly for anything non-trivial.
name?: string
// Input type for the editor (text, email, url, tel, …). Default "text".
type?: string
// Raw value passed to the editor's <input value>. Falls back to `value`
// when that is a plain string.
inputValue?: string
required?: boolean
}
// ── View ────────────────────────────────────────────────────────────────
// The read-only record. Carries hx-target="this" + hx-swap="outerHTML" so
// any descendant request (the Edit button, and later the editor's Save /
// Cancel) replaces this whole element. `editHref` is the GET that returns
// the editor fragment.
type EditInPlaceProps = PropsWithChildren<{
// GET endpoint that returns the editor form fragment.
editHref: string
// Fields rendered as a <dl>. Omit when you pass custom children instead.
fields?: EditInPlaceField[]
// Label on the Edit button. Default "Edit".
editLabel?: string
id?: string
class?: ClassValue
}> &
Record<string, any>
export function EditInPlace(props: EditInPlaceProps) {
const {
children,
editHref,
fields,
editLabel = "Edit",
class: className,
id,
...rest
} = props
return (
<div
data-slot="edit-in-place"
data-mode="view"
id={id}
hx-target="this"
hx-swap="outerHTML"
class={cn(container, className)}
{...rest}
>
{fields ? (
<dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
{fields.map((f) => (
<div>
<dt class={fieldTermClass}>{f.label}</dt>
<dd class={fieldValueClass}>{f.value}</dd>
</div>
))}
</dl>
) : (
children
)}
<div class="flex">
<button
type="button"
data-slot="edit-in-place-edit"
hx-get={editHref}
hx-target="closest [data-slot='edit-in-place']"
hx-swap="outerHTML"
class={buttonClasses({ variant: "outline", size: "sm" })}
>
{editLabel}
</button>
</div>
</div>
)
}
// ── Editor ──────────────────────────────────────────────────────────────
// The pre-filled form returned by `editHref`. Submitting issues the PUT
// (Save); Cancel re-GETs the view. Both target `this` form and swap
// outerHTML, restoring the view in place.
type EditInPlaceFormProps = PropsWithChildren<{
// PUT endpoint hit on Save.
putHref: string
// GET endpoint that restores the read-only view on Cancel.
cancelHref: string
// Fields to pre-fill. Omit when you pass custom children instead.
fields?: EditInPlaceField[]
saveLabel?: string
cancelLabel?: string
id?: string
class?: ClassValue
}> &
Record<string, any>
export function EditInPlaceForm(props: EditInPlaceFormProps) {
const {
children,
putHref,
cancelHref,
fields,
saveLabel = "Save",
cancelLabel = "Cancel",
class: className,
id,
...rest
} = props
return (
<form
data-slot="edit-in-place"
data-mode="edit"
id={id}
hx-put={putHref}
hx-target="this"
hx-swap="outerHTML"
class={cn(container, className)}
{...rest}
>
{fields ? (
<div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
{fields.map((f) => {
const name = f.name ?? f.label.toLowerCase()
const fieldId = `${id ?? "field"}-${name}`
const value =
f.inputValue ?? (typeof f.value === "string" ? f.value : undefined)
return (
<div class="grid gap-1.5">
<label for={fieldId} class={labelClasses({ class: fieldTermClass })}>
{f.label}
</label>
<input
id={fieldId}
name={name}
type={f.type ?? "text"}
value={value}
required={f.required}
data-slot="input"
class={inputClasses()}
/>
</div>
)
})}
</div>
) : (
children
)}
<div class="flex gap-2">
<button
type="submit"
data-slot="edit-in-place-save"
class={buttonClasses({ size: "sm" })}
>
{saveLabel}
</button>
<button
type="button"
data-slot="edit-in-place-cancel"
hx-get={cancelHref}
hx-target="closest [data-slot='edit-in-place']"
hx-swap="outerHTML"
class={buttonClasses({ variant: "secondary", size: "sm" })}
>
{cancelLabel}
</button>
</div>
</form>
)
}
1. Save the file
Copy edit-in-place.html into templates/components/.
2. Use it
{% from "components/edit-in-place.html" import edit_in_place, edit_in_place_form %}
{# View #}
{{ edit_in_place(edit_href="/users/1/edit", id="user", fields=[
{"label": "Name", "value": user.name},
{"label": "Email", "value": user.email, "type": "email"},
]) }}
{# Editor #}
{{ edit_in_place_form(put_href="/users/1", cancel_href="/users/1", id="user", fields=[
{"label": "Name", "value": user.name},
{"label": "Email", "value": user.email, "type": "email"},
]) }}View source
{# Edit In Place macros — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/edit-in-place.tsx for Python/Flask/FastAPI/Django/Jinja2.
The canonical htmx editable record: a read-only view with an Edit button
that swaps in a pre-filled form. Save = PUT, Cancel re-GETs the view.
No modal, no custom JS — the whole thing rides on outerHTML swaps over REST.
See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.
Usage:
{% from "components/edit-in-place.html" import edit_in_place, edit_in_place_form %}
{# View — GET /users/1 returns this #}
{{ edit_in_place(edit_href="/users/1/edit", id="user", fields=[
{"label": "Name", "value": user.name},
{"label": "Email", "value": user.email, "type": "email"},
]) }}
{# Editor — GET /users/1/edit returns this #}
{{ edit_in_place_form(put_href="/users/1", cancel_href="/users/1", id="user", fields=[
{"label": "Name", "value": user.name},
{"label": "Email", "value": user.email, "type": "email"},
]) }}
hx-target="this" + hx-swap="outerHTML" on each root mean every descendant
request replaces the whole element in place. #}
{%- set _container = "flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm" -%}
{%- set _term = "text-xs font-medium tracking-wide text-muted-foreground uppercase" -%}
{%- set _value = "mt-0.5 text-sm font-medium text-foreground" -%}
{%- set _edit_btn = "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -%}
{%- set _save_btn = "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -%}
{%- set _cancel_btn = "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -%}
{%- set _input = "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70" -%}
{%- set _label = "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase" -%}
{# View — read-only record + Edit button. #}
{% macro edit_in_place(edit_href, fields=none, edit_label="Edit", id=none, extra_class="", caller=none) %}
<div data-slot="edit-in-place" data-mode="view"
{%- if id %} id="{{ id }}"{% endif %}
hx-target="this" hx-swap="outerHTML"
class="{{ _container }} {{ extra_class }}">
{%- if fields %}
<dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
{%- for f in fields %}
<div>
<dt class="{{ _term }}">{{ f.label }}</dt>
<dd class="{{ _value }}">{{ f.value }}</dd>
</div>
{%- endfor %}
</dl>
{%- elif caller %}{{ caller() }}{% endif %}
<div class="flex">
<button type="button" data-slot="edit-in-place-edit" hx-get="{{ edit_href }}" hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML" class="{{ _edit_btn }}">{{ edit_label }}</button>
</div>
</div>
{% endmacro %}
{# Editor — pre-filled form. Save submits the PUT; Cancel re-GETs the view. #}
{% macro edit_in_place_form(put_href, cancel_href, fields=none, save_label="Save", cancel_label="Cancel", id=none, extra_class="", caller=none) %}
<form data-slot="edit-in-place" data-mode="edit"
{%- if id %} id="{{ id }}"{% endif %}
hx-put="{{ put_href }}" hx-target="this" hx-swap="outerHTML"
class="{{ _container }} {{ extra_class }}">
{%- if fields %}
<div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
{%- for f in fields %}
{%- set name = f.name if f.name else f.label|lower %}
{%- set field_id = (id if id else "field") ~ "-" ~ name %}
<div class="grid gap-1.5">
<label for="{{ field_id }}" class="{{ _label }}">{{ f.label }}</label>
<input id="{{ field_id }}" name="{{ name }}" type="{{ f.type if f.type else 'text' }}"
{%- if f.value is not none %} value="{{ f.value }}"{% endif %}
{%- if f.required %} required{% endif %}
data-slot="input" class="{{ _input }}">
</div>
{%- endfor %}
</div>
{%- elif caller %}{{ caller() }}{% endif %}
<div class="flex gap-2">
<button type="submit" data-slot="edit-in-place-save" class="{{ _save_btn }}">{{ save_label }}</button>
<button type="button" data-slot="edit-in-place-cancel" hx-get="{{ cancel_href }}" hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML" class="{{ _cancel_btn }}">{{ cancel_label }}</button>
</div>
</form>
{% endmacro %}
1. Save the file
Add edit-in-place.tmpl alongside your templates.
2. Use it
{{/* View */}}
{{template "edit_in_place" (dict "ID" "user" "EditHref" "/users/1/edit"
"Fields" $fields)}}
{{/* Editor */}}
{{template "edit_in_place_form" (dict "ID" "user"
"PutHref" "/users/1" "CancelHref" "/users/1" "Fields" $fields)}}View source
{{/*
Edit In Place templates — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/edit-in-place.tsx for Go projects using html/template.
The canonical htmx editable record: a read-only view with an Edit button
that swaps in a pre-filled form. Save = PUT, Cancel re-GETs the view. No
modal, no custom JS — the whole thing rides on outerHTML swaps over REST.
See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.
Two templates:
- "edit_in_place" — the read-only view (GET /users/1 returns this).
- "edit_in_place_form" — the pre-filled editor (GET /users/1/edit).
Field struct shared by both:
type EipField struct {
Label string // "Email"
Value string // current value (shown in view, prefilled in editor)
Name string // form field name (required in the editor)
Type string // input type (default "text")
Required bool
}
View args:
type EipArgs struct {
ID string
EditHref string // GET endpoint returning the editor
EditLabel string // default "Edit"
Fields []EipField
}
tpl.ExecuteTemplate(w, "edit_in_place", EipArgs{
ID: "user", EditHref: "/users/1/edit",
Fields: []EipField{
{Label: "Name", Value: "Joe Smith"},
{Label: "Email", Value: "[email protected]", Type: "email"},
},
})
Editor args:
type EipFormArgs struct {
ID string
PutHref string // PUT endpoint hit on Save
CancelHref string // GET endpoint restoring the view on Cancel
SaveLabel string // default "Save"
CancelLabel string // default "Cancel"
Fields []EipField
}
hx-target="this" + hx-swap="outerHTML" on each root mean every descendant
request replaces the whole element in place.
*/}}
{{define "edit_in_place"}}
{{- $container := "flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm" -}}
{{- $term := "text-xs font-medium tracking-wide text-muted-foreground uppercase" -}}
{{- $value := "mt-0.5 text-sm font-medium text-foreground" -}}
{{- $editBtn := "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -}}
{{- $editLabel := or .EditLabel "Edit" -}}
<div data-slot="edit-in-place" data-mode="view"
{{- if .ID}} id="{{.ID}}"{{end}}
hx-target="this" hx-swap="outerHTML"
class="{{$container}}">
<dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
{{- range .Fields}}
<div>
<dt class="{{$term}}">{{.Label}}</dt>
<dd class="{{$value}}">{{.Value}}</dd>
</div>
{{- end}}
</dl>
<div class="flex">
<button type="button" data-slot="edit-in-place-edit" hx-get="{{.EditHref}}" hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML" class="{{$editBtn}}">{{$editLabel}}</button>
</div>
</div>
{{end}}
{{define "edit_in_place_form"}}
{{- $container := "flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm" -}}
{{- $label := "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase" -}}
{{- $input := "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70" -}}
{{- $saveBtn := "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -}}
{{- $cancelBtn := "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -}}
{{- $saveLabel := or .SaveLabel "Save" -}}
{{- $cancelLabel := or .CancelLabel "Cancel" -}}
{{- $id := or .ID "field" -}}
<form data-slot="edit-in-place" data-mode="edit"
{{- if .ID}} id="{{.ID}}"{{end}}
hx-put="{{.PutHref}}" hx-target="this" hx-swap="outerHTML"
class="{{$container}}">
<div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
{{- range .Fields}}
{{- $name := .Name}}
{{- $fieldId := printf "%s-%s" $id $name}}
<div class="grid gap-1.5">
<label for="{{$fieldId}}" class="{{$label}}">{{.Label}}</label>
<input id="{{$fieldId}}" name="{{$name}}" type="{{or .Type "text"}}"
value="{{.Value}}"
{{- if .Required}} required{{end}}
data-slot="input" class="{{$input}}">
</div>
{{- end}}
</div>
<div class="flex gap-2">
<button type="submit" data-slot="edit-in-place-save" class="{{$saveBtn}}">{{$saveLabel}}</button>
<button type="button" data-slot="edit-in-place-cancel" hx-get="{{.CancelHref}}" hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML" class="{{$cancelBtn}}">{{$cancelLabel}}</button>
</div>
</form>
{{end}}
1. Save the file
Drop edit_in_place.ex into lib/my_app_web/components/.
2. Use it
<%# View %>
<.edit_in_place id="user" edit_href={~p"/users/1/edit"} fields={[
%{label: "Name", value: @user.name},
%{label: "Email", value: @user.email, type: "email"}
]} />
<%# Editor %>
<.edit_in_place_form id="user" put_href={~p"/users/1"} cancel_href={~p"/users/1"} fields={[
%{label: "Name", value: @user.name, name: "name"},
%{label: "Email", value: @user.email, name: "email", type: "email"}
]} />View source
defmodule ShadcnHtmx.Components.EditInPlace do
@moduledoc """
Edit In Place — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.
Mirrors registry/ui/edit-in-place.tsx. The canonical htmx editable record:
a read-only view with an Edit button that swaps in a pre-filled form.
Save = PUT, Cancel re-GETs the view. No modal, no custom JS — the whole
thing rides on outerHTML swaps over REST.
See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.
Two function components:
- `edit_in_place/1` — the read-only view (GET /users/1 returns this).
- `edit_in_place_form/1` — the pre-filled editor (GET /users/1/edit).
Each `:fields` entry is a map:
%{label: "Email", value: "[email protected]", name: "email", type: "email", required: true}
## Examples
<%# View %>
<.edit_in_place id="user" edit_href="/users/1/edit" fields={[
%{label: "Name", value: @user.name},
%{label: "Email", value: @user.email, type: "email"}
]} />
<%# Editor %>
<.edit_in_place_form id="user" put_href="/users/1" cancel_href="/users/1" fields={[
%{label: "Name", value: @user.name},
%{label: "Email", value: @user.email, type: "email"}
]} />
hx-target="this" + hx-swap="outerHTML" on each root mean every descendant
request replaces the whole element in place.
"""
use Phoenix.Component
@container "flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm"
@term "text-xs font-medium tracking-wide text-muted-foreground uppercase"
@value "mt-0.5 text-sm font-medium text-foreground"
@label "flex items-center gap-2 text-sm leading-none font-medium select-none " <>
"group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 " <>
"peer-disabled:cursor-not-allowed peer-disabled:opacity-50 " <>
"text-xs font-medium tracking-wide text-muted-foreground uppercase"
@input "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs " <>
"transition-[color,box-shadow] outline-none " <>
"selection:bg-primary selection:text-primary-foreground " <>
"file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground " <>
"placeholder:text-muted-foreground " <>
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " <>
"md:text-sm dark:bg-input/30 " <>
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " <>
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " <>
"[&.htmx-request]:opacity-70"
@btn_base "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none " <>
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " <>
"disabled:pointer-events-none disabled:opacity-50 " <>
"aria-disabled:pointer-events-none aria-disabled:opacity-50 " <>
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " <>
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 " <>
"[&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 " <>
"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5"
@edit_btn @btn_base <>
" border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50"
@save_btn @btn_base <> " bg-primary text-primary-foreground hover:bg-primary/90"
@cancel_btn @btn_base <> " bg-secondary text-secondary-foreground hover:bg-secondary/80"
attr :id, :string, default: nil
attr :edit_href, :string, required: true
attr :edit_label, :string, default: "Edit"
attr :fields, :list, default: []
attr :class, :string, default: nil
attr :rest, :global, include: ~w(hx-get hx-post hx-put hx-patch hx-target hx-swap hx-trigger hx-indicator)
def edit_in_place(assigns) do
assigns =
assigns
|> assign(:container, @container)
|> assign(:term, @term)
|> assign(:value, @value)
|> assign(:edit_btn, @edit_btn)
~H"""
<div
data-slot="edit-in-place"
data-mode="view"
id={@id}
hx-target="this"
hx-swap="outerHTML"
class={[@container, @class]}
{@rest}
>
<dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
<div :for={f <- @fields}>
<dt class={@term}>{f.label}</dt>
<dd class={@value}>{f.value}</dd>
</div>
</dl>
<div class="flex">
<button
type="button"
data-slot="edit-in-place-edit"
hx-get={@edit_href}
hx-target="closest [data-slot='edit-in-place']"
hx-swap="outerHTML"
class={@edit_btn}
>
{@edit_label}
</button>
</div>
</div>
"""
end
attr :id, :string, default: nil
attr :put_href, :string, required: true
attr :cancel_href, :string, required: true
attr :save_label, :string, default: "Save"
attr :cancel_label, :string, default: "Cancel"
attr :fields, :list, default: []
attr :class, :string, default: nil
attr :rest, :global, include: ~w(hx-target hx-swap hx-trigger hx-indicator)
def edit_in_place_form(assigns) do
base_id = assigns.id || "field"
assigns =
assigns
|> assign(:base_id, base_id)
|> assign(:container, @container)
|> assign(:label, @label)
|> assign(:input, @input)
|> assign(:save_btn, @save_btn)
|> assign(:cancel_btn, @cancel_btn)
~H"""
<form
data-slot="edit-in-place"
data-mode="edit"
id={@id}
hx-put={@put_href}
hx-target="this"
hx-swap="outerHTML"
class={[@container, @class]}
{@rest}
>
<div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
<div :for={f <- @fields} class="grid gap-1.5">
<label for={"#{@base_id}-#{f[:name] || String.downcase(f.label)}"} class={@label}>
{f.label}
</label>
<input
id={"#{@base_id}-#{f[:name] || String.downcase(f.label)}"}
name={f[:name] || String.downcase(f.label)}
type={f[:type] || "text"}
value={f.value}
required={f[:required] || nil}
data-slot="input"
class={@input}
/>
</div>
</div>
<div class="flex gap-2">
<button type="submit" data-slot="edit-in-place-save" class={@save_btn}>
{@save_label}
</button>
<button
type="button"
data-slot="edit-in-place-cancel"
hx-get={@cancel_href}
hx-target="closest [data-slot='edit-in-place']"
hx-swap="outerHTML"
class={@cancel_btn}
>
{@cancel_label}
</button>
</div>
</form>
"""
end
end
1. Save the file
Paste the markup; relies only on theme tokens.
2. Use it
<!-- View: GET /users/1 returns this -->
<div data-slot="edit-in-place" id="user" hx-target="this" hx-swap="outerHTML" class="…">
<dl>…</dl>
<button type="button" hx-get="/users/1/edit"
hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML"
class="…">Edit</button>
</div>
<!-- Editor: GET /users/1/edit returns this -->
<form data-slot="edit-in-place" hx-put="/users/1" hx-target="this" hx-swap="outerHTML" class="…">
<label for="user-name">Name</label>
<input id="user-name" name="name" value="Joe Smith" class="…">
<button type="submit">Save</button>
<button type="button" hx-get="/users/1"
hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML">Cancel</button>
</form>View source
<!--
shadcn-htmx — raw HTML Edit In Place snippet.
Mirrors registry/ui/edit-in-place.tsx. Drop onto any page that loads
htmx v4 + Tailwind CSS v4 and the shadcn theme variables (card, border,
ring, primary, secondary, muted, foreground). Relies only on theme tokens.
The canonical htmx editable record: a read-only VIEW with an Edit button
that GETs the EDITOR form; Save issues the PUT; Cancel re-GETs the view.
No modal, no custom JS — the whole thing rides on outerHTML swaps over REST.
See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.
REST endpoints (the URL is the resource, the method is the action):
GET /users/1 → the view fragment below
GET /users/1/edit → the editor fragment below
PUT /users/1 → updates, then returns the view fragment
hx-target="this" + hx-swap="outerHTML" on each root mean every descendant
request (Edit / Save / Cancel) replaces the whole element in place.
-->
<!-- ─── VIEW (GET /users/1) ─────────────────────────────────────────── -->
<div data-slot="edit-in-place" data-mode="view" id="user"
hx-target="this" hx-swap="outerHTML"
class="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm">
<dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
<div>
<dt class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Name</dt>
<dd class="mt-0.5 text-sm font-medium text-foreground">Joe Smith</dd>
</div>
<div>
<dt class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Email</dt>
<dd class="mt-0.5 text-sm font-medium text-foreground">[email protected]</dd>
</div>
</dl>
<div class="flex">
<button type="button" data-slot="edit-in-place-edit" hx-get="/users/1/edit"
hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML"
class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5">Edit</button>
</div>
</div>
<!-- ─── EDITOR (GET /users/1/edit) ──────────────────────────────────── -->
<form data-slot="edit-in-place" data-mode="edit" id="user"
hx-put="/users/1" hx-target="this" hx-swap="outerHTML"
class="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm">
<div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
<div class="grid gap-1.5">
<label for="user-name" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase">Name</label>
<input id="user-name" name="name" type="text" value="Joe Smith" data-slot="input"
class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70">
</div>
<div class="grid gap-1.5">
<label for="user-email" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase">Email</label>
<input id="user-email" name="email" type="email" value="[email protected]" data-slot="input"
class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70">
</div>
</div>
<div class="flex gap-2">
<button type="submit" data-slot="edit-in-place-save"
class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5">Save</button>
<button type="button" data-slot="edit-in-place-cancel" hx-get="/users/1"
hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML"
class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5">Cancel</button>
</div>
</form>
Examples
Record view ↔ editor
Click Edit to swap in the form. Save PUTs the changes and the server returns the updated view; Cancel re-fetches the view, discarding edits.
The view carries hx-target="this" + hx-swap="outerHTML", so the Edit button's response replaces the whole card in place. The editor is a real <form>: Enter submits, the browser validates, and Save issues the PUT. No JavaScript of our own — the platform and htmx do all the work.
- Name
- Joe Smith
- [email protected]
<EditInPlace id="user" editHref="/users/1/edit" fields={[
{ label: "Name", value: user.name },
{ label: "Email", value: user.email, type: "email" },
]} />{{ edit_in_place(edit_href="/users/1/edit", id="user", fields=[
{"label": "Name", "value": user.name},
{"label": "Email", "value": user.email, "type": "email"},
]) }}{{template "edit_in_place" (dict "ID" "user"
"EditHref" "/users/1/edit" "Fields" $fields)}}<.edit_in_place id="user" edit_href={~p"/users/1/edit"} fields={[
%{label: "Name", value: @user.name},
%{label: "Email", value: @user.email, type: "email"}
]} /><div data-slot="edit-in-place" data-mode="view" id="ex-eip-user" hx-target="this" hx-swap="outerHTML" class="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm">
<dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
<div>
<dt class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Name</dt>
<dd class="mt-0.5 text-sm font-medium text-foreground">Joe Smith</dd>
</div>
<div>
<dt class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Email</dt>
<dd class="mt-0.5 text-sm font-medium text-foreground">[email protected]</dd>
</div>
</dl>
<div class="flex">
<button type="button" data-slot="edit-in-place-edit" hx-get="/edit-in-place/user/edit" hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML" class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5">Edit</button>
</div>
</div>Editor with a required field
The editor is a native form, so required + type="email" are enforced by the browser before the PUT ever fires.
Because Save is a type="submit" button inside a real <form>, native constraint validation runs first: an empty required field or a malformed email blocks submission with the browser's own message, and htmx never sends the request. Zero extra code.
<EditInPlaceForm id="user" putHref="/users/1" cancelHref="/users/1" fields={[
{ label: "Name", value: user.name, required: true },
{ label: "Email", value: user.email, type: "email", required: true },
]} />{{ edit_in_place_form(put_href="/users/1", cancel_href="/users/1", id="user", fields=[
{"label": "Name", "value": user.name, "required": true},
{"label": "Email", "value": user.email, "type": "email", "required": true},
]) }}{{template "edit_in_place_form" (dict "ID" "user"
"PutHref" "/users/1" "CancelHref" "/users/1" "Fields" $fields)}}<.edit_in_place_form id="user" put_href={~p"/users/1"} cancel_href={~p"/users/1"} fields={[
%{label: "Name", value: @user.name, name: "name", required: true},
%{label: "Email", value: @user.email, name: "email", type: "email", required: true}
]} /><form data-slot="edit-in-place" data-mode="edit" id="ex-eip-edit" hx-put="/edit-in-place/user" hx-target="this" hx-swap="outerHTML" class="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm">
<div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
<div class="grid gap-1.5">
<label for="ex-eip-edit-name" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase">Name</label>
<input id="ex-eip-edit-name" name="name" type="text" value="Joe Smith" required="" data-slot="input" class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70"/>
</div>
<div class="grid gap-1.5">
<label for="ex-eip-edit-email" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase">Email</label>
<input id="ex-eip-edit-email" name="email" type="email" value="[email protected]" required="" data-slot="input" class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70"/>
</div>
</div>
<div class="flex gap-2">
<button type="submit" data-slot="edit-in-place-save" class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5">Save</button>
<button type="button" data-slot="edit-in-place-cancel" hx-get="/edit-in-place/user" hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML" class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5">Cancel</button>
</div>
</form>Further reading
API Reference
Edit In Place
| Prop | Type | Default | Description |
|---|---|---|---|
editHref* | string | — | GET endpoint that returns the editor form fragment. Set on EditInPlace (the view); wired to the Edit button's hx-get.htmxhx-get |
putHref* | string | — | PUT endpoint hit when Save is pressed. Set on EditInPlaceForm (the editor); the response (the updated view) replaces the form in place.htmxhx-put |
cancelHref* | string | — | GET endpoint that restores the read-only view on Cancel, discarding edits. Set on EditInPlaceForm. |
fields | EditInPlaceField[] | — | Record fields: { label, value, name?, type?, inputValue?, required? }. Rendered as a <dl> of term/value pairs in the view and as pre-filled native inputs in the editor. Omit to pass custom children instead.MDN<dl> element |
editLabel | string | "Edit" | Text on the Edit button in the view. |
saveLabel | string | "Save" | Text on the Save submit button in the editor. |
cancelLabel | string | "Cancel" | Text on the Cancel button in the editor. |
id | string | — | Root id. Reused across the view<->editor swap so the element keeps its identity; also seeds each editor input id as {id}-{name}. |
class | string | — | Extra Tailwind classes appended to the root element. |
hx-* | any | — | Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference |
* required