`ModelForm submit_on_change` does not always work when coupled with `tabs` · Issue #330 · pydantic/FastUI · GitHub
Skip to content
This repository was archived by the owner on Jun 7, 2026. It is now read-only.
This repository was archived by the owner on Jun 7, 2026. It is now read-only.

ModelForm submit_on_change does not always work when coupled with tabs #330

Open

Description

@tim-x-y-z

I am trying to combine using tabs and a model form to vary 2 variables and when switching tabs the submit on change of the model form seems to stop working. But then, refreshing the page works well but swtiching tabs again breaks it again. see the video below for a more "ui" explanation of the issue.
Let me know if i should try to explain more; also included a code snippet to reproduce.
Also, sorry if i used one of the component in a wrong way and it forces this error.

video

Screen.Recording.2024-06-03.at.11.01.38.mov

reproducible example uvicorn main:router --reload:

import typing as _t
from datetime import date, timedelta

from fastapi import FastAPI, Depends
from fastapi.responses import HTMLResponse
from fastui import FastUI, prebuilt_html
from fastui import components as c
from fastui.events import PageEvent
from pydantic import BaseModel
from enum import Enum


class ErcotMarket(Enum):
    RTM = "rtm"
    DAM = "dam"


class Nodes(Enum):
    HB_NORTH = "HB_NORTH"
    HB_SOUTH = "HB_SOUTH"


class HourlyMarketPriceModel(BaseModel):
    delivery_date: date
    delivery_hour_ending: int
    market: ErcotMarket
    node: str
    price_per_mwh: float


async def get_market_prices_from_db(delivery_date, market, node):
    return [
        HourlyMarketPriceModel(
            delivery_date=delivery_date,
            delivery_hour_ending=i,
            market=market,
            node=node,
            price_per_mwh=i * 10.0 + 10 * (1 if market == ErcotMarket.RTM else 0),
        )
        for i in range(1, 25)
    ]


router = FastAPI()

ErcotMarketLiteral: _t.TypeAlias = _t.Literal["rtm", "dam", "dart"]


class NodesFormModel(BaseModel):
    node: Nodes


@router.get("/api/{ercot_market}", response_model=FastUI, response_model_exclude_none=True)
async def market(
    ercot_market: ErcotMarketLiteral,
    node: str = "HB_NORTH",
):
    return [
        c.Page(
            components=[
                c.LinkList(
                    links=[
                        c.Link(
                            components=[c.Text(text="Real Time Market")],
                            on_click=PageEvent(
                                name="change-market",
                                push_path=f"/rtm?node={node}",
                                context={"ercot_market": "rtm", "node": node},
                            ),
                            active="/rtm",
                        ),
                        c.Link(
                            components=[c.Text(text="Day Ahead Market")],
                            on_click=PageEvent(
                                name="change-market",
                                push_path=f"/dam?node={node}",
                                context={"ercot_market": "dam", "node": node},
                            ),
                            active="/dam",
                        ),
                        c.Link(
                            components=[c.Text(text="DA-RT")],
                            on_click=PageEvent(
                                name="change-market",
                                push_path=f"/dart?node={node}",
                                context={"ercot_market": "dart", "node": node},
                            ),
                            active="/dart",
                        ),
                    ],
                    mode="tabs",
                    class_name="+ mb-4",
                ),
                c.ModelForm(
                    model=NodesFormModel,
                    submit_url=".",
                    initial={"node": Nodes.HB_NORTH},
                    method="GOTO",
                    submit_on_change=True,
                    display_mode="inline",
                ),
                c.ServerLoad(
                    path="/content/{ercot_market}?node={node}",
                    load_trigger=PageEvent(name="change-market"),
                    components=await market_content(ercot_market, node),
                ),
            ],
        )
    ]


@router.get(
    "/api/content/{ercot_market}",
    response_model=FastUI,
    response_model_exclude_none=True,
)
async def market_content(ercot_market: ErcotMarketLiteral, node: str) -> list[c.AnyComponent]:
    yesterday = date.today() - timedelta(days=1)
    match ercot_market:
        case "rtm":
            market_prices = await get_market_prices_from_db(
                delivery_date=yesterday,
                market=ErcotMarket.RTM,
                node=Nodes(node.upper()),
            )
        case "dam":
            market_prices = await get_market_prices_from_db(
                delivery_date=yesterday,
                market=ErcotMarket.DAM,
                node=Nodes(node.upper()),
            )
        case "dart":
            dam_market_prices = await get_market_prices_from_db(
                delivery_date=yesterday,
                market=ErcotMarket.DAM,
                node=Nodes(node.upper()),
            )
            rtm_market_prices = await get_market_prices_from_db(
                delivery_date=yesterday,
                market=ErcotMarket.RTM,
                node=Nodes(node.upper()),
            )
            dam_prices = {(p.delivery_date, p.delivery_hour_ending, p.node): p for p in dam_market_prices}
            rtm_prices = {(p.delivery_date, p.delivery_hour_ending, p.node): p for p in rtm_market_prices}

            market_prices = []
            for key, dam_price in dam_prices.items():
                rtm_price = rtm_prices.get(key)
                if rtm_price:
                    dart_price = dam_price.price_per_mwh - rtm_price.price_per_mwh
                    market_prices.append(HourlyMarketPriceModel(**{**rtm_price.dict(), "price_per_mwh": dart_price}))
        case _:
            raise ValueError("Invalid market")

    return [
        c.Table(
            data=market_prices,
            data_model=HourlyMarketPriceModel,
            columns=[
                c.display.DisplayLookup(
                    field="delivery_date",
                    table_width_percent=10,
                    mode=c.display.DisplayMode.date,
                ),
                c.display.DisplayLookup(field="delivery_hour_ending", table_width_percent=10),
                c.display.DisplayLookup(field="node", table_width_percent=20),
                c.display.DisplayLookup(field="price_per_mwh", table_width_percent=20),
            ],
        )
    ]


@router.get("/{path:path}")
async def html_landing() -> HTMLResponse:
    """Simple HTML page which serves the React app, comes last as it matches all paths."""
    return HTMLResponse(prebuilt_html(title="FastUI Demo"))

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions