Browse Source

Merge pull request #2342 from iron3oxide/main

Fix updating of error message for validation elements
Falko Schindler 1 year ago
parent
commit
59c3dee195

+ 7 - 4
nicegui/elements/input.py

@@ -1,4 +1,4 @@
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable, Dict, List, Optional, Union
 
 from .icon import Icon
 from .mixins.disableable_element import DisableableElement
@@ -17,7 +17,8 @@ class Input(ValidationElement, DisableableElement, component='input.js'):
                  password_toggle_button: bool = False,
                  on_change: Optional[Callable[..., Any]] = None,
                  autocomplete: Optional[List[str]] = None,
-                 validation: Optional[Dict[str, Callable[..., bool]]] = None) -> None:
+                 validation: Optional[Union[Callable[..., Optional[str]], Dict[str, Callable[..., bool]]]] = None,
+                 ) -> None:
         """Text Input
 
         This element is based on Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component.
@@ -26,8 +27,10 @@ class Input(ValidationElement, DisableableElement, component='input.js'):
         If you want to wait until the user confirms the input, you can register a custom event callback, e.g.
         `ui.input(...).on('keydown.enter', ...)` or `ui.input(...).on('blur', ...)`.
 
-        You can use the `validation` parameter to define a dictionary of validation rules.
+        You can use the `validation` parameter to define a dictionary of validation rules,
+        e.g. ``{'Too long!': lambda value: len(value) < 3}``.
         The key of the first rule that fails will be displayed as an error message.
+        Alternatively, you can pass a callable that returns an optional error message.
 
         Note about styling the input:
         Quasar's `QInput` component is a wrapper around a native `input` element.
@@ -42,7 +45,7 @@ class Input(ValidationElement, DisableableElement, component='input.js'):
         :param password_toggle_button: whether to show a button to toggle the password visibility (default: False)
         :param on_change: callback to execute when the value changes
         :param autocomplete: optional list of strings for autocompletion
-        :param validation: dictionary of validation rules, e.g. ``{'Too long!': lambda value: len(value) < 3}``
+        :param validation: dictionary of validation rules or a callable that returns an optional error message
         """
         super().__init__(value=value, on_value_change=on_change, validation=validation)
         if label is not None:

+ 5 - 5
nicegui/elements/mixins/validation_element.py

@@ -21,12 +21,12 @@ class ValidationElement(ValueElement):
 
         :param error: The optional error message
         """
+        if self._error == error:
+            return
         self._error = error
-        if self._error is None:
-            self.props(remove='error')
-        else:
-            self._props['error-message'] = self._error
-            self.props('error')
+        self._props['error'] = error is not None
+        self._props['error-message'] = error
+        self.update()
 
     def validate(self) -> bool:
         """Validate the current value and set the error message if necessary.

+ 6 - 4
nicegui/elements/number.py

@@ -1,4 +1,4 @@
-from typing import Any, Callable, Dict, Optional
+from typing import Any, Callable, Dict, Optional, Union
 
 from ..events import GenericEventArguments
 from .mixins.disableable_element import DisableableElement
@@ -20,14 +20,16 @@ class Number(ValidationElement, DisableableElement):
                  suffix: Optional[str] = None,
                  format: Optional[str] = None,  # pylint: disable=redefined-builtin
                  on_change: Optional[Callable[..., Any]] = None,
-                 validation: Optional[Dict[str, Callable[..., bool]]] = None,
+                 validation: Optional[Union[Callable[..., Optional[str]], Dict[str, Callable[..., bool]]]] = None,
                  ) -> None:
         """Number Input
 
         This element is based on Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component.
 
-        You can use the `validation` parameter to define a dictionary of validation rules.
+        You can use the `validation` parameter to define a dictionary of validation rules,
+        e.g. ``{'Too small!': lambda value: value < 3}``.
         The key of the first rule that fails will be displayed as an error message.
+        Alternatively, you can pass a callable that returns an optional error message.
 
         :param label: displayed name for the number input
         :param placeholder: text to show if no value is entered
@@ -40,7 +42,7 @@ class Number(ValidationElement, DisableableElement):
         :param suffix: a suffix to append to the displayed value
         :param format: a string like "%.2f" to format the displayed value
         :param on_change: callback to execute when the value changes
-        :param validation: dictionary of validation rules, e.g. ``{'Too large!': lambda value: value < 3}``
+        :param validation: dictionary of validation rules or a callable that returns an optional error message
         """
         self.format = format
         super().__init__(tag='q-input', value=value, on_value_change=on_change, validation=validation)

+ 6 - 4
nicegui/elements/textarea.py

@@ -1,4 +1,4 @@
-from typing import Any, Callable, Dict, Optional
+from typing import Any, Callable, Dict, Optional, Union
 
 from .input import Input
 
@@ -10,21 +10,23 @@ class Textarea(Input, component='input.js'):
                  placeholder: Optional[str] = None,
                  value: str = '',
                  on_change: Optional[Callable[..., Any]] = None,
-                 validation: Optional[Dict[str, Callable[..., bool]]] = None,
+                 validation: Optional[Union[Callable[..., Optional[str]], Dict[str, Callable[..., bool]]]] = None,
                  ) -> None:
         """Textarea
 
         This element is based on Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component.
         The ``type`` is set to ``textarea`` to create a multi-line text input.
 
-        You can use the `validation` parameter to define a dictionary of validation rules.
+        You can use the `validation` parameter to define a dictionary of validation rules,
+        e.g. ``{'Too long!': lambda value: len(value) < 3}``.
         The key of the first rule that fails will be displayed as an error message.
+        Alternatively, you can pass a callable that returns an optional error message.
 
         :param label: displayed name for the textarea
         :param placeholder: text to show if no value is entered
         :param value: the initial value of the field
         :param on_change: callback to execute when the value changes
-        :param validation: dictionary of validation rules, e.g. ``{'Too long!': lambda value: len(value) < 3}``
+        :param validation: dictionary of validation rules or a callable that returns an optional error message
         """
         super().__init__(label, placeholder=placeholder, value=value, on_change=on_change, validation=validation)
         self._props['type'] = 'textarea'

+ 13 - 7
tests/test_input.py

@@ -57,22 +57,28 @@ def test_toggle_button(screen: Screen):
 @pytest.mark.parametrize('use_callable', [False, True])
 def test_input_validation(use_callable: bool, screen: Screen):
     if use_callable:
-        input_ = ui.input('Name', validation=lambda value: 'Too short' if len(value) < 5 else None)
+        input_ = ui.input('Name', validation=lambda x: 'Short' if len(x) < 3 else 'Still short' if len(x) < 5 else None)
     else:
-        input_ = ui.input('Name', validation={'Too short': lambda value: len(value) >= 5})
+        input_ = ui.input('Name', validation={'Short': lambda x: len(x) >= 3, 'Still short': lambda x: len(x) >= 5})
 
     screen.open('/')
     screen.should_contain('Name')
 
     element = screen.selenium.find_element(By.XPATH, '//*[@aria-label="Name"]')
-    element.send_keys('John')
-    screen.should_contain('Too short')
-    assert input_.error == 'Too short'
+    element.send_keys('Jo')
+    screen.should_contain('Short')
+    assert input_.error == 'Short'
+    assert not input_.validate()
+
+    element.send_keys('hn')
+    screen.should_contain('Still short')
+    assert input_.error == 'Still short'
     assert not input_.validate()
 
     element.send_keys(' Doe')
-    screen.wait(0.5)
-    screen.should_not_contain('Too short')
+    screen.wait(1.0)
+    screen.should_not_contain('Short')
+    screen.should_not_contain('Still short')
     assert input_.error is None
     assert input_.validate()