Skip to content

Commit

Permalink
Merge pull request #1447 from zauberzeug/rx-use-state
Browse files Browse the repository at this point in the history
Alternative implementation for ui.use_state
  • Loading branch information
falkoschindler authored Oct 19, 2023
2 parents 40257cd + 5630067 commit b5ee25f
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 5 deletions.
36 changes: 32 additions & 4 deletions nicegui/functions/refreshable.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -11,13 +13,20 @@

@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:
Expand Down Expand Up @@ -67,7 +76,8 @@ def refresh(*args: Any, _instance=self.instance, **kwargs: Any) -> None:

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)

Expand Down Expand Up @@ -106,3 +116,21 @@ def prune(self) -> None:
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
3 changes: 2 additions & 1 deletion nicegui/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
'notify',
'open',
'refreshable',
'state',
'update',
'page',
'drawer',
Expand Down Expand Up @@ -181,7 +182,7 @@
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
Expand Down
23 changes: 23 additions & 0 deletions tests/test_refreshable.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,26 @@ def ui(self):
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 changes: 20 additions & 0 deletions website/more_documentation/refreshable_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,23 @@ def show_info():
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')

0 comments on commit b5ee25f

Please sign in to comment.