瀏覽代碼

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 月之前
父節點
當前提交
67025a54d7

+ 14 - 1
nicegui/app/app.py

@@ -5,7 +5,7 @@ import signal
 import urllib
 import urllib
 from enum import Enum
 from enum import Enum
 from pathlib import Path
 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 import FastAPI, HTTPException, Request, Response
 from fastapi.responses import FileResponse
 from fastapi.responses import FileResponse
@@ -258,3 +258,16 @@ class App(FastAPI):
         self._connect_handlers.clear()
         self._connect_handlers.clear()
         self._disconnect_handlers.clear()
         self._disconnect_handlers.clear()
         self._exception_handlers[:] = [log.exception]
         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 fastapi.responses import PlainTextResponse
 from selenium.webdriver.common.by import By
 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
 from nicegui.testing import Screen
 
 
 
 
@@ -322,3 +322,22 @@ def test_ip(screen: Screen):
 
 
     screen.open('/')
     screen.open('/')
     screen.should_contain('127.0.0.1')
     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')
     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)
 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)
     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', '''
 @doc.demo('Modularize with APIRouter', '''
     You can use the NiceGUI specialization of
     You can use the NiceGUI specialization of
     [FastAPI's APIRouter](https://fastapi.tiangolo.com/tutorial/bigger-applications/?h=apirouter#apirouter)
     [FastAPI's APIRouter](https://fastapi.tiangolo.com/tutorial/bigger-applications/?h=apirouter#apirouter)