Forráskód Böngészése

Multicasting of page-scoped content (#3968)

PR for https://github.com/zauberzeug/nicegui/discussions/3828

As discussed there added an iterator to `app` for yielding clients based
on path. Also added documentation (at Page and ElementFilter since it
kinda applies to both of them). Added a test.

I was also experimenting a bit with adding a wrapper or context manager
as in my initial example, but I think @falkoschindler is right: an
iterator is cleaner and then the developer can just do what they want
with it.

E.g., in my original example
```python
    def update_clock():
        def _update():
            for element in ElementFilter(kind=ui.label, marker='clock'):
                element.text = time.strftime('%H:%M:%S')
        with_clients(OTHER_PAGE_ROUTE, _update)
```
versus now
```python
    def update_clock():
        for client in app.clients(OTHER_PAGE_ROUTE):
            with client.content:
                for element in ElementFilter(kind=ui.label, marker='clock'):
                    element.text = time.strftime('%H:%M:%S')
```

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
aranvir 6 hónapja
szülő
commit
67025a54d7

+ 14 - 1
nicegui/app/app.py

@@ -5,7 +5,7 @@ import signal
 import urllib
 from enum import Enum
 from pathlib import Path
-from typing import Any, Awaitable, Callable, List, Optional, Union
+from typing import Any, Awaitable, Callable, Iterator, List, Optional, Union
 
 from fastapi import FastAPI, HTTPException, Request, Response
 from fastapi.responses import FileResponse
@@ -258,3 +258,16 @@ class App(FastAPI):
         self._connect_handlers.clear()
         self._disconnect_handlers.clear()
         self._exception_handlers[:] = [log.exception]
+
+    @staticmethod
+    def clients(path: str) -> Iterator[Client]:
+        """Iterate over all connected clients with a matching path.
+
+        When using `@ui.page("/path")` each client gets a private view of this page.
+        Updates must be sent to each client individually, which this iterator simplifies.
+
+        :param path: string to filter clients by
+        """
+        for client in Client.instances.values():
+            if client.page.path == path:
+                yield client

+ 20 - 1
tests/test_page.py

@@ -6,7 +6,7 @@ from uuid import uuid4
 from fastapi.responses import PlainTextResponse
 from selenium.webdriver.common.by import By
 
-from nicegui import background_tasks, ui
+from nicegui import app, background_tasks, ui
 from nicegui.testing import Screen
 
 
@@ -322,3 +322,22 @@ def test_ip(screen: Screen):
 
     screen.open('/')
     screen.should_contain('127.0.0.1')
+
+
+def test_multicast(screen: Screen):
+    def update():
+        for client in app.clients('/'):
+            with client:
+                ui.label('added')
+
+    @ui.page('/')
+    def page():
+        ui.button('add label', on_click=update)
+
+    screen.open('/')
+    screen.switch_to(1)
+    screen.open('/')
+    screen.click('add label')
+    screen.should_contain('added')
+    screen.switch_to(0)
+    screen.should_contain('added')

+ 21 - 0
website/documentation/content/element_filter_documentation.py

@@ -60,4 +60,25 @@ def marker_demo() -> None:
     ElementFilter(marker='red strong', local_scope=True).classes('bg-red-600 text-white')
 
 
+@doc.demo('Find elements on other pages', '''
+    You can use the `app.clients` iterator to apply the element filter to all clients of a specific page.
+''')
+def multicasting():
+    from nicegui import app
+    import time
+
+    @ui.page('/log')
+    def page():
+        ui.log()
+
+    def log_time():
+        for client in app.clients('/log'):
+            with client:
+                for log in ElementFilter(kind=ui.log):
+                    log.push(f'{time.strftime("%H:%M:%S")}')
+
+    ui.button('Log current time', on_click=log_time)
+    ui.link('Open log', '/log', new_tab=True)
+
+
 doc.reference(ElementFilter)

+ 21 - 0
website/documentation/content/page_documentation.py

@@ -52,6 +52,27 @@ def wait_for_connected_demo():
     ui.link('wait for connection', wait_for_connection)
 
 
+@doc.demo('Multicasting', '''
+    The content on a page is private to the client (the browser tab) and has its own local element context.
+    If you want to send updates to _all_ clients of a specific page, you can use the `app.clients` iterator.
+    This is useful for modifying UI elements from a background process or from other pages.
+''')
+def multicasting():
+    from nicegui import app
+
+    @ui.page('/multicast_receiver')
+    def page():
+        ui.label('This page will show messages from the index page.')
+
+    def send(message: str):
+        for client in app.clients('/multicast_receiver'):
+            with client:
+                ui.notify(message)
+
+    ui.button('Send message', on_click=lambda: send('Hi!'))
+    ui.link('Open receiver', '/multicast_receiver', new_tab=True)
+
+
 @doc.demo('Modularize with APIRouter', '''
     You can use the NiceGUI specialization of
     [FastAPI's APIRouter](https://fastapi.tiangolo.com/tutorial/bigger-applications/?h=apirouter#apirouter)