Skip to content

Commit 92f37e7

Browse files
committed
Pass RAGOT virtual scroll load context through SPRAG
Forward Ragot VirtualScroller's loadCtx argument into SPRAG chunk callbacks so async virtual-scroll chunks can detect stale loads safely. Document the current SPRAG-to-Ragot virtual-scroll callback mapping, including measureChunk/buildPlaceholder/onRecycle/onChunkEvicted and horizontal measurement behavior. Add a regression test covering horizontal virtual-scroll codegen with async chunk callbacks and optional Ragot callback hooks.
1 parent 881ff7f commit 92f37e7

5 files changed

Lines changed: 60 additions & 7 deletions

File tree

docs/app/content/docs/ragot/decorators.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,20 @@ The decorated component must define:
120120
- `chunk(self, i)` — returns the DOM element for chunk `i`
121121
- `total(self)` — returns the total item count
122122

123-
Optional methods: `measure`, `placeholder`, `recycle` (required if `pool_size > 0`), `evicted`.
123+
`chunk` may also accept Ragot's load context as a second argument:
124+
`chunk(self, i, load_ctx)`. This is useful for async chunks; call
125+
`load_ctx.is_current()` before committing delayed work.
126+
127+
Optional methods map directly to Ragot `VirtualScroller` callbacks:
128+
129+
| SPRAG method | Ragot option | Use |
130+
|---|---|---|
131+
| `measure(self, el, i)` | `measureChunk(el, i)` | Return the rendered chunk size in pixels |
132+
| `placeholder(self, i, px)` | `buildPlaceholder(i, px)` | Return the placeholder element for an evicted chunk |
133+
| `recycle(self, el, i)` | `onRecycle(el, i)` | Fully patch a pooled chunk element for its new index |
134+
| `evicted(self, i)` | `onChunkEvicted(i)` | React after a chunk leaves the active window |
135+
136+
Ragot's default measurement uses the chunk element's height. For horizontal virtual scrolling, define `measure(self, el, i)` and return the chunk width, for example `el.offset_width`.
124137

125138
The scroller handle is available at `self.virtual_scroll` in Python (emitted as `this.virtualScroll` in JS).
126139

sprag/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Public SPRAG framework surface."""
22

3-
__version__ = "0.1.13"
3+
__version__ = "0.1.14"
44

55
from .runtime import dom
66
from .runtime.app import App

sprag/dev/codegen/components.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ def _emit_virtual_scroll_setup(cfg: dict, component_class) -> list[str]:
779779
f"initialChunks: {cfg['initial_chunks']}",
780780
f"rootMargin: {json.dumps(cfg['root_margin'])}",
781781
f"axis: {json.dumps(cfg['axis'])}",
782-
"renderChunk: (i) => this.chunk(i)",
782+
"renderChunk: (i, loadCtx) => this.chunk(i, loadCtx)",
783783
"totalItems: () => this.total()",
784784
]
785785
if cfg["root"]:

sprag/runtime/browser.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,17 @@ def virtual_scroll(
114114
115115
The component must define:
116116
117-
- ``chunk(self, i)`` — returns the DOM element for chunk ``i``
117+
- ``chunk(self, i)`` — returns the DOM element for chunk ``i``. It may
118+
also accept Ragot's load context as ``chunk(self, i, load_ctx)`` for
119+
async stale-load checks.
118120
- ``total(self)`` — returns the total item count
119121
120-
Optional methods: ``measure``, ``placeholder``, ``recycle`` (REQUIRED if
121-
``pool_size > 0``), ``evicted``.
122+
Optional methods map to Ragot callbacks: ``measure`` ->
123+
``measureChunk``, ``placeholder`` -> ``buildPlaceholder``, ``recycle`` ->
124+
``onRecycle`` (REQUIRED if ``pool_size > 0``), and ``evicted`` ->
125+
``onChunkEvicted``. Ragot's default measurement is height-based; for
126+
horizontal scrollers, define ``measure(self, el, i)`` and return the
127+
chunk width.
122128
123129
Decorated components get a public scroller handle at
124130
``self.virtual_scroll`` in Python authoring code (emitted as

tests/test_codegen_diagnostics.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import json
33
import inspect
44

5-
from sprag import Component, Module, browser, imports, ui
5+
from sprag import Component, Module, browser, imports, ui, virtual_scroll
66
from sprag.dev.codegen.components import compile_component_artifact, compile_component_class
77
from sprag.dev.codegen.modules import compile_module_artifact, compile_module_class
88
from sprag.dev.codegen.mappings import JSCodegenError
@@ -99,6 +99,29 @@ def render(self, props=None):
9999
)
100100

101101

102+
@virtual_scroll(chunk=20, axis="horizontal", root=".rail", container_class="rail-inner")
103+
class VirtualScrollCallbacksComponent(Component):
104+
def render(self, props=None):
105+
return ui.div(class_="rail")
106+
107+
def total(self):
108+
return 1000
109+
110+
async def chunk(self, i, load_ctx):
111+
if load_ctx and not load_ctx.is_current():
112+
return None
113+
return ui.div("chunk " + str(i), class_="chunk")
114+
115+
def measure(self, el, i):
116+
return el.offset_width
117+
118+
def placeholder(self, i, px):
119+
return ui.div(class_="placeholder")
120+
121+
def evicted(self, i):
122+
return i
123+
124+
102125
class InvalidComponentSubscription(Component):
103126
def render(self, props=None):
104127
return ui.button("Subscribe")
@@ -197,6 +220,17 @@ def test_compile_component_supports_set_metadata_helper(self):
197220
self.assertIn("window.__SPRAG_SET_METADATA__", compiled)
198221
self.assertIn('this.setMetadata({ "og:title": "Updated OG title" });', compiled)
199222

223+
def test_compile_component_supports_virtual_scroll_callbacks(self):
224+
compiled = compile_component_class(VirtualScrollCallbacksComponent)
225+
self.assertIn("VirtualScroller", compiled)
226+
self.assertIn('axis: "horizontal"', compiled)
227+
self.assertIn("renderChunk: (i, loadCtx) => this.chunk(i, loadCtx)", compiled)
228+
self.assertIn("measureChunk: (el, i) => this.measure(el, i)", compiled)
229+
self.assertIn("buildPlaceholder: (i, px) => this.placeholder(i, px)", compiled)
230+
self.assertIn("onChunkEvicted: (i) => this.evicted(i)", compiled)
231+
self.assertIn("async chunk(i, load_ctx)", compiled)
232+
self.assertIn("load_ctx.isCurrent()", compiled)
233+
200234
def test_compile_module_artifact_emits_source_map_metadata(self):
201235
artifact = compile_module_artifact(SupportedModule)
202236
payload = json.loads(artifact.source_map)

0 commit comments

Comments
 (0)