Skip to content

Inline Editor

InlineEditor renders a read state and an edit state for small pieces of content. It is designed for HTMX partial replacement workflows without custom JavaScript.

Import

from faststrap import InlineEditor

Read Mode

InlineEditor(
    name="title",
    value="Quarterly planning",
    endpoint="/tasks/1/title",
    edit_endpoint="/tasks/1/title/edit",
    id="task-title",
)

In read mode, the edit button can request edit_endpoint and replace the editor target with the edit form.

Edit Mode

InlineEditor(
    name="title",
    value="Quarterly planning",
    endpoint="/tasks/1/title",
    editing=True,
    id="task-title",
)

In edit mode, the form posts to endpoint using HTMX and swaps the response back into the same editor container.

Patch Endpoint

InlineEditor(
    name="headline",
    value="Launch brief",
    endpoint="/briefs/42/headline",
    method="patch",
    editing=True,
    id="brief-headline",
)

Complete HTMX Flow

This is the simplest read/edit loop:

from fasthtml.common import fast_app
from faststrap import InlineEditor

app, rt = fast_app()

title = "Quarterly planning"


def title_editor(editing: bool = False):
    return InlineEditor(
        "title",
        title,
        editing=editing,
        endpoint="/title",
        edit_endpoint="/title/edit",
        id="title-editor",
        method="patch",
    )


@rt("/")
def home():
    return title_editor()


@rt("/title/edit")
def edit_title():
    return title_editor(editing=True)


@rt("/title", methods=["PATCH"])
async def save_title(request):
    global title
    form = await request.form()
    title = form.get("title", "")
    return title_editor()

The important idea: the edit endpoint returns editing=True, and the save endpoint returns the read view again.

Parameters

Param Type Description
name str Input name.
value str Current raw value.
display str | None Optional display value for read mode.
editing bool Render the edit form instead of read mode.
endpoint str | None Save endpoint.
edit_endpoint str | None Endpoint that returns edit mode markup.
method post | put | patch HTMX save method.
input_type str Input type for edit mode.
hx_target str | None HTMX swap target. Defaults to the component id when available.
hx_swap str HTMX swap mode.

faststrap.components.forms.inline_editor.InlineEditor(name, value='', *, display=None, editing=False, endpoint=None, edit_endpoint=None, method='post', input_type='text', save_label='Save', cancel_label='Cancel', edit_label='Edit', hx_target=None, hx_swap='outerHTML', input_cls=None, actions_cls=None, **kwargs)

Render a compact inline display/edit surface.

Apps can render editing=False for the read view and return editing=True from an HTMX endpoint for the edit view.

Source code in src/faststrap/components/forms/inline_editor.py
@register(category="forms")
@beta
def InlineEditor(
    name: str,
    value: str = "",
    *,
    display: Any | None = None,
    editing: bool = False,
    endpoint: str | None = None,
    edit_endpoint: str | None = None,
    method: InlineEditorMethod = "post",
    input_type: str = "text",
    save_label: str = "Save",
    cancel_label: str = "Cancel",
    edit_label: str = "Edit",
    hx_target: str | None = None,
    hx_swap: str = "outerHTML",
    input_cls: str | None = None,
    actions_cls: str | None = None,
    **kwargs: Any,
) -> Div:
    """Render a compact inline display/edit surface.

    Apps can render `editing=False` for the read view and return
    `editing=True` from an HTMX endpoint for the edit view.
    """
    if method not in {"get", "post", "put", "patch"}:
        msg = f"method must be one of get, post, put, patch; got {method!r}"
        raise ValueError(msg)

    user_cls = kwargs.pop("cls", "")
    attrs: dict[str, Any] = {
        "cls": merge_classes(
            "faststrap-inline-editor d-inline-flex align-items-center gap-2", user_cls
        ),
        "data_fs_inline_editor": "true",
    }
    attrs.update(convert_attrs(kwargs))

    target = hx_target
    if target is None and attrs.get("id"):
        target = f"#{attrs['id']}"

    if not editing:
        edit_attrs: dict[str, Any] = {}
        if edit_endpoint:
            edit_attrs["hx_get"] = edit_endpoint
            if target:
                edit_attrs["hx_target"] = target
            edit_attrs["hx_swap"] = hx_swap

        return Div(
            Span(display if display is not None else value, cls="faststrap-inline-editor-value"),
            Button(edit_label, variant="link", size="sm", cls="p-0", **edit_attrs),
            **attrs,
        )

    form_attrs: dict[str, Any] = {
        "method": method,
        "cls": "d-inline-flex align-items-center gap-2",
    }
    if endpoint:
        form_attrs["action"] = endpoint
        form_attrs[f"hx_{method}"] = endpoint
        if target:
            form_attrs["hx_target"] = target
        form_attrs["hx_swap"] = hx_swap

    input_attrs = {
        "type": input_type,
        "name": name,
        "value": value,
        "cls": merge_classes("form-control form-control-sm", input_cls),
        "aria_label": f"Edit {name}",
    }

    actions = Div(
        Button(save_label, type="submit", variant="primary", size="sm"),
        Button(cancel_label, type="button", variant="secondary", outline=True, size="sm"),
        cls=merge_classes("d-inline-flex gap-2", actions_cls),
    )

    return Div(FTForm(FTInput(**input_attrs), actions, **convert_attrs(form_attrs)), **attrs)