ex4nicegui 0.6.8__py3-none-any.whl → 0.6.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ex4nicegui/__init__.py CHANGED
@@ -6,6 +6,7 @@ from ex4nicegui.utils.types import (
6
6
  ReadonlyRef,
7
7
  TGetterOrReadonlyRef,
8
8
  )
9
+ from ex4nicegui.utils.scheduler import next_tick
9
10
  from ex4nicegui.utils.signals import (
10
11
  effect,
11
12
  effect_refreshable,
@@ -49,5 +50,6 @@ __all__ = [
49
50
  "to_raw",
50
51
  "is_setter_ref",
51
52
  "new_scope",
53
+ "next_tick",
52
54
  "__version__",
53
55
  ]
@@ -1,4 +1,4 @@
1
- from typing import Callable, TypeVar, Generic
1
+ from typing import Callable, Optional, TypeVar, Generic
2
2
  from nicegui import ui, Client
3
3
  from weakref import WeakKeyDictionary
4
4
 
@@ -16,16 +16,16 @@ class ClientInstanceLocker(Generic[_T]):
16
16
  self._client_instances: WeakKeyDictionary[Client, _T] = WeakKeyDictionary()
17
17
  self._factory = factory
18
18
 
19
- def get_object(self):
19
+ def get_object(self, client: Optional[Client] = None):
20
20
  if not ui.context.slot_stack:
21
21
  return None
22
22
 
23
- client = ui.context.client
23
+ client = client or ui.context.client
24
24
  if client not in self._client_instances:
25
25
  self._client_instances[client] = self._factory()
26
26
 
27
27
  @client.on_disconnect
28
28
  def _():
29
- del self._client_instances[client]
29
+ self._client_instances.pop(client, None)
30
30
 
31
31
  return self._client_instances[client]
@@ -51,7 +51,9 @@ from .officials.tab import TabBindableUi as tab
51
51
  from .officials.tab_panels import TabPanelsBindableUi as tab_panels
52
52
  from .officials.tab_panel import TabPanelBindableUi as tab_panel
53
53
  from .officials.element import ElementBindableUi as element
54
+ from .officials.tab_panels import LazyTabPanelsBindableUi as lazy_tab_panels
54
55
  from .q_pagination import PaginationBindableUi as q_pagination
56
+ from .officials.chip import ChipBindableUi as chip
55
57
 
56
58
  from .local_file_picker import local_file_picker
57
59
  from .UseDraggable.UseDraggable import use_draggable
@@ -70,6 +72,7 @@ pagination = q_pagination
70
72
  __all__ = [
71
73
  "element",
72
74
  "tab_panels",
75
+ "lazy_tab_panels",
73
76
  "tab_panel",
74
77
  "tabs",
75
78
  "tab",
@@ -120,4 +123,5 @@ __all__ = [
120
123
  "q_pagination",
121
124
  "pagination",
122
125
  "mermaid",
126
+ "chip",
123
127
  ]
@@ -0,0 +1,426 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import (
5
+ Any,
6
+ Callable,
7
+ Dict,
8
+ List,
9
+ Optional,
10
+ TypeVar,
11
+ Generic,
12
+ Union,
13
+ cast,
14
+ overload,
15
+ )
16
+
17
+ from typing_extensions import Self
18
+ from ex4nicegui.utils.apiEffect import ui_effect
19
+ from ex4nicegui.utils.signals import (
20
+ TGetterOrReadonlyRef,
21
+ to_value,
22
+ is_ref,
23
+ WatchedState,
24
+ on,
25
+ )
26
+ from ex4nicegui.utils.clientScope import new_scope
27
+ from nicegui import Tailwind, ui
28
+ from nicegui.elements.mixins.text_element import TextElement
29
+ from ex4nicegui.reactive.services.reactive_service import inject_handle_delete
30
+ from ex4nicegui.reactive.scopedStyle import ScopedStyle
31
+ from ex4nicegui.reactive.mixins.disableable import DisableableMixin
32
+ from functools import partial
33
+
34
+ T = TypeVar("T")
35
+
36
+
37
+ TWidget = TypeVar("TWidget", bound=ui.element)
38
+
39
+ _T_bind_classes_type_dict = Dict[str, TGetterOrReadonlyRef[bool]]
40
+ _T_bind_classes_type_ref_dict = TGetterOrReadonlyRef[Dict[str, bool]]
41
+ _T_bind_classes_type_single = TGetterOrReadonlyRef[str]
42
+ _T_bind_classes_type_array = List[_T_bind_classes_type_single]
43
+
44
+
45
+ _T_bind_classes_type = Union[
46
+ _T_bind_classes_type_dict,
47
+ _T_bind_classes_type_ref_dict,
48
+ _T_bind_classes_type_single,
49
+ _T_bind_classes_type_array,
50
+ ]
51
+
52
+
53
+ class BindableUi(Generic[TWidget]):
54
+ def __init__(self, element: TWidget) -> None:
55
+ self._element = element
56
+ inject_handle_delete(self.element, self._on_element_delete)
57
+ self.tailwind = Tailwind(cast(ui.element, self._element))
58
+ self._effect_scope = new_scope()
59
+ self.__used_scope_style = False
60
+
61
+ def _on_element_delete(self):
62
+ self._effect_scope.dispose()
63
+
64
+ if self.__used_scope_style:
65
+ scope_style = ScopedStyle.get()
66
+ if scope_style:
67
+ scope_style.remove_style(self.element)
68
+
69
+ @property
70
+ def _ui_effect(self):
71
+ return partial(ui_effect, scope=self._effect_scope)
72
+
73
+ @property
74
+ def _ui_signal_on(self):
75
+ return partial(on, scope=self._effect_scope)
76
+
77
+ def props(self, add: Optional[str] = None, *, remove: Optional[str] = None):
78
+ cast(ui.element, self.element).props(add, remove=remove)
79
+ return self
80
+
81
+ def classes(
82
+ self,
83
+ add: Optional[str] = None,
84
+ *,
85
+ remove: Optional[str] = None,
86
+ replace: Optional[str] = None,
87
+ ):
88
+ cast(ui.element, self.element).classes(add, remove=remove, replace=replace)
89
+ return self
90
+
91
+ def style(
92
+ self,
93
+ add: Optional[str] = None,
94
+ *,
95
+ remove: Optional[str] = None,
96
+ replace: Optional[str] = None,
97
+ ):
98
+ cast(ui.element, self.element).style(add, remove=remove, replace=replace)
99
+ return self
100
+
101
+ def __enter__(self) -> Self:
102
+ self.element.__enter__()
103
+ return self
104
+
105
+ def __exit__(self, *_):
106
+ self.element.default_slot.__exit__(*_)
107
+
108
+ def tooltip(self, text: str) -> Self:
109
+ cast(ui.element, self.element).tooltip(text)
110
+ return self
111
+
112
+ def add_slot(self, name: str, template: Optional[str] = None):
113
+ """Add a slot to the element.
114
+
115
+ :param name: name of the slot
116
+ :param template: Vue template of the slot
117
+ :return: the slot
118
+ """
119
+ return cast(ui.element, self.element).add_slot(name, template)
120
+
121
+ @property
122
+ def element(self):
123
+ return self._element
124
+
125
+ def delete(self) -> None:
126
+ """Delete the element."""
127
+ self.element.delete()
128
+
129
+ def move(
130
+ self, target_container: Optional[ui.element] = None, target_index: int = -1
131
+ ):
132
+ """Move the element to another container.
133
+
134
+ :param target_container: container to move the element to (default: the parent container)
135
+ :param target_index: index within the target slot (default: append to the end)
136
+ """
137
+ return self.element.move(target_container, target_index)
138
+
139
+ def remove(self, element: Union[ui.element, int]) -> None:
140
+ """Remove a child element.
141
+
142
+ :param element: either the element instance or its ID
143
+ """
144
+ return self.element.remove(element)
145
+
146
+ def bind_prop(self, prop: str, ref_ui: TGetterOrReadonlyRef[Any]):
147
+ """data binding is manipulating an element's property
148
+
149
+ @see - https://github.com/CrystalWindSnake/ex4nicegui/blob/main/README.en.md#bind_prop
150
+ @中文文档 - https://gitee.com/carson_add/ex4nicegui/tree/main/#bind_prop
151
+
152
+ Args:
153
+ prop (str): property name
154
+ ref_ui (TGetterOrReadonlyRef[Any]): a reference to the value to bind to
155
+
156
+ """
157
+ if prop == "visible":
158
+ return self.bind_visible(ref_ui)
159
+
160
+ if prop == "text" and isinstance(self.element, TextElement):
161
+
162
+ @self._ui_effect
163
+ def _():
164
+ cast(TextElement, self.element).set_text(to_value(ref_ui))
165
+ self.element.update()
166
+
167
+ @self._ui_effect
168
+ def _():
169
+ element = cast(ui.element, self.element)
170
+ element._props[prop] = to_value(ref_ui)
171
+ element.update()
172
+
173
+ return self
174
+
175
+ def bind_visible(self, ref_ui: TGetterOrReadonlyRef[bool]):
176
+ @self._ui_effect
177
+ def _():
178
+ element = cast(ui.element, self.element)
179
+ element.set_visibility(to_value(ref_ui))
180
+
181
+ return self
182
+
183
+ def bind_not_visible(self, ref_ui: TGetterOrReadonlyRef[bool]):
184
+ return self.bind_visible(lambda: not to_value(ref_ui))
185
+
186
+ def on(
187
+ self,
188
+ type: str,
189
+ handler: Optional[Callable[..., Any]] = None,
190
+ args: Optional[List[str]] = None,
191
+ *,
192
+ throttle: float = 0.0,
193
+ leading_events: bool = True,
194
+ trailing_events: bool = True,
195
+ ):
196
+ ele = cast(ui.element, self.element)
197
+ ele.on(
198
+ type,
199
+ handler,
200
+ args,
201
+ throttle=throttle,
202
+ leading_events=leading_events,
203
+ trailing_events=trailing_events,
204
+ )
205
+
206
+ return self
207
+
208
+ def clear(self) -> None:
209
+ cast(ui.element, self.element).clear()
210
+
211
+ @overload
212
+ def bind_classes(self, classes: Dict[str, TGetterOrReadonlyRef[bool]]):
213
+ ...
214
+
215
+ @overload
216
+ def bind_classes(self, classes: TGetterOrReadonlyRef[Dict[str, bool]]):
217
+ ...
218
+
219
+ @overload
220
+ def bind_classes(self, classes: List[TGetterOrReadonlyRef[str]]):
221
+ ...
222
+
223
+ @overload
224
+ def bind_classes(self, classes: TGetterOrReadonlyRef[str]):
225
+ ...
226
+
227
+ def bind_classes(self, classes: _T_bind_classes_type):
228
+ """data binding is manipulating an element's class list
229
+
230
+ @see - https://github.com/CrystalWindSnake/ex4nicegui/blob/main/README.en.md#bind-class-names
231
+ @中文文档 - https://gitee.com/carson_add/ex4nicegui/tree/main/#%E7%BB%91%E5%AE%9A%E7%B1%BB%E5%90%8D
232
+
233
+ Args:
234
+ classes (_T_bind_classes_type): dict of refs | ref to dict | str ref | list of refs
235
+
236
+ ## usage
237
+
238
+ bind class names with dict,value is bool ref, for example:
239
+
240
+ ```python
241
+ bg_color = to_ref(True)
242
+ has_error = to_ref(False)
243
+
244
+ rxui.label('Hello').bind_classes({'bg-blue':bg_color, 'text-red':has_error})
245
+ ```
246
+
247
+ bind list of class names with ref
248
+
249
+ ```python
250
+ color = to_ref('red')
251
+ bg_color = lambda: f"bg-{color.value}"
252
+
253
+ rxui.label('Hello').bind_classes([bg_color])
254
+ ```
255
+
256
+ bind single class name with ref
257
+
258
+ ```python
259
+ color = to_ref('red')
260
+ bg_color = lambda: f"bg-{color.value}"
261
+
262
+ rxui.label('Hello').bind_classes(bg_color)
263
+ ```
264
+
265
+ """
266
+ if isinstance(classes, dict):
267
+ for name, ref_obj in classes.items():
268
+
269
+ @self._ui_effect
270
+ def _(name=name, ref_obj=ref_obj):
271
+ if to_value(ref_obj):
272
+ self.classes(add=name)
273
+ else:
274
+ self.classes(remove=name)
275
+
276
+ elif is_ref(classes) or isinstance(classes, Callable):
277
+ ref_obj = to_value(classes) # type: ignore
278
+
279
+ if isinstance(ref_obj, dict):
280
+
281
+ @self._ui_effect
282
+ def _():
283
+ for name, value in cast(Dict, to_value(classes)).items(): # type: ignore
284
+ if value:
285
+ self.classes(add=name)
286
+ else:
287
+ self.classes(remove=name)
288
+ else:
289
+ self._bind_single_class(cast(_T_bind_classes_type_single, classes))
290
+
291
+ elif isinstance(classes, list):
292
+ for ref_name in classes:
293
+ self._bind_single_class(ref_name)
294
+
295
+ return self
296
+
297
+ def _bind_single_class(self, class_name: _T_bind_classes_type_single):
298
+ if is_ref(class_name) or isinstance(class_name, Callable):
299
+
300
+ @on(class_name)
301
+ def _(state: WatchedState):
302
+ self.classes(add=state.current, remove=state.previous)
303
+ else:
304
+ self.classes(class_name) # type: ignore
305
+
306
+ return self
307
+
308
+ def bind_style(self, style: Dict[str, TGetterOrReadonlyRef[Any]]):
309
+ """data binding is manipulating an element's style
310
+
311
+ @see - https://github.com/CrystalWindSnake/ex4nicegui/blob/main/README.en.md#bind-style
312
+ @中文文档 - https://gitee.com/carson_add/ex4nicegui/tree/main/#bind-style
313
+
314
+ Args:
315
+ style (Dict[str, Union[ReadonlyRef[str], Ref[str]]]): dict of style name and ref value
316
+
317
+
318
+ ## usage
319
+ ```python
320
+ bg_color = to_ref("blue")
321
+ text_color = to_ref("red")
322
+
323
+ rxui.label("test").bind_style(
324
+ {
325
+ "background-color": bg_color,
326
+ "color": text_color,
327
+ }
328
+ )
329
+ ```
330
+ """
331
+ if isinstance(style, dict):
332
+ for name, ref_obj in style.items():
333
+ if is_ref(ref_obj) or isinstance(ref_obj, Callable):
334
+
335
+ @self._ui_effect
336
+ def _(name=name, ref_obj=ref_obj):
337
+ self.element._style[name] = str(to_value(ref_obj))
338
+ self.element.update()
339
+
340
+ return self
341
+
342
+ def scoped_style(self, selector: str, style: Union[str, Path]):
343
+ """add scoped style to the element
344
+
345
+ @see - https://github.com/CrystalWindSnake/ex4nicegui/blob/main/README.en.md#scoped_style
346
+ @中文文档 - https://gitee.com/carson_add/ex4nicegui/tree/main/#scoped_style
347
+
348
+ Args:
349
+ selector (str): css selector
350
+ style (Union[str, Path]): path to css file or inline style string
351
+
352
+ ## usage
353
+ ```python
354
+ # all children of the element will have red outline, excluding itself
355
+ with rxui.row().scoped_style("*", "outline: 1px solid red;") as row:
356
+ ui.label("Hello")
357
+ ui.label("World")
358
+
359
+ # all children of the element will have red outline, including the element itself
360
+ with rxui.row().scoped_style(":self *", "outline: 1px solid red;") as row:
361
+ ui.label("Hello")
362
+ ui.label("World")
363
+
364
+ # all children of the element will have red outline when element is hovered
365
+ with rxui.row().scoped_style(":hover *", "outline: 1px solid red;") as row:
366
+ ui.label("Hello")
367
+ ui.label("World")
368
+
369
+ # all children of the element and itself will have red outline when element is hovered
370
+ with rxui.row().scoped_style(":self:hover *", "outline: 1px solid red;") as row:
371
+ ui.label("Hello")
372
+ ui.label("World")
373
+ ```
374
+ """
375
+
376
+ is_css_file = isinstance(style, Path)
377
+
378
+ if is_css_file:
379
+ style = style.read_text(encoding="utf-8")
380
+
381
+ id = f"c{self.element.id}"
382
+ selector_with_self = _utils._parent_id_with_selector(id, selector, is_css_file)
383
+ css = ""
384
+ if is_css_file:
385
+ css = f"{selector_with_self} {style}"
386
+ else:
387
+ css = f"{selector_with_self}{{{style}}}"
388
+
389
+ scope_style = ScopedStyle.get()
390
+ assert scope_style, "can not find scope style"
391
+ scope_style.create_style(self.element, css)
392
+ self.__used_scope_style = True
393
+ return self
394
+
395
+ def update(self):
396
+ """Update the element on the client side."""
397
+ self.element.update()
398
+
399
+
400
+ class _utils:
401
+ @staticmethod
402
+ def _parent_id_with_selector(
403
+ parent_id: str, selector: str, is_css_file=False
404
+ ) -> str:
405
+ selector_with_self = f"#{parent_id}"
406
+
407
+ selector = selector.strip()
408
+ if (not selector) and (not is_css_file):
409
+ selector = "* "
410
+
411
+ if selector.startswith(":self"):
412
+ selector = selector[5:].lstrip()
413
+ parent_selector = f"#{parent_id}"
414
+ if selector.startswith(":"):
415
+ parent_selector = f"{parent_selector}{selector.split()[0]}"
416
+
417
+ selector_with_self = f"{parent_selector},{selector_with_self}"
418
+
419
+ if not selector.startswith(":"):
420
+ selector_with_self = selector_with_self + " "
421
+
422
+ selector_with_self = selector_with_self + selector
423
+ return selector_with_self
424
+
425
+
426
+ DisableableBindableUi = DisableableMixin
@@ -14,10 +14,11 @@ class DeferredTask:
14
14
  for task in self._tasks:
15
15
  task()
16
16
 
17
- self._tasks.clear()
18
-
19
17
  # Avoid events becoming ineffective due to page refresh when sharing the client.
20
18
  if not client.shared:
19
+ # In a shared page, execution is required with every refresh.
20
+ # note:https://github.com/CrystalWindSnake/ex4nicegui/issues/227
21
+ self._tasks.clear()
21
22
  client.connect_handlers.remove(on_client_connect) # type: ignore
22
23
 
23
24
  ui.context.client.on_connect(on_client_connect)
@@ -0,0 +1,41 @@
1
+ from typing import (
2
+ Any,
3
+ Callable,
4
+ Protocol,
5
+ )
6
+
7
+ import signe
8
+ from ex4nicegui.utils.signals import (
9
+ TGetterOrReadonlyRef,
10
+ WatchedState,
11
+ )
12
+ from nicegui import ui
13
+ from ex4nicegui.reactive.systems import color_system
14
+
15
+
16
+ class BackgroundColorableMixin(Protocol):
17
+ _ui_signal_on: Callable[[Callable[..., Any]], signe.Effect[None]]
18
+
19
+ @property
20
+ def element(self) -> ui.element:
21
+ ...
22
+
23
+ def _bind_background_color(self, ref_ui: TGetterOrReadonlyRef[str]):
24
+ @self._ui_signal_on(ref_ui) # type: ignore
25
+ def _(state: WatchedState):
26
+ if state.previous is not None:
27
+ color_system.remove_background_color(self.element, state.previous)
28
+
29
+ color_system.add_background_color(self.element, state.current)
30
+
31
+ self.element.update()
32
+
33
+ def bind_color(self, ref_ui: TGetterOrReadonlyRef[str]):
34
+ """bind color to the element
35
+
36
+ Args:
37
+ ref_ui (TGetterOrReadonlyRef[str]): a reference to the color value
38
+
39
+ """
40
+ self._bind_background_color(ref_ui)
41
+ return self
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import (
4
+ Any,
5
+ Callable,
6
+ Protocol,
7
+ TypeVar,
8
+ )
9
+
10
+ import signe
11
+ from ex4nicegui.utils.signals import (
12
+ TGetterOrReadonlyRef,
13
+ to_value,
14
+ )
15
+ from nicegui.elements.mixins.disableable_element import DisableableElement
16
+
17
+
18
+ _T_DisableableBinder = TypeVar("_T_DisableableBinder", bound=DisableableElement)
19
+
20
+
21
+ class DisableableMixin(Protocol):
22
+ _ui_effect: Callable[[Callable[..., Any]], signe.Effect[None]]
23
+
24
+ @property
25
+ def element(self) -> DisableableElement:
26
+ ...
27
+
28
+ def bind_enabled(self, ref_ui: TGetterOrReadonlyRef[bool]):
29
+ @self._ui_effect
30
+ def _():
31
+ value = to_value(ref_ui)
32
+ self.element.set_enabled(value)
33
+ self.element._handle_enabled_change(value)
34
+
35
+ return self
36
+
37
+ def bind_disable(self, ref_ui: TGetterOrReadonlyRef[bool]):
38
+ @self._ui_effect
39
+ def _():
40
+ value = not to_value(ref_ui)
41
+ self.element.set_enabled(value)
42
+ self.element._handle_enabled_change(value)
43
+
44
+ return self
@@ -0,0 +1,66 @@
1
+ from typing import (
2
+ Any,
3
+ Callable,
4
+ Dict,
5
+ Protocol,
6
+ )
7
+
8
+ import signe
9
+ from ex4nicegui.utils.signals import (
10
+ TGetterOrReadonlyRef,
11
+ WatchedState,
12
+ )
13
+ from nicegui import ui
14
+ from ex4nicegui.reactive.systems import color_system
15
+
16
+
17
+ class TextColorableMixin(Protocol):
18
+ _ui_signal_on: Callable[[Callable[[TGetterOrReadonlyRef[str]], Any]], signe.Effect]
19
+
20
+ @property
21
+ def element(self) -> ui.element:
22
+ ...
23
+
24
+ def _bind_text_color(self, ref_ui: TGetterOrReadonlyRef[str]):
25
+ @self._ui_signal_on(ref_ui) # type: ignore
26
+ def _(state: WatchedState):
27
+ if state.previous is not None:
28
+ color_system.remove_text_color(self.element, state.previous)
29
+
30
+ color_system.add_text_color(self.element, state.current)
31
+
32
+ self.element.update()
33
+
34
+ def bind_color(self, ref_ui: TGetterOrReadonlyRef[str]):
35
+ """bind text color to the element
36
+
37
+ Args:
38
+ ref_ui (TGetterOrReadonlyRef[str]): a reference to the color value
39
+
40
+ """
41
+ self._bind_text_color(ref_ui)
42
+ return self
43
+
44
+
45
+ class HtmlTextColorableMixin(Protocol):
46
+ _ui_signal_on: Callable[[Callable[[TGetterOrReadonlyRef[str]], Any]], signe.Effect]
47
+
48
+ @property
49
+ def element(self) -> ui.element:
50
+ ...
51
+
52
+ def bind_style(self, style: Dict[str, TGetterOrReadonlyRef[Any]]):
53
+ ...
54
+
55
+ def _bind_text_color(self, ref_ui: TGetterOrReadonlyRef[str]):
56
+ return self.bind_style({"color": ref_ui})
57
+
58
+ def bind_color(self, ref_ui: TGetterOrReadonlyRef[str]):
59
+ """bind text color to the element
60
+
61
+ Args:
62
+ ref_ui (TGetterOrReadonlyRef[str]): a reference to the color value
63
+
64
+ """
65
+ self._bind_text_color(ref_ui)
66
+ return self