Implementing a todo web app with Python and Schorle framework

Ivan Trusov
5 min readFeb 18, 2024

--

Small Worlds by Wassily Kandinsky, 1922. Image source — Wikiart

In my last post, I mentioned my concept of a Pure Python UI library called Schorle. I’ve put some more effort into it, making the API a bit more Pythonic and simplifying it for rendering purposes.

Let’s see how a to-do list web app can be implemented with almost Pure Python (we still use a bit of CSS for styling, but it’s pretty much inevitable).

What’s new?

Recent changes I’ve made provide capabilities for quite interesting functionality:

  1. Conditional rendering
  2. Rendering of lists
  3. Suspense and loading fallback

All these features play quite nicely with the example I’ve implemented. You can take a look at the full source code here, and play with it a bit by installing the schorle package.

Components

While cooking Schorle, I’ve taken some good practices from other UI libraries. One of the abstractions I like in particular is the abstraction of a Component from React. Practically, it allows to combine various HTML elements, as well as other components into logical blocks.

However, one thing that I don’t like about components in React is that you need to use JSX to add some logic (e.g. if-else or for each operation). Considering these ideas, I came up with the following API in Python:

class TodoView(Component):
state: State
classes: Classes = Classes("max-w-96 w-2/3 space-y-2 flex flex-col items-center")

def render(self):
with p(classes=Classes("text-xl")):
_text = "No todos yet." if not self.state.todos else "Your todos:"
text(_text)

for todo in self.state.todos:
with div(classes=Classes("flex flex-row items-center justify-between w-full")):
with p():
text(todo)
with Button(
on=On("click", partial(self.state.remove, todo)),
modifier="error",
):
text("Delete")

def initialize(self):
self.suspense = Suspense(on=self.state, fallback=Loading())
self.bind(self.state)

Let’s stop here for a second to consider what’s happening.

In the render method of a component, the layout is described. Since it’s a valid Python code, any standard Pythonic construction can be used, for example, the if-else conditioning on the text, or a for loop to display all of the todos.

The state class provides access for read and write operations on the page state. Using the On class, it’s quite easy to define a server-side callback to the click trigger.

Indeed, the component needs to be re-rendered on any change of the state. To do so, thebind method is used. Also, since the operations on the state might take time, suspense is added to the component.

What’s happening in the background?

From the client side, prepared HTML will look like this:

<div class="max-w-96 w-2/3 space-y-2 flex flex-col items-center" id="sle-div-7be95dec" hx-swap-oob="morph">
<p class="text-xl" hx-swap-oob="morph">Your todos:</p>
<div class="flex flex-row items-center justify-between w-full" hx-swap-oob="morph">
<p hx-swap-oob="morph">Buy milk</p>
<button class="btn btn-error" hx-swap-oob="morph" ws-send="" hx-trigger="click" id="sle-059f8171">Delete</button>
</div>
<div class="flex flex-row items-center justify-between w-full" hx-swap-oob="morph">
<p hx-swap-oob="morph">Do laundry</p>
<button class="btn btn-error" hx-swap-oob="morph" ws-send="" hx-trigger="click" id="sle-a561de43">Delete</button>
</div>
</div>

The generated IDs are used for client-server communication. For instance, deleting an item will cause the following message:

{
"HEADERS": {
"HX-Request": "true",
"HX-Trigger": "sle-e164c325",
"HX-Trigger-Name": null,
"HX-Target": "sle-e164c325",
"HX-Current-URL": "http://127.0.0.1:8000/",
"HX-Trigger-Type": "click"
}
}

At first, the server will respond with a loading view:

<div id="sle-div-ed017a60" hx-swap-oob="morph">
<div id="sle-div-7f1e0849">
<span class="loading loading-lg loading-infinity"></span>
</div>
</div>

And then, as soon as the operation is finished, a proper response will come:

<div class="max-w-96 w-2/3 space-y-2 flex flex-col items-center" id="sle-div-ed017a60" hx-swap-oob="morph">
<p class="text-xl" hx-swap-oob="morph">Your todos:</p>
<div class="flex flex-row items-center justify-between w-full" hx-swap-oob="morph">
<p hx-swap-oob="morph">Buy milk</p>
<button class="btn btn-error" hx-swap-oob="morph" ws-send="" hx-trigger="click" id="sle-ff9a97ae">Delete</button>
</div>
</div>

On the UI, all of this will look as follows:

Example of a todo list

State operations

Since I’ve mentioned state, let’s take a look at the implementation of it:

class State(ReactiveModel):
current: str = ""
todos: list[str] = Field(default_factory=lambda: ["Buy milk", "Do laundry"])

@effector
async def set_current(self, value):
self.current = value

@effector
async def add_todo(self):
await asyncio.sleep(random() * 2) # simulate a slow backend
self.todos.append(self.current)
await self.set_current("")

@effector
async def remove(self, todo):
await asyncio.sleep(random() * 2) # simulate a slow backend
self.todos.remove(todo)

ReactiveModel is a class provided by the schorle framework. To simplify the explanation, consider it as a standard Pydantic BaseModel, with some necessary additions to make @effector work.

The @effector decorator highlights that this method has effects on the UI. Calling it outside of the schorle context, without any binds won’t affect the logic of the methods inside.

Text input

Since we also need to match the input together with the “Add” button, we need some way to exchange the input information with the server. Fortunately, we can use the same state class to do so, and add another UI component to combine this all together:

class InputSection(Component):
state: State

def render(self):
with div(classes=Classes("flex flex-row w-3/4 justify-center items-center space-x-4")):
TextInput(
value=self.state.current,
placeholder="Add a todo",
classes=Classes("input-bordered"),
on=On("change", self.state.set_current),
)
with Button(on=On("click", self.state.add_todo), modifier="primary"):
text("Add")

def initialize(self):
self.bind(self.state)

Since the render is triggered only when any effectors on the state are fired, it will always correctly display the value of the input. When the user clicks Add, the value saved into current is used to add the todo.

Summary

Altogether, it took only 98 lines of code to prepare quite a functional to-do list using the framework, which I find particularly good.

Take note that we don’t use any frontend code, there is no need to JSONify the inputs back and forth between the server and the client. In this sense, the Schorle framework closely follows the Hypermedia As The Engine Of Application State (HATEAOS) principles.

What I find the most interesting is that with such an architecture developers can immediately interop with the inputs on the server side. For instance, to add the todo information into a database, the only required thing is to add saving methods to the State class defined above.

Under the hood, schorle indeed stands on the shoulders of giants:

  • FastAPI — web framework
  • HTMX — client-side library for dynamic HTML
  • Tailwind CSS — CSS framework
  • DaisyUI — Component library for Tailwind CSS
  • Pydantic — classes, and utilities for elements

I find a combination of FastAPI and HTMX very powerful, and the idea of the framework is to expose this power to the developers without having too much hassle with various low-level details.

CSS styling is almost inevitable for a properly extensible framework, and here Tailwind + DaisyUI seems like the best fit from the existing toolset.

If you’re interested in the concepts or ideas implemented in Schorle, I would be happy to hear your feedback. Try it out yourselves, and let me know if there are any issues or ideas on what could make it a better framework ☺️

--

--

Ivan Trusov

Senior Specialist Solutions Architect @ Databricks. All opinions are my own.