Browse Source

refactor: Introduces `LabelElement` (#4529)

This PR introduces the `LabelElement `and applies it to the inheritance
of the following components to provide a series of methods about label:

- ui.input
- ui.select
- ui.color_input
- ui.number
- ui.tab
- ui.text_area
- ui.upload

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
不留 1 tháng trước cách đây
mục cha
commit
1a2e882a28

+ 3 - 5
nicegui/elements/chat_message.py

@@ -1,11 +1,11 @@
 import html
 from typing import List, Optional, Union
 
-from ..element import Element
 from .html import Html
+from .mixins.label_element import LabelElement
 
 
-class ChatMessage(Element):
+class ChatMessage(LabelElement):
 
     def __init__(self,
                  text: Union[str, List[str]] = ..., *,  # type: ignore
@@ -28,7 +28,7 @@ class ChatMessage(Element):
         :param sent: render as a sent message (so from current user) (default: False)
         :param text_html: render text as HTML (default: False)
         """
-        super().__init__('q-chat-message')
+        super().__init__(tag='q-chat-message', label=label)
 
         if text is ...:
             text = []
@@ -40,8 +40,6 @@ class ChatMessage(Element):
 
         if name is not None:
             self._props['name'] = name
-        if label is not None:
-            self._props['label'] = label
         if stamp is not None:
             self._props['stamp'] = stamp
         if avatar is not None:

+ 3 - 4
nicegui/elements/color_input.py

@@ -4,10 +4,11 @@ from ..events import Handler, ValueChangeEventArguments
 from .button import Button as button
 from .color_picker import ColorPicker as color_picker
 from .mixins.disableable_element import DisableableElement
+from .mixins.label_element import LabelElement
 from .mixins.value_element import ValueElement
 
 
-class ColorInput(ValueElement, DisableableElement):
+class ColorInput(LabelElement, ValueElement, DisableableElement):
     LOOPBACK = False
 
     def __init__(self,
@@ -27,9 +28,7 @@ class ColorInput(ValueElement, DisableableElement):
         :param on_change: callback to execute when the value changes
         :param preview: change button background to selected color (default: False)
         """
-        super().__init__(tag='q-input', value=value, on_value_change=on_change)
-        if label is not None:
-            self._props['label'] = label
+        super().__init__(tag='q-input', label=label, value=value, on_value_change=on_change)
         if placeholder is not None:
             self._props['placeholder'] = placeholder
 

+ 3 - 4
nicegui/elements/input.py

@@ -3,10 +3,11 @@ from typing import Any, List, Optional, Union
 from ..events import Handler, ValueChangeEventArguments
 from .icon import Icon
 from .mixins.disableable_element import DisableableElement
+from .mixins.label_element import LabelElement
 from .mixins.validation_element import ValidationDict, ValidationElement, ValidationFunction
 
 
-class Input(ValidationElement, DisableableElement, component='input.js'):
+class Input(LabelElement, ValidationElement, DisableableElement, component='input.js'):
     VALUE_PROP: str = 'value'
     LOOPBACK = False
 
@@ -49,9 +50,7 @@ class Input(ValidationElement, DisableableElement, component='input.js'):
         :param autocomplete: optional list of strings for autocompletion
         :param validation: dictionary of validation rules or a callable that returns an optional error message (default: None for no validation)
         """
-        super().__init__(value=value, on_value_change=on_change, validation=validation)
-        if label is not None:
-            self._props['label'] = label
+        super().__init__(label=label, value=value, on_value_change=on_change, validation=validation)
         if placeholder is not None:
             self._props['placeholder'] = placeholder
         self._props['type'] = 'password' if password else 'text'

+ 89 - 0
nicegui/elements/mixins/label_element.py

@@ -0,0 +1,89 @@
+from typing import Any, Callable, Optional, cast
+
+from typing_extensions import Self
+
+from ...binding import BindableProperty, bind, bind_from, bind_to
+from ...element import Element
+
+
+class LabelElement(Element):
+    label = BindableProperty(
+        on_change=lambda sender, label: cast(Self, sender)._handle_label_change(label))  # pylint: disable=protected-access
+
+    def __init__(self, *, label: Optional[str], **kwargs: Any) -> None:
+        super().__init__(**kwargs)
+        self.label = label
+        if label is not None:
+            self._props['label'] = label
+
+    def bind_label_to(self,
+                      target_object: Any,
+                      target_name: str = 'label',
+                      forward: Callable[..., Any] = lambda x: x,
+                      ) -> Self:
+        """Bind the label of this element to the target object's target_name property.
+
+        The binding works one way only, from this element to the target.
+        The update happens immediately and whenever a value changes.
+
+        :param target_object: The object to bind to.
+        :param target_name: The name of the property to bind to.
+        :param forward: A function to apply to the value before applying it to the target.
+        """
+        bind_to(self, 'label', target_object, target_name, forward)
+        return self
+
+    def bind_label_from(self,
+                        target_object: Any,
+                        target_name: str = 'label',
+                        backward: Callable[..., Any] = lambda x: x,
+                        ) -> Self:
+        """Bind the label of this element from the target object's target_name property.
+
+        The binding works one way only, from the target to this element.
+        The update happens immediately and whenever a value changes.
+
+        :param target_object: The object to bind from.
+        :param target_name: The name of the property to bind from.
+        :param backward: A function to apply to the value before applying it to this element.
+        """
+        bind_from(self, 'label', target_object, target_name, backward)
+        return self
+
+    def bind_label(self,
+                   target_object: Any,
+                   target_name: str = 'label', *,
+                   forward: Callable[..., Any] = lambda x: x,
+                   backward: Callable[..., Any] = lambda x: x,
+                   ) -> Self:
+        """Bind the label of this element to the target object's target_name property.
+
+        The binding works both ways, from this element to the target and from the target to this element.
+        The update happens immediately and whenever a value changes.
+        The backward binding takes precedence for the initial synchronization.
+
+        :param target_object: The object to bind to.
+        :param target_name: The name of the property to bind to.
+        :param forward: A function to apply to the value before applying it to the target.
+        :param backward: A function to apply to the value before applying it to this element.
+        """
+        bind(self, 'label', target_object, target_name, forward=forward, backward=backward)
+        return self
+
+    def set_label(self, label: Optional[str]) -> None:
+        """Set the label of this element.
+
+        :param label: The new label.
+        """
+        self.label = label
+
+    def _handle_label_change(self, label: Optional[str]) -> None:
+        """Called when the label of this element changes.
+
+        :param label: The new label.
+        """
+        if label is None:
+            del self._props['label']
+        else:
+            self._props['label'] = label
+        self.update()

+ 3 - 4
nicegui/elements/number.py

@@ -2,10 +2,11 @@ from typing import Any, Optional, Union
 
 from ..events import GenericEventArguments, Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
+from .mixins.label_element import LabelElement
 from .mixins.validation_element import ValidationDict, ValidationElement, ValidationFunction
 
 
-class Number(ValidationElement, DisableableElement):
+class Number(LabelElement, ValidationElement, DisableableElement):
     LOOPBACK = False
 
     def __init__(self,
@@ -46,10 +47,8 @@ class Number(ValidationElement, DisableableElement):
         :param validation: dictionary of validation rules or a callable that returns an optional error message (default: None for no validation)
         """
         self.format = format
-        super().__init__(tag='q-input', value=value, on_value_change=on_change, validation=validation)
+        super().__init__(tag='q-input', label=label, value=value, on_value_change=on_change, validation=validation)
         self._props['type'] = 'number'
-        if label is not None:
-            self._props['label'] = label
         if placeholder is not None:
             self._props['placeholder'] = placeholder
         if min is not None:

+ 3 - 4
nicegui/elements/select.py

@@ -5,10 +5,11 @@ from typing import Any, Callable, Dict, Iterator, List, Literal, Optional, Union
 from ..events import GenericEventArguments, Handler, ValueChangeEventArguments
 from .choice_element import ChoiceElement
 from .mixins.disableable_element import DisableableElement
+from .mixins.label_element import LabelElement
 from .mixins.validation_element import ValidationDict, ValidationElement, ValidationFunction
 
 
-class Select(ValidationElement, ChoiceElement, DisableableElement, component='select.js'):
+class Select(LabelElement, ValidationElement, ChoiceElement, DisableableElement, component='select.js'):
 
     def __init__(self,
                  options: Union[List, Dict], *,
@@ -60,9 +61,7 @@ class Select(ValidationElement, ChoiceElement, DisableableElement, component='se
                 value = [value]
             else:
                 value = value[:]  # NOTE: avoid modifying the original list which could be the list of options (#3014)
-        super().__init__(options=options, value=value, on_change=on_change, validation=validation)
-        if label is not None:
-            self._props['label'] = label
+        super().__init__(label=label, options=options, value=value, on_change=on_change, validation=validation)
         if isinstance(key_generator, Generator):
             next(key_generator)  # prime the key generator, prepare it to receive the first value
         self.key_generator = key_generator

+ 5 - 3
nicegui/elements/tabs.py

@@ -6,6 +6,7 @@ from ..context import context
 from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.icon_element import IconElement
+from .mixins.label_element import LabelElement
 from .mixins.value_element import ValueElement
 
 
@@ -29,7 +30,7 @@ class Tabs(ValueElement):
         return value.props['name'] if isinstance(value, (Tab, TabPanel)) else value
 
 
-class Tab(IconElement, DisableableElement):
+class Tab(LabelElement, IconElement, DisableableElement):
 
     def __init__(self, name: str, label: Optional[str] = None, icon: Optional[str] = None) -> None:
         """Tab
@@ -41,9 +42,10 @@ class Tab(IconElement, DisableableElement):
         :param label: label of the tab (default: `None`, meaning the same as `name`)
         :param icon: icon of the tab (default: `None`)
         """
-        super().__init__(tag='q-tab', icon=icon)
+        if label is None:
+            label = name
+        super().__init__(tag='q-tab', label=label, icon=icon)
         self._props['name'] = name
-        self._props['label'] = label if label is not None else name
         self.tabs = context.slot.parent
 
 

+ 3 - 3
nicegui/elements/upload.py

@@ -7,9 +7,10 @@ from typing_extensions import Self
 from ..events import Handler, MultiUploadEventArguments, UiEventArguments, UploadEventArguments, handle_event
 from ..nicegui import app
 from .mixins.disableable_element import DisableableElement
+from .mixins.label_element import LabelElement
 
 
-class Upload(DisableableElement, component='upload.js'):
+class Upload(LabelElement, DisableableElement, component='upload.js'):
 
     def __init__(self, *,
                  multiple: bool = False,
@@ -36,9 +37,8 @@ class Upload(DisableableElement, component='upload.js'):
         :param label: label for the uploader (default: `''`)
         :param auto_upload: automatically upload files when they are selected (default: `False`)
         """
-        super().__init__()
+        super().__init__(label=label)
         self._props['multiple'] = multiple
-        self._props['label'] = label
         self._props['auto-upload'] = auto_upload
         self._props['url'] = f'/_nicegui/client/{self.client.id}/upload/{self.id}'