Click To Load
This example shows how to implement click-to-load the next page in a table of data. We create a table containing a list of contacts which we lazy load whenever user clicks on a button.
Demo
#Implementation
#First, we define the contact's attributes and also attributes of an endpoint-based component we'll use later to hold the contacts and the current page:
from ludic.attrs import Attrs class ContactAttrs(Attrs): id: str name: str email: str class ContactsSliceAttrs(Attrs): page: int contacts: list[ContactAttrs]
Now we need to create two components. First one to display the table layout and its header columns. The second one to load the contacts data in the form of the table's rows. The reason for separating these components is to avoid duplicated code.
from typing import Self, override from ludic.catalog.tables import Table, TableHead, TableRow from ludic.components import Blank, Component from ludic.web import Endpoint, LudicApp from your_app.attrs import ContactAttrs, ContactsSliceAttrs from your_app.database import db from your_app.components import LoadMoreButton app = LudicApp() @app.get("/") async def table_of_contacts() -> ContactsTable: return ContactsTable(await ContactsSlice.get(QueryParams(page=1))), @app.endpoint("/contacts/") class ContactsSlice(Endpoint[ContactsSliceAttrs]): @classmethod async def get(cls, params: QueryParams) -> Self: page = int(params.get("page", 1)) return cls(page=page, contacts=db.load_contacts_page(page)) @override def render(self) -> Blank[TableRow]: next_page = self.attrs["page"] + 1 return Blank( *( TableRow(contact["id"], contact["name"], contact["email"]) for contact in self.attrs["contacts"] ), TableRow( td( LoadMoreButton( url=self.url_for( ContactsSlice ).include_query_params( page=next_page ), ), colspan=3, ), id=LoadMoreButton.target, ), ) class ContactsTable(Component[ContactsSlice, Attrs]): @override def render(self) -> Table[TableHead, ContactsSlice]: return Table( TableHead("ID", "Name", "Email"), *self.children, classes=["text-align-center"], )
First endpoint is function-based and is rendering the initial table of contacts. We use the ContactsSlice.get()
method to load the first page. It would probably be better to separate this loading to some handler function.
The ContactsSlice
endpoint-based component is loading the table rows and implements two methods:
get
– handles the GET request which loads contacts from the database. The handler returns an instance of itself.render
– handles rendering of the table rows and theLoad More
button. In order for everything to work properly, we render only the table's rows while wrapping them in theBlank
component. This component does not render any HTML element, it's just a wrapper when we want to render a list of elements.
Last, the ContactsTable
class is a regular component that renders the table's columns.
The last remaining piece is the LoadMoreButton
component which triggers the GET
request calling the ContactsSlice
method to load more contact data:
from typing import override from ludic.attrs import Attrs from ludic.catalog.buttons import ButtonPrimary from ludic.components import ComponentStrict class LoadMoreAttrs(Attrs): url: str class LoadMoreButton(ComponentStrict[LoadMoreAttrs]): target: str = "replace-me" @override def render(self) -> ButtonPrimary: return ButtonPrimary( "Load More Agents...", hx_get=self.attrs["url"], hx_target=f"#{self.target}", )
We use the HTMX swap operation to replace the button itself with a new slice of contacts rendered by the ContactsSlice
endpoint. That is, the next page of rows as well as the button itself running the HTMX operation. Since the ContactsSlice
always bumps the button link's page, we get the next page of contacts on every click.