This is the structure of my app.
from __future__ import annotations
import asyncio
import sys
from typing import Any
import param
import panel as pn
import panel_material_ui as pmui
pn.extension(
throttled=True,
notifications=True,
disconnect_notification="ERROR: Connection lost, try reloading the page...",
)
class FeedItem(pn.viewable.Viewer):
"""Panel view for one selectable group."""
visible = param.Boolean(default=False, doc="Whether this event has been loaded.")
def __init__(self, item_id: int, **params: Any) -> None:
"""Create a test event list item."""
self._item_id = item_id
with pn.config.set(sizing_mode="stretch_width"):
self._button = pmui.MenuButton(
label=f"Open item {self._item_id}",
items=["thing1", "thing2", "thing3"],
sizing_mode=None,
align="center",
disable_elevation=True,
)
self._button_row = pn.Row(
self._button,
sizing_mode="stretch_width",
)
self._details = pmui.FlexBox(
align_items="flex-start",
flex_wrap="wrap",
gap="24px",
min_height=160,
sizing_mode="stretch_width",
styles={
"border-top": "1px solid #d9d9d9",
"border-bottom": "1px solid #d9d9d9",
"padding": "8px 0",
},
)
self._layout = pn.Column(
self._button_row,
self._details,
sizing_mode="stretch_width",
margin=(0, 0, 8, 0),
)
super().__init__(**params)
@param.depends("visible", watch=True)
async def on_visible(self) -> None:
"""Hook for loading additional data when the item becomes visible."""
self._details.loading = True
# Doing "some slow requests here"
await asyncio.sleep(2)
self._details.loading = False
# Formatting the results of my requests here
self._details.objects = [
pn.pane.Markdown("# Detail 1\n\nSome stuff"),
pn.pane.Markdown("# Detail 2\n\nSome more stuff"),
]
def __panel__(self) -> pn.Column:
"""Return the Panel representation of this test event."""
return self._layout
class FeedList(pn.viewable.Viewer):
"""Feed layout containing selectable test events."""
def __init__(self, items: list[TestEventItem], **params: Any) -> None:
"""Create the test event feed."""
self._items = items
self._layout = pn.Column(scroll=False)
self._feed = pmui.layout.Feed(
*self._items,
load_buffer=3,
scroll=False,
min_height=500,
sizing_mode="stretch_width",
scroll_position=0,
view_latest=False,
)
super().__init__(**params)
self._layout.objects = [self._feed]
self._feed.objects = self._items
@param.depends("_feed.visible_range", watch=True)
def _visible_range_changed(self) -> None:
"""Run item hooks for visible feed objects."""
start, end = self._feed.visible_range
# visible range is inclusive so need to be careful to add one to end
visible_items = self._items[start : end + 1]
print(
"len(items)=",
len(self._items),
"visible",
len(visible_items),
f"{start=} {end=}",
)
count = 0
for item in visible_items:
if not item.visible:
item.visible = True
count += 1
print(f"toggled {count=}")
def __panel__(self):
"""Return the Panel representation of the test event list."""
return self._layout
class TestApp(pn.viewable.Viewer):
def __init__(
self,
**params: Any,
) -> None:
self._button = pn.widgets.Button(label="Open Feed")
self._main_content = pn.Column(
self._button, sizing_mode="stretch_width", scroll=False
)
self._template = pn.template.MaterialTemplate(
title="TestApp",
main=[self._main_content],
)
super().__init__(**params)
self._button.on_click(self.load_feed)
def load_feed(self, *args) -> None:
if self._button in self._main_content.objects:
items = [FeedItem(i) for i in range(50)]
self._main_content.objects = [FeedList(items)]
def __panel__(self) -> pn.template.MaterialTemplate:
return self._template
@classmethod
def create_app(cls) -> Buoy:
return cls()
if pn.state.served:
TestApp.create_app().servable()