Skip to content

Inline Edit

Inline editing allows users to modify a single field directly from the list page. Clicking a cell opens a small popover, eliminating the need to navigate to the full edit form. Use this feature for quick, single-field updates like fixing a title, toggling a status, or adjusting a date. The interaction mirrors the familiar x-editable pattern.

This feature is opt-in and disabled by default. Enabling it does not affect the standard edit page, which remains the primary interface for complex, multi-field edits.

For a runnable example with inline editing, see examples/01-quickstart.

Basic Usage

Declare the editable field names in the inline_editable_fields list:

from starlette_admin.contrib.sqla import ModelView


class PostView(ModelView):
    fields = ["id", "title", "content", "status", "views", "published_at"]
    inline_editable_fields = ["title", "status", "views", "published_at"]

When enabled, editable cells display a dashed underline on the list page. Clicking a cell opens a popover with the field's standard form control, pre-filled with the current value.

The underline is not painted on the whole cell. Each list template places the inline-edit-value CSS class on the exact element to underline, and the style only activates inside an editable cell. All built-in list templates already carry the class. A custom list_template that should show the affordance must add the class itself:

<span class="avatar avatar-xs me-2">...</span>
<span class="inline-edit-value">{{ data.name }}</span>

Omitting the class keeps the cell fully clickable but renders it without the underline.

  • Save: Click the check button or press Enter (for single-line inputs). This validates the field, persists the change, and refreshes the row without a page reload.
  • Cancel: Click the x button or press Esc to discard changes.

Configuration Rules

The application validates inline_editable_fields at startup to ensure misconfigurations fail fast. A ValueError is raised when a listed name meets any of the following conditions:

  • Is not declared in fields.
  • Is the primary key field.
  • Is excluded from the list page (exclude_from_list) or the edit form (exclude_from_edit).
  • Is a container or read-only field (CollectionField, ListField, ComputedField, FileField, or ImageField).sa

Field Support

Every editable field renders the exact form widget it uses on the edit page. A field's specific JavaScript and CSS assets (like select2, flatpickr, JSONEditor, or TinyMCE) load on the list page only when that field is inline-editable. Views without inline edit keep their current lightweight page footprint.

Field Type Supported Popover Widget
StringField, EmailField, URLField, PhoneField, ColorField, PasswordField Yes Plain input
SlugField Yes Plain input (source field excluded)
TextAreaField Yes Textarea
IntegerField, DecimalField, FloatField Yes Number input
BooleanField Yes Toggle
DateField, DateTimeField, TimeField, ArrowField Yes flatpickr
EnumField, TimeZoneField, CountryField, CurrencyField Yes select2 or native select
TagsField Yes select2 tags
JSONField Yes JSONEditor
TinyMCEEditorField Yes TinyMCE
HasOne, HasMany Yes select2 with async lookup
FileField, ImageField No None (requires edit page)
CollectionField, ListField, ComputedField No None (read-only containers)

Permissions

Inline editing reuses the existing permission model. The popover appears and accepts requests only when both is_accessible(request) and can_edit(request) return True. Overriding can_edit automatically secures inline edits:

class PostView(ModelView):
    inline_editable_fields = ["title", "status"]

    def can_edit(self, request: Request) -> bool:
        # Also disables inline edit when False
        return "edit:post" in request.state.admin_user.roles

Validation

An inline save validates and writes only the edited field.

  • The field's required check and validators chain run exactly as they do on the edit page.
  • Other fields are bypassed. A save from the list page cannot overwrite a concurrent edit to a different field, and existing invalid data in other fields will not block the save.

The view's cross-field validate hook still runs, but the data dictionary contains only the edited field. Hooks expecting full-form submissions will raise a KeyError if they index missing keys directly. Use conditional checks to verify key presence:

from typing import Any
from starlette.requests import Request
from starlette_admin.exceptions import FormValidationError
from starlette_admin.contrib.sqla import ModelView


class PostView(ModelView):
    inline_editable_fields = ["title", "status", "published_at"]

    async def validate(self, request: Request, data: dict[str, Any]) -> None:
        errors: dict[str, str] = {}

        if "title" in data and (not data["title"] or len(data["title"]) < 3):
            errors["title"] = "Ensure this value has at least 3 characters"

        if (
            "published_at" in data
            and data.get("status") == "published"
            and data["published_at"] is None
        ):
            errors["published_at"] = "Required when status is published"

        if errors:
            raise FormValidationError(errors)

        await super().validate(request, data)

On validation failure, the popover stays open with the submitted value intact. The edited field's message renders under the control, exactly as on the edit page. A message keyed to another field is prefixed with that field's label.

Detect an inline save inside a hook using request.state.action == RequestAction.INLINE_EDIT. This is useful for skipping flash messages intended for full page renders.

Warning A validation rule keyed to a field that was not edited will not fire during an inline save. If a field's invariants depend on values the user cannot see or change from the list page, exclude it from inline_editable_fields.


Lifecycle Hooks and Events

Inline saves route through the view's standard edit() path. The before_edit, after_edit, and after_edit_committed hooks fire normally, and the corresponding events use the standard context types. However, the data and old_data payloads contain only the edited field to accurately reflect what the save touched.

To distinguish an inline save within an event listener, check ctx.extra["inline"]. This value is set to True for inline edits:

from starlette_admin import AdminEvent
from starlette_admin.events import AfterEditContext


@admin.events.on(AdminEvent.AFTER_EDIT)
async def audit(ctx: AfterEditContext) -> None:
    source = "list page" if ctx.extra.get("inline") else "edit page"
    logger.info("updated %s pk=%s from the %s", ctx.view_key, ctx.pk, source)

Custom Fields

Custom fields support inline editing automatically if they follow the standard BaseField contract. Because RequestAction.INLINE_EDIT is a form action, action.is_form() returns True. If your custom field code checks action == RequestAction.EDIT to generate a form-value representation, update it to use action.is_form() instead. This ensures the popover receives the correct representation. Review Custom Fields for the complete field contract.