ソースを参照

Merge pull request #1447 from zauberzeug/rx-use-state

Alternative implementation for ui.use_state
Falko Schindler 1 年間 前
コミット
b5ee25f91f

+ 32 - 4
nicegui/functions/refreshable.py

@@ -1,5 +1,7 @@
-from dataclasses import dataclass
-from typing import Any, Awaitable, Callable, Dict, List, Tuple, Union
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any, Awaitable, Callable, ClassVar, Dict, List, Optional, Tuple, Union
 
 from typing_extensions import Self
 
@@ -11,13 +13,20 @@ from ..helpers import is_coroutine_function
 
 @dataclass(**KWONLY_SLOTS)
 class RefreshableTarget:
-    container: Element
+    container: RefreshableContainer
+    refreshable: refreshable
     instance: Any
     args: Tuple[Any, ...]
     kwargs: Dict[str, Any]
 
+    current_target: ClassVar[Optional[RefreshableTarget]] = None
+    locals: List[Any] = field(default_factory=list)
+    next_index: int = 0
+
     def run(self, func: Callable[..., Any]) -> Union[None, Awaitable]:
         """Run the function and return the result."""
+        RefreshableTarget.current_target = self
+        self.next_index = 0
         # pylint: disable=no-else-return
         if is_coroutine_function(func):
             async def wait_for_result() -> None:
@@ -67,7 +76,8 @@ class refreshable:
 
     def __call__(self, *args: Any, **kwargs: Any) -> Union[None, Awaitable]:
         self.prune()
-        target = RefreshableTarget(container=RefreshableContainer(), instance=self.instance, args=args, kwargs=kwargs)
+        target = RefreshableTarget(container=RefreshableContainer(), refreshable=self, instance=self.instance,
+                                   args=args, kwargs=kwargs)
         self.targets.append(target)
         return target.run(self.func)
 
@@ -106,3 +116,21 @@ class refreshable:
             for target in self.targets
             if target.container.client.id in globals.clients and target.container.id in target.container.client.elements
         ]
+
+
+def state(value: Any) -> Tuple[Any, Callable[[Any], None]]:
+    target = RefreshableTarget.current_target
+    assert target is not None
+
+    if target.next_index >= len(target.locals):
+        target.locals.append(value)
+    else:
+        value = target.locals[target.next_index]
+
+    def set_value(new_value: Any, index=target.next_index) -> None:
+        target.locals[index] = new_value
+        target.refreshable.refresh()
+
+    target.next_index += 1
+
+    return value, set_value

+ 2 - 1
nicegui/ui.py

@@ -85,6 +85,7 @@ __all__ = [
     'notify',
     'open',
     'refreshable',
+    'state',
     'update',
     'page',
     'drawer',
@@ -181,7 +182,7 @@ from .functions.html import add_body_html, add_head_html
 from .functions.javascript import run_javascript
 from .functions.notify import notify
 from .functions.open import open  # pylint: disable=redefined-builtin
-from .functions.refreshable import refreshable
+from .functions.refreshable import refreshable, state
 from .functions.update import update
 from .page import page
 from .page_layout import Drawer as drawer

+ 23 - 0
tests/test_refreshable.py

@@ -180,3 +180,26 @@ def test_refresh_with_function_reference(screen: Screen):
     screen.should_contain('Refreshing A')
     screen.click('B')
     screen.should_contain('Refreshing B')
+
+
+def test_refreshable_with_state(screen: Screen):
+    @ui.refreshable
+    def counter(title: str):
+        count, set_count = ui.state(0)
+        ui.label(f'{title}: {count}')
+        ui.button(f'Increment {title}', on_click=lambda: set_count(count + 1))
+
+    counter('A')
+    counter('B')
+
+    screen.open('/')
+    screen.should_contain('A: 0')
+    screen.should_contain('B: 0')
+
+    screen.click('Increment A')
+    screen.should_contain('A: 1')
+    screen.should_contain('B: 0')
+
+    screen.click('Increment B')
+    screen.should_contain('A: 1')
+    screen.should_contain('B: 1')

+ 20 - 0
website/more_documentation/refreshable_documentation.py

@@ -67,3 +67,23 @@ def more() -> None:
                         ui.label(rule).classes('text-xs text-red')
 
         show_info()
+
+    @text_demo('Refreshable UI with reactive state', '''
+        You can create reactive state variables with the `ui.state` function, like `count` and `color` in this demo.
+        They can be used like normal variables for creating UI elements like the `ui.label`.
+        Their corresponding setter functions can be used to set new values, which will automatically refresh the UI.
+    ''')
+    def reactive_state():
+        @ui.refreshable
+        def counter(name: str):
+            with ui.card():
+                count, set_count = ui.state(0)
+                color, set_color = ui.state('black')
+                ui.label(f'{name} = {count}').classes(f'text-{color}')
+                ui.button(f'{name} += 1', on_click=lambda: set_count(count + 1))
+                ui.select(['black', 'red', 'green', 'blue'],
+                          value=color, on_change=lambda e: set_color(e.value))
+
+        with ui.row():
+            counter('A')
+            counter('B')