Edit Row
This example shows how to implement editable rows. In this example, we create a table with a row for each person in the database. Each row contains an edit button which allows inline editing of user's data.
Demo
#Implementation
#In this example, we'll be creating three class-based endpoints:
- One for displaying the person's information in a row with the edit button;
- Another for displaying the form for editing the person's data and an action button to confirm the changes;
- Last one for displaying the whole table of people.
First, we create the attributes which define what kind of data we want to display:
from typing import Annotated, NotRequired from ludic.attrs import Attrs from ludic.catalog.tables import ColumnMeta class PersonAttrs(Attrs): id: NotRequired[str] name: Annotated[str, ColumnMeta()] email: Annotated[str, ColumnMeta()] class PeopleAttrs(Attrs): people: list[PersonAttrs]
Now we create the PersonRow
endpoint for displaying the person's information in a row:
from typing import Self, override from ludic.base import JavaScript from ludic.catalog.buttons import ButtonPrimary from ludic.catalog.tables import TableRow from ludic.web import Endpoint, LudicApp from ludic.web.parsers import Parser from your_app.attrs import PersonAttrs from your_app.database import db app = LudicApp() @app.endpoint("/people/{id}") class PersonRow(Endpoint[PersonAttrs]): on_click_script: JavaScript = JavaScript( """ let editing = document.querySelector('.editing') if (editing) { alert('You are already editing a row') } else { htmx.trigger(this, 'edit') } """ ) @classmethod async def put(cls, id: str, data: Parser[PersonAttrs]) -> Self: person = db.people.get(id) if person is None: raise NotFoundError("Person not found") for attr, value in data.validate().items(): setattr(person, attr, value) return cls(**person.dict()) @classmethod async def get(cls, id: str) -> Self: person = db.people.get(id) if person is None: raise NotFoundError("Person not found") return cls(**person.dict()) @override def render(self) -> TableRow: return TableRow( self.attrs["name"], self.attrs["email"], ButtonPrimary( "Edit", hx_get=self.url_for(PersonForm), hx_trigger="edit", on_click=self.on_click_script, classes=["small"], ), )
We created a class-based endpoint, here is the explanation of its individual parts:
on_click_script
– this script is executed when the edit button is clicked. We don't want to allow the user to edit multiple rows at once so we check if the user is already editing a row.put
– handles the PUT request which updates a person in the database. We return the updated person data as a row in the response.get
– handles the GET request which returns an instance of thePeopleRow
of the requested person.render
– handles rendering of the row. Apart from the textual columns, we also render an action button to edit a row.
Now, we want to create an endpoint which renders the form we want to display when a row is being edited:
from typing import Self, override from ludic.catalog.buttons import ButtonSecondary, ButtonSuccess from ludic.catalog.forms import InputField from ludic.catalog.tables import TableRow from ludic.web import Endpoint from your_app.attrs import PersonAttrs from your_app.database import db @app.endpoint("/people/{id}/form/") class PersonForm(Endpoint[PersonAttrs]): @classmethod async def get(cls, id: str) -> Self: person = db.people.get(id) if person is None: raise NotFoundError("Person not found") return cls(**person.dict()) @override def render(self) -> TableRow: return TableRow( InputField(name="name", value=self.attrs["name"]), InputField(name="email", value=self.attrs["email"]), Cluster( ButtonSecondary( "Cancel", hx_get=self.url_for(PersonRow), classes=["small"], ), ButtonSuccess( "Save", hx_put=self.url_for(PersonRow), hx_include="closest tr", classes=["small"], ), classes=["cluster-small"], ), classes=["editing"], )
We created a class-based endpoint with the following methods:
get
– handles the GET request which renders the requested person's row.render
– handles rendering of the person's edit row. Apart from the name and email columns, we are also rendering two action buttons, one canceling the action and one saving the changes.
The last class-based endpoint is for displaying the table of people:
from typing import Self, override from ludic.catalog.tables import Table, TableHead from ludic.web import Endpoint from your_app.attrs import PeopleAttrs from your_app.database import db # ... PersonRow and PersonForm classes omitted along with the imports @app.endpoint("/people/") class PeopleTable(Endpoint[PeopleAttrs]): @classmethod async def get(cls) -> Self: return cls(people=[person.dict() for person in db.people.values()]) @override def render(self) -> Table[TableHead, PersonRow]: return Table[TableHead, PersonRow]( TableHead("Name", "Email", "Action"), *(PersonRow(**person) for person in self.attrs["people"]), body_attrs=HtmxAttrs( hx_target="closest tr" ), classes=["text-align-center"], )
We created a class-based endpoint with the following methods:
get
– handles the GET request which returns an instance of thePeopleTable
filled with a list of people fetched from database.render
– handles rendering of the table of people. We use a specialbody_attrs
attribute to configure HTMX operations on thetbody
element.