Skip to content

1. A server-rendered list

Goal: rows from the store on the screen, through the full mount — the skeleton every later part hangs off.

The mount

app.py
from __future__ import annotations

from collections.abc import Mapping
from pathlib import Path

import situ
from dishka import Provider, Scope, make_async_container, provide
from dishka.integrations.litestar import setup_dishka
from litestar import Litestar
from litestar.plugins.jinja import JinjaTemplateEngine
from litestar.static_files import create_static_files_router
from litestar.template.config import TemplateConfig
from situ import mount_component

from store import IssueStore

HERE = Path(__file__).parent


class AppProvider(Provider):
    scope = Scope.APP

    @provide
    def store(self) -> IssueStore:
        return IssueStore()  # one in-memory store shared by every request


async def context(service: IssueStore, view: Mapping[str, str]) -> Mapping[str, object]:
    """(service, url-view) -> the dict the template renders, fresh on every render."""
    which = view.get("filter", "all")
    return {"issues": service.visible(which), "filter": which}


def create_app() -> Litestar:
    app = Litestar(
        route_handlers=[
            mount_component(
                path="/",
                stem=HERE / "components" / "tracker",
                template="page.html",
                meta={"name": "Tracker"},
                service_type=IssueStore,
                context=context,
            ),
            create_static_files_router(path="/static", directories=[situ.static_dir()]),
        ],
        template_config=TemplateConfig(
            directory=[HERE / "templates", situ.templates_dir()],
            engine=JinjaTemplateEngine,
        ),
    )
    setup_dishka(make_async_container(AppProvider()), app)
    return app


app = create_app()

Three things to register:

  • mount_component compiles the component pair once and derives every route. service_type=IssueStore is what gets resolved from the Dishka container per request; context turns the service (plus the URL view — used from part 3) into the dict the template renders.
  • The template config lists two directories — yours first, then situ's — so template="page.html" resolves your copy while situ's default stays available as a fallback.
  • The static router serves situ.static_dir(), where the shared shim _rt.js lives.

The component, v1

from __future__ import annotations

from situ import Server

issues: Server[object]
<section class="tracker">
  <div data-region>
    <p class="count">{{ issues | length }} shown</p>
    <ul class="issue-list">
      {% for t in issues %}
      <li data-id="{{ t.id }}">
        <span>{{ t.title }}</span>
        <span class="status {{ t.status }}">{{ t.status }}</span>
      </li>
      {% endfor %}
    </ul>
  </div>
</section>

One signal, sited Server: the authoritative value lives on the server. In the template, issues is whatever context() returned under that name — ordinary Jinja renders it. (The object type parameter becomes a typed facade Protocol in part 5, when handlers start calling it.)

The <div data-region> matters: it is one of the two roots situ wires at boot, and the only part of the page a server command will re-render later. Keep data-region as the div's first attribute.

Run it

uvicorn app:app --reload
# open http://localhost:8000/

Three rows render. --reload restarts the process on every file save, which matters here: a mount compiles its component once per process, so the restart is what picks up your edits throughout this tutorial.

What got derived

Two routes exist so far — GET / (the page) and GET /island.js (the generated island, ~5.3 KB even now: it carries the shared boot scaffolding, waiting for signals). No command routes yet: the component has no handlers. Open /island.js and read it; it will grow with each part.

Next: Part 2 — live search, where the browser gets its first signal.