Skip to content

Backend-driven UI

Streamsync facilitates backend-initiated user interface modifications. These changes are made possible through Code-Managed Components (CMCs), distinct from Builder-Managed Components (BMCs).

CMCs, unlike BMCs, are dynamically created and modified via backend code, and cannot be edited (but still can be viewed) within the application builder. It's important to also note that CMCs do not persist in your application's ui.json file and exist only during the application runtime, supporting dynamic UI adjustments.

Experimental feature

This Streamsync feature is still evolving. You may encounter unexpected behaviour. Your feedback is invaluable — please feel free to share your experience and suggestions.

To summarise

CMC – Code-Managed Component

  • created via application backend;
  • cannot be edited in builder;
  • is not saved to ui.json.

BMC – Builder-Managed Component

  • created via builder;
  • can be edited in builder;
  • is saved to ui.json.

UI manager

Streamsync provides two independent approaches for managing your application's UI: initializing a base UI and making session-specific updates.

Initializing base UI

The init_ui() method sets up a UI manager to configure UI components at the application's startup. This creates a component set that is accessible across all sessions:

import streamsync as ss

with ss.init_ui() as ui:
    with ui.Page(id="my-page"):
        ui.Header({"text": "Hello World!"})

Making session-specific updates

For dynamic, session-specific UI updates, the ui parameter is used within handler functions. This approach allows for real-time modifications tailored to individual user sessions:

def display_user_data(ui, state):
    with ui.find("column-container"):
        with ui.Column():
            ui.Text({"text": f"And welcome {state["username"]}!"})
        with ui.Column():
            ui.Text({"text": f"Your data: {state["user_data"]}"})

UI manager methods

find method

You can use the ui.find(component_id: str) method to access existing components by ID:

with ui.find("column-container"):
    with ui.Column():

If the component couldn't be found, the method raises a RuntimeError.

Component methods

UI manager contains methods linked to each frontend component. For example, in previous code snippets we provide a ui.Text method, which is used for creating Text components.

This method expects content: dict as first argument, which enables you to set the field properties of the component, through corresponding keys:

        "text": "Hello World!",  
        # The text content of the component
        "useMarkdown": "no",  
        # Will not use Markdown
        "alignment": "left",  
        # Text is aligned to the left
        "primaryTextColor": "#000000",  
        # The text color is black
        "cssClasses": "my-text hello-world"  
        # Apply 'my-text' and 'hello-world' CSS classes

In a similar way, every other component method also expects content as its first argument:

        "src": "",
        "autoplay": "yes",
        "controls": "no",
        "muted": "no",
        "loop": "no",

In addition to content, a set of fields which is specific to the component type, you can also modify the base properties of the component itself, which are:

  • id: str: A unique identifier used for accessing the component after it was created.
    Providing an identifier that is already taken would result in RuntimeWarning and the existing component being overwritten with a newly created one.
        {"text": "Hello World!"}, 
    If no ID is provided with a component, a UUID is automatically generated for it.
    ::: note Make sure to provide an id if you intend to find the component later
    As the find method relies on id of the component, retrieval might get tricky if its id was generated randomly. :::
  • position: int: Determines the display order of the component in relation to its siblings.
    Position 0 means that the component is the first child of its parent.
    Position -2 is used for components – such as sidebars – that have a specific reserved position not related to their siblings.
        {"text": "Hello Parent, I'm your first child!"}, 
    Position is calculated automatically for each component, and you should be careful when you override it with predefined value, as this might lead to unexpected results.
  • parentId: str: Determines the parent container for the component. By default, components recognise the container in the context of which they were defined as their parent. This allows for linking components to their parents outside of context, or for overriding a parent within a context.
        {"text": "Hello Parent, I'm your child too!"}, 
  • visible: bool | str: Determines the visibility of the component, True by default.
    ui.Text({"text": "I'm visible!"}, visible=True)
    ui.Text({"text": "And I'm not!"}, visible=False)
    ui.Text({"text": "My visibility depends on the @{my_var}!"}, visible="my_var")
  • handlers: dict[str, callable]: Attaches event handlers to the component. Each dictionary key represents an event, and its value is the corresponding handler.:
    def increment(state):
        state["counter"] += 1
    initial_state = ss.init_state({"counter": 0})
        {"text": "My Counter: @{counter}"}, 
        handlers={"ss-click": increment}
    # You have two options for adding a function 
    # to the `handlers` dictionary: 
    # directly pass the function itself, 
    # or use the function's name as a string. 
    # Both approaches yield the same outcome.
    A component can be linked to multiple event handlers.
  • binding: dict[str, str]: Links the component to a state variable via binding. The dictionary key is the bindable event, and the value is the state variable's name:
    initial_state = ss.init_state({
        "header_text": "Default Text"
        "counter": 0
        {"label": "Bound Text"}, 
        binding={"ss-change": "header_text"}
    # This input will display "Default Text"
    # Changing the text in this input will modify the `header_text` variable
        {"minValue": 0, "maxValue": 300, "stepSize": 1}, 
        binding={"ss-number-change": "counter"}
    # This slider will have 0 as a default value
    # Sliding it will modify the `counter` variable
    Unlike handlers, a component can be linked to just one variable via a bindable event. If the binding dictionary includes multiple event-variable pairs, a RuntimeError will be triggered.

Container components

Streamsync provides multiple layout components that can serve as containers for other components.

You can use with keyword to define such layouts:

with ui.Section({"title": "My Section"}):
    ui.Text({"text": 'Hello World!'}, id="hello-world")

It also allows for "chaining" multiple containers together, creating extensive and deeply-nested layout structures when needed:

with ui.ColumnContainer(id="cmc-column-container"):
    with ui.Column(id="cmc-column-1"):
        with ui.Section({"title": "My Section 1"}):
            ui.Text({"text": 'Hello World!'}, id="hello-world-1")
    with ui.Column(id="cmc-column-2"):
        with ui.Section({"title": "My Section 2"}):
            ui.Text({"text": 'Hello World again!'}, id="hello-world-2")

Most components depend on being inside of a container

This means, for example, that Text components in code above cannot be created as "orphans", outside a Column or Section. Attempting to do so would raise an UIError.

By default, components inside container's with are being appended to it:

with ui.Column(id="cmc-column-1"):
    ui.Text({"text": 'Hello World!'}, id="hello-world-1")


# Retrieves the Column component created before
with ui.find(id="cmc-column-1"): 
    # The following component is going to be appended 
    # to the retrieved Column
    ui.Text({"text": 'Hello World again!'}, id="hello-world-2")