October 31, 2024
3 min read

Interesting challenges

When creating something new, challenges inevitably arise during development, and UiWizard is no exception. I'd like to outline some interesting insights I discovered during the initial development of UiWizard.

Pages

One of the first issues that really hit was the way to handle a page. When the user of the library wants to create a new page they should not care about most of the html document. One could imagine that there is not much to the setup. However the user should not care about the html tag, meta tags and header tags. What users care about is the content that gets inserted to the body tag.

The solution is quite easy. I just created a default template that everything uses and insert the user specific parts inside the body of the HTML document easy.

The hard part is I wanted to support the user being able to do the following.

@app.page("/")
async def index():
    with ui.element():
        ui.label("hello world")

You might not notice it at first, but the function does not return any values and implicitly returns None. This means we need to hook into the request and ensure that whenever any type of element is created, it is added to the element tree with the correct references to parents and children. Outside the function, it should be possible to fetch the created elements. Also note that ui.element does not explicitly set ui.label as a child; instead, this is handled implicitly by the element's context manager.

Each request to the server needs its own tree of elements to ensure that two requests do not update the same tree. The key factor is that the framework can execute asynchronously. In synchronous mode, this is not an issue. This means that, throughout the application, it should be possible to identify the correct tree. In UiWizard, this is achieved using a class called Frame and by obtaining the ID of the current async task, which represents the current execution.

The async task will ensure that everywhere in the application it is possible to get the correct tree.

Frame.get_stack()

Whenever an element is created, it needs to reference the Frame and append itself to the tree. If it is the first element created, it must set itself as the root of the tree. If it is not the root, it should set itself as a child of the last parent element created.

With our index function example this means that the ui.element will be root and parent. Then when the ui.label is created it will call.

Frame.get_stack().get_parent_node().children.append(self)

This solved one of my biggest issues: requiring the user to manually add children to the parent. It leverages Python's context managers to handle this automatically. If you're unfamiliar with context managers, they work by calling the __enter__ method when the instance is specified with with, and the__exit__ method when the block of code following with ends. Used with the Frame, this approach can generate the HTML tree effectively.

I’m quite pleased with the final interaction with the framework. I think it reads well and functions as intended. An added benefit is that the user is not required to return HTML elements as this allows the function to return the standard Response classes that FastAPI expects, giving you the flexibility to override the framework’s functionality if needed!