瀏覽代碼

Allow yield in page to exc code after its loaded

Rodja Trappe 2 年之前
父節點
當前提交
c27ad191a4
共有 4 個文件被更改,包括 75 次插入16 次删除
  1. 20 1
      api_docs_and_examples.py
  2. 11 11
      examples/map/main.py
  3. 16 4
      nicegui/page.py
  4. 28 0
      tests/test_pages.py

+ 20 - 1
api_docs_and_examples.py

@@ -419,7 +419,7 @@ You can run a function or coroutine as a parallel task by passing it to one of t
 - `ui.on_startup`: Called when NiceGUI is started or restarted.
 - `ui.on_shutdown`: Called when NiceGUI is shut down or restarted.
 - `ui.on_connect`: Called when a client connects to NiceGUI. (Optional argument: Starlette request)
-- `ui.on_page_ready`: Called when the page is ready and the websocket is connected. (Optional argument: socket)
+- `ui.on_page_ready`: Called when the page is ready and the websocket is connected (Optional argument: socket). See [Yield for Page Ready](#yield_for_page_ready) as an alternative.
 - `ui.on_disconnect`: Called when a client disconnects from NiceGUI. (Optional argument: socket)
 
 When NiceGUI is shut down or restarted, the startup tasks will be automatically canceled.
@@ -581,6 +581,25 @@ To make it "private" or to change other attributes like title, favicon etc. you
         ui.link('private page', private_page)
         ui.link('shared page', shared_page)
 
+    yield_page_ready = '''#### Yield for Page Ready
+
+This is an handy alternative to using the `on_page_ready` callback (either as parameter of `@ui.page` or via `ui.on_page_ready` function).
+
+If a `yield` statement is provided in a page builder function, all code below that statement is executed after the page is ready.
+This allows you to execute JavaScript; which is only possible after the page has been loaded (see [#112](https://github.com/zauberzeug/nicegui/issues/112)).
+Also it's possible to do async stuff while the user alreay sees the content added before the yield statement.
+    '''
+    with example(yield_page_ready):
+        @ui.page('/yield_page_ready')
+        async def yield_page_ready():
+            ui.label('This text is displayed immediately.')
+            yield
+            ui.run_javascript('document.title = "JavaScript Controlled Title")')
+            await asyncio.sleep(3)
+            ui.label('This text is displayed 3 seconds after the page has been fully loaded.')
+
+        ui.link('show page ready code after yield', '/yield_page_ready')
+
     with example(ui.open):
         @ui.page('/yet_another_page')
         def yet_another_page():

+ 11 - 11
examples/map/main.py

@@ -4,21 +4,21 @@ from nicegui import ui
 
 import leaflet
 
-locations = {
-    (52.5200, 13.4049): 'Berlin',
-    (40.7306, -74.0060): 'New York',
-    (39.9042, 116.4074): 'Beijing',
-    (35.6895, 139.6917): 'Tokyo',
-}
-selection = None
 
-
-@ui.page('/', on_page_ready=lambda: selection.set_value(next(iter(locations))))
+@ui.page('/')
 def main_page():
-    # NOTE we need to use the on_page_ready event to make sure the page is loaded before we execute javascript
-    global selection
     map = leaflet.map()
+    locations = {
+        (52.5200, 13.4049): 'Berlin',
+        (40.7306, -74.0060): 'New York',
+        (39.9042, 116.4074): 'Beijing',
+        (35.6895, 139.6917): 'Tokyo',
+    }
     selection = ui.select(locations, on_change=map.set_location).style('width: 10em')
+    yield  # all code below is executed after page is ready
+    default_location = next(iter(locations))
+    # this will trigger the map.set_location event; which is js and must be run after page is ready
+    selection.set_value(default_location)
 
 
 ui.run()

+ 16 - 4
nicegui/page.py

@@ -3,6 +3,7 @@ from __future__ import annotations
 import asyncio
 import inspect
 import time
+import types
 import uuid
 from functools import wraps
 from typing import Callable, Dict, Optional
@@ -45,6 +46,7 @@ class Page(jp.QuasarPage):
         self.css = css
         self.connect_handler = on_connect
         self.page_ready_handler = on_page_ready
+        self.page_ready_generator: types.GeneratorType = None
         self.disconnect_handler = on_disconnect
         self.delete_flag = not shared
 
@@ -68,8 +70,13 @@ class Page(jp.QuasarPage):
         return self
 
     async def handle_page_ready(self, msg: AdDict) -> bool:
-        if self.page_ready_handler:
-            with globals.within_view(self.view):
+        with globals.within_view(self.view):
+            if self.page_ready_generator is not None:
+                if isinstance(self.page_ready_generator, types.AsyncGeneratorType):
+                    await self.page_ready_generator.__anext__()
+                elif isinstance(self.page_ready_generator, types.GeneratorType):
+                    next(self.page_ready_generator)
+            if self.page_ready_handler:
                 arg_count = len(inspect.signature(self.page_ready_handler).parameters)
                 is_coro = is_coroutine(self.page_ready_handler)
                 if arg_count == 1:
@@ -159,7 +166,7 @@ class page:
         :param classes: tailwind classes for the container div (default: `'q-ma-md column items-start gap-4'`)
         :param css: CSS definitions
         :param on_connect: optional function or coroutine which is called for each new client connection
-        :param on_page_ready: optional function or coroutine which is called when the websocket is connected
+        :param on_page_ready: optional function or coroutine which is called when the websocket is connected;  see `"Yield for Page Ready" <https://nicegui.io/#yield_for_page_ready>`_ as an alternative.
         :param on_disconnect: optional function or coroutine which is called when a client disconnects
         :param shared: whether the page instance is shared between multiple clients (default: `False`)
         """
@@ -197,7 +204,12 @@ class page:
                     kwargs['request'] = request
                 await self.connected(request)
                 await self.header()
-                await func(*args, **kwargs) if is_coroutine(func) else func(*args, **kwargs)
+                result = await func(*args, **kwargs) if is_coroutine(func) else func(*args, **kwargs)
+                if isinstance(result, types.GeneratorType):
+                    next(result)
+                if isinstance(result, types.AsyncGeneratorType):
+                    await result.__anext__()
+                self.page.page_ready_generator = result
                 await self.footer()
             return self.page
         builder = PageBuilder(decorated, self.shared)

+ 28 - 0
tests/test_pages.py

@@ -1,4 +1,5 @@
 import asyncio
+from time import time
 from uuid import uuid4
 
 import justpy.htmlcomponents
@@ -185,3 +186,30 @@ def test_adding_elements_in_on_page_ready_event(screen: Screen):
 
     screen.open('/')
     screen.should_contain('Hello, world!')
+
+
+def test_pageready_after_yield_on_async_page(screen: Screen):
+    @ui.page('/')
+    async def page():
+        ui.label('before')
+        yield
+        await asyncio.sleep(1)
+        ui.label('after')
+
+    screen.open('/')
+    screen.should_contain('before')
+    screen.should_not_contain('after')
+    screen.wait(1)
+    screen.should_contain('after')
+
+
+def test_pageready_after_yield_on_non_async_page(screen: Screen):
+    @ui.page('/')
+    def page():
+        t = time()
+        yield
+        ui.label(f'loading page took {time() - t:.2f} seconds')
+
+    screen.open('/')
+    timing = screen.find('loading page took')
+    assert 0 < float(timing.text.split()[-2]) < 1