Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions marimo/_output/hypertext.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from marimo._utils.methods import getcallable

if TYPE_CHECKING:
from collections.abc import Iterator
from collections.abc import Iterator, Sequence

from marimo._plugins.ui._core.ui_element import UIElement
from marimo._plugins.ui._impl.batch import batch as batch_plugin
Expand Down Expand Up @@ -289,7 +289,39 @@ def _repr_html_(self) -> str:
return self.text


class _BlockWrapped(Html):
class ContainerHtml(Html):
"""Base class for Html wrappers that contain other renderables.

A `ContainerHtml` holds *strong* references to its children and re-renders
from their live `.text` on every access. This matters for two reasons:

- UI elements are only weakly referenced by the UI element registry, so a
container that merely froze `child.text` at construction time would let
its children be garbage collected -- breaking interactivity for elements
returned from helper functions. Holding strong references keeps the
children alive, mirroring `mo.hstack`/`mo.vstack`.
- Mutable children (e.g. `mo.status.spinner`) re-render live rather than
being frozen at construction time.

Subclasses store any extra rendering state on `self` *before* calling
`super().__init__()` and implement `_build_text` in terms of
`self._children`.
"""

def __init__(self, children: Sequence[Html]) -> None:
self._children: list[Html] = list(children)
super().__init__(self._build_text())

def _build_text(self) -> str:
raise NotImplementedError

@property
def text(self) -> str: # type: ignore[override]
"""Re-render children live on every access."""
return self._build_text()


class _BlockWrapped(ContainerHtml):
"""Wraps another Html in a plain block `<div>`.

Used by :meth:`Html.center`, :meth:`Html.left`, and :meth:`Html.right`
Expand All @@ -303,17 +335,12 @@ class _BlockWrapped(Html):
"""

def __init__(self, inner: Html) -> None:
self._inner = inner
super().__init__(self._build_text())
super().__init__([inner])

def _build_text(self) -> str:
from marimo._output.builder import h

return h.div(self._inner.text)

@property
def text(self) -> str:
return self._build_text()
return h.div(self._children[0].text)


MARIMO_NO_JS_KEY = "MARIMO_NO_JS"
Expand Down
53 changes: 33 additions & 20 deletions marimo/_plugins/stateless/accordion.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@
from __future__ import annotations

from marimo._output.formatting import as_html
from marimo._output.hypertext import Html
from marimo._output.hypertext import ContainerHtml, Html
from marimo._output.md import md
from marimo._output.rich_help import mddoc
from marimo._plugins.core.web_component import build_stateless_plugin
from marimo._plugins.stateless.lazy import lazy as lazy_ui


@mddoc
def accordion(
items: dict[str, object], multiple: bool = False, lazy: bool = False
) -> Html:
class accordion(ContainerHtml):
"""Accordion of one or more items.

Args:
Expand Down Expand Up @@ -43,21 +41,36 @@ def accordion(
returns the item to render.
"""

def render_content(tab: object) -> str:
if lazy:
return lazy_ui(tab).text
if isinstance(tab, str):
return md(tab).text
return as_html(tab).text

item_labels = [md(label).text for label in items]
item_content = "".join(
["<div>" + render_content(item) + "</div>" for item in items.values()]
)
return Html(
build_stateless_plugin(
def __init__(
self,
items: dict[str, object],
multiple: bool = False,
lazy: bool = False,
) -> None:
self._multiple = multiple
self._lazy = lazy

self._tabs: list[Html]
if self._lazy:
self._tabs = [lazy_ui(tab) for tab in items.values()]
else:
self._tabs = [
md(tab) if isinstance(tab, str) else as_html(tab)
for tab in items.values()
]

self._labels = [md(label) for label in items]

super().__init__([*self._tabs, *self._labels])

def _build_text(self) -> str:
return build_stateless_plugin(
component_name="marimo-accordion",
args={"labels": item_labels, "multiple": multiple},
slotted_html=item_content,
args={
"labels": [label.text for label in self._labels],
"multiple": self._multiple,
},
slotted_html="".join(
[f"<div>{tab.text}</div>" for tab in self._tabs]
),
)
)
25 changes: 16 additions & 9 deletions marimo/_plugins/stateless/callout.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
from typing import Literal

from marimo._output.formatting import as_html
from marimo._output.hypertext import Html
from marimo._output.hypertext import ContainerHtml
from marimo._output.rich_help import mddoc
from marimo._plugins.core.web_component import build_stateless_plugin

CalloutKind = Literal["neutral", "warn", "success", "info", "danger"]


@mddoc
def callout(
value: object,
kind: Literal["neutral", "warn", "success", "info", "danger"] = "neutral",
) -> Html:
class callout(ContainerHtml):
"""Build a callout output.

Args:
Expand All @@ -23,9 +22,17 @@ def callout(
Returns:
Html (marimo.Html): An HTML object.
"""
return Html(
build_stateless_plugin(

def __init__(
self,
value: object,
kind: CalloutKind = "neutral",
) -> None:
self._kind = kind
super().__init__([as_html(value)])

def _build_text(self) -> str:
return build_stateless_plugin(
component_name="marimo-callout-output",
args={"html": as_html(value).text, "kind": kind},
args={"html": self._children[0].text, "kind": self._kind},
)
)
29 changes: 14 additions & 15 deletions marimo/_plugins/stateless/carousel.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import TYPE_CHECKING

from marimo._output.formatting import as_html
from marimo._output.hypertext import Html
from marimo._output.hypertext import ContainerHtml
from marimo._output.md import md
from marimo._output.rich_help import mddoc
from marimo._plugins.core.web_component import build_stateless_plugin
Expand All @@ -14,9 +14,7 @@


@mddoc
def carousel(
items: Sequence[object],
) -> Html:
class carousel(ContainerHtml):
"""Create a carousel of items.

Args:
Expand All @@ -30,17 +28,18 @@ def carousel(
mo.carousel([mo.md("..."), mo.ui.text_area()])
```
"""
item_content = "".join(
[
(md(item).text if isinstance(item, str) else as_html(item).text)
for item in items
]
)

return Html(
build_stateless_plugin(

def __init__(self, items: Sequence[object]) -> None:
super().__init__(
[
md(item) if isinstance(item, str) else as_html(item)
for item in items
]
)

def _build_text(self) -> str:
return build_stateless_plugin(
component_name="marimo-carousel",
args={},
slotted_html=item_content,
slotted_html="".join(c.text for c in self._children),
)
)
28 changes: 17 additions & 11 deletions marimo/_plugins/stateless/sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

from marimo._output import md
from marimo._output.formatting import as_html
from marimo._output.hypertext import Html
from marimo._output.hypertext import ContainerHtml, Html
from marimo._output.rich_help import mddoc
from marimo._plugins.core.web_component import build_stateless_plugin
from marimo._plugins.stateless.flex import vstack


@mddoc
class sidebar(Html):
class sidebar(ContainerHtml):
"""Displays content in a sidebar.

This is a special layout component that will display the content in a sidebar
Expand Down Expand Up @@ -78,17 +78,23 @@ def __init__(
item = vstack([item, footer], justify="space-between")

# Build props
props: dict[str, Any] = {}
self._props: dict[str, Any] = {}
if width is not None:
# Width must be a string for JSON serialization
props["width"] = str(width)

super().__init__(
build_stateless_plugin(
"marimo-sidebar",
props,
as_html(item).text,
)
self._props["width"] = str(width)

# Retain a strong reference to the (processed) item so a wrapped UI
# element is not garbage collected. The UI element registry holds
# elements weakly and the slotted HTML only freezes their text, so
# without this the item -- and, for the list form, the vstack wrapping
# it -- would be collected and lose interactivity.
super().__init__([as_html(item)])

def _build_text(self) -> str:
return build_stateless_plugin(
"marimo-sidebar",
self._props,
self._children[0].text,
)

# Not supported
Expand Down
42 changes: 25 additions & 17 deletions marimo/_plugins/stateless/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@

from marimo._output.builder import h
from marimo._output.formatting import as_dom_node
from marimo._output.hypertext import Html
from marimo._output.hypertext import ContainerHtml
from marimo._output.rich_help import mddoc


@mddoc
def style(
item: object, style: dict[str, Any] | None = None, **kwargs: Any
) -> Html:
class style(ContainerHtml):
"""Wrap an object in a styled container.

Example:
Expand All @@ -33,16 +31,26 @@ def style(
Html: An HTML object representing the item wrapped in a div
with the specified styles.
"""
# Initialize combined_style with style dict if provided,
# otherwise empty dict
combined_style = style or {}

# Add kwargs to combined_style, converting snake_case to kebab-case
for key, value in kwargs.items():
kebab_key = key.replace("_", "-")
combined_style[kebab_key] = value

style_str = ";".join(
[f"{key}:{value}" for key, value in combined_style.items()]
)
return Html(h.div(children=as_dom_node(item).text, style=style_str))

def __init__(
self,
item: object,
style: dict[str, Any] | None = None,
**kwargs: Any,
) -> None:
# Initialize combined_style with style dict if provided,
# otherwise empty dict
combined_style = style or {}

# Add kwargs to combined_style, converting snake_case to kebab-case
for key, value in kwargs.items():
kebab_key = key.replace("_", "-")
combined_style[kebab_key] = value

self._style_str = ";".join(
[f"{key}:{value}" for key, value in combined_style.items()]
)
super().__init__([as_dom_node(item)])

def _build_text(self) -> str:
return h.div(children=self._children[0].text, style=self._style_str)
21 changes: 15 additions & 6 deletions marimo/_plugins/ui/_impl/tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import TYPE_CHECKING, Final, Literal

from marimo._output.formatting import as_html
from marimo._output.hypertext import Html
from marimo._output.md import md
from marimo._output.rich_help import mddoc
from marimo._plugins.stateless.lazy import lazy as lazy_ui
Expand Down Expand Up @@ -78,17 +79,25 @@ def __init__(
label: str = "",
on_change: Callable[[str], None] | None = None,
) -> None:
def render_content(tab: object) -> str:
def render_content(tab: object) -> Html:
if lazy:
return lazy_ui(tab).text
return lazy_ui(tab)
if isinstance(tab, str):
return md(tab).text
return as_html(tab).text
return md(tab)
return as_html(tab)

# Retain strong references to the rendered tab contents. The slotted
# HTML only freezes their text, and the UI element registry holds
# elements weakly, so without this a UI element placed in a tab would
# be garbage collected and lose its interactivity.
self._children: list[Html] = [
render_content(tab) for tab in tabs.values()
]

tab_items = "".join(
[
"<div data-kind='tab'>" + render_content(tab) + "</div>"
for tab in tabs.values()
"<div data-kind='tab'>" + child.text + "</div>"
for child in self._children
]
)

Expand Down
Loading
Loading