Repository: zauberzeug/nicegui
Pull request #1775 (2023-10-09 08:15:29)

Refresh binding before page delivery
Following up on the discussion #1561 with @laserir, this PR adds another binding refresh step right before generating the page response. This fixes this minimal example:

```py
state = {'count': 0}

@ui.page('/')
def main_page() -> None:
    ui.label().bind_text_from(state, 'count')
    state['count'] += 1
    print(state)  # Output: 1, but label still displays 0
```


Patch:
diff --git a/nicegui/binding.py b/nicegui/binding.py
index 580386e3..14728581 100644
--- a/nicegui/binding.py
+++ b/nicegui/binding.py
@@ -32,24 +32,28 @@ def set_attribute(obj: Union[object, Mapping], name: str, value: Any) -> None:
         setattr(obj, name, value)
 
 
-async def loop() -> None:
+async def refresh_loop() -> None:
     while True:
-        visited: Set[Tuple[int, str]] = set()
-        t = time.time()
-        for link in active_links:
-            (source_obj, source_name, target_obj, target_name, transform) = link
-            if has_attribute(source_obj, source_name):
-                value = transform(get_attribute(source_obj, source_name))
-                if not has_attribute(target_obj, target_name) or get_attribute(target_obj, target_name) != value:
-                    set_attribute(target_obj, target_name, value)
-                    propagate(target_obj, target_name, visited)
-            del link, source_obj, target_obj  # pylint: disable=modified-iterating-list
-        if time.time() - t > MAX_PROPAGATION_TIME:
-            globals.log.warning(
-                f'binding propagation for {len(active_links)} active links took {time.time() - t:.3f} s')
+        _refresh_step()
         await asyncio.sleep(globals.binding_refresh_interval)
 
 
+def _refresh_step():
+    visited: Set[Tuple[int, str]] = set()
+    t = time.time()
+    for link in active_links:
+        (source_obj, source_name, target_obj, target_name, transform) = link
+        if has_attribute(source_obj, source_name):
+            value = transform(get_attribute(source_obj, source_name))
+            if not has_attribute(target_obj, target_name) or get_attribute(target_obj, target_name) != value:
+                set_attribute(target_obj, target_name, value)
+                propagate(target_obj, target_name, visited)
+        del link, source_obj, target_obj  # pylint: disable=modified-iterating-list
+    if time.time() - t > MAX_PROPAGATION_TIME:
+        globals.log.warning(
+            f'binding propagation for {len(active_links)} active links took {time.time() - t:.3f} s')
+
+
 def propagate(source_obj: Any, source_name: str, visited: Optional[Set[Tuple[int, str]]] = None) -> None:
     if visited is None:
         visited = set()
diff --git a/nicegui/nicegui.py b/nicegui/nicegui.py
index 1b6b79c3..c4d1910d 100644
--- a/nicegui/nicegui.py
+++ b/nicegui/nicegui.py
@@ -91,7 +91,7 @@ def handle_startup(with_welcome_message: bool = True) -> None:
     with globals.index_client:
         for t in globals.startup_handlers:
             safe_invoke(t)
-    background_tasks.create(binding.loop(), name='refresh bindings')
+    background_tasks.create(binding.refresh_loop(), name='refresh bindings')
     background_tasks.create(outbox.loop(), name='send outbox')
     background_tasks.create(prune_clients(), name='prune clients')
     background_tasks.create(prune_slot_stacks(), name='prune slot stacks')
diff --git a/nicegui/page.py b/nicegui/page.py
index d9be928f..58ca092c 100644
--- a/nicegui/page.py
+++ b/nicegui/page.py
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, Callable, Optional, Union
 
 from fastapi import Request, Response
 
-from . import background_tasks, globals  # pylint: disable=redefined-builtin
+from . import background_tasks, binding, globals  # pylint: disable=redefined-builtin
 from .client import Client
 from .favicon import create_favicon_route
 from .language import Language
@@ -100,6 +100,7 @@ class page:
                 result = task.result() if task.done() else None
             if isinstance(result, Response):  # NOTE if setup returns a response, we don't need to render the page
                 return result
+            binding._refresh_step()  # pylint: disable=protected-access
             return client.build_response(request)
 
         parameters = [p for p in inspect.signature(func).parameters.values() if p.name != 'client']


Test patch:
diff --git a/tests/test_binding.py b/tests/test_binding.py
index f3a3c87d..fe383675 100644
--- a/tests/test_binding.py
+++ b/tests/test_binding.py
@@ -1,4 +1,3 @@
-
 from selenium.webdriver.common.keys import Keys
 
 from nicegui import ui
@@ -83,3 +82,15 @@ def test_binding_to_input(screen: Screen):
     element.value = 'five'
     screen.should_contain_input('five')
     assert data.text == 'five'
+
+
+def test_binding_refresh_before_page_delivery(screen: Screen):
+    state = {'count': 0}
+
+    @ui.page('/')
+    def main_page() -> None:
+        ui.label().bind_text_from(state, 'count')
+        state['count'] += 1
+
+    screen.open('/')
+    screen.should_contain('1')