Bladeren bron

Merge pull request #894 from itay-raveh/input-error

Add `Input.error` property
Falko Schindler 1 jaar geleden
bovenliggende
commit
149662ca58

+ 5 - 16
nicegui/elements/input.py

@@ -2,10 +2,10 @@ from typing import Any, Callable, Dict, List, Optional
 
 from .icon import Icon
 from .mixins.disableable_element import DisableableElement
-from .mixins.value_element import ValueElement
+from .mixins.validation_element import ValidationElement
 
 
-class Input(ValueElement, DisableableElement):
+class Input(ValidationElement, DisableableElement):
     LOOPBACK = False
 
     def __init__(self,
@@ -35,9 +35,9 @@ class Input(ValueElement, DisableableElement):
         :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 short!': lambda value: len(value) < 3}``
+        :param validation: dictionary of validation rules, e.g. ``{'Too long!': lambda value: len(value) < 3}``
         """
-        super().__init__(tag='q-input', value=value, on_value_change=on_change)
+        super().__init__(tag='q-input', value=value, on_value_change=on_change, validation=validation)
         if label is not None:
             self._props['label'] = label
         if placeholder is not None:
@@ -52,8 +52,6 @@ class Input(ValueElement, DisableableElement):
                     self.props(f'type={"text" if is_hidden else "password"}')
                 icon = Icon('visibility_off').classes('cursor-pointer').on('click', toggle_type)
 
-        self.validation = validation
-
         if autocomplete:
             def find_autocompletion() -> Optional[str]:
                 if self.value:
@@ -71,16 +69,7 @@ class Input(ValueElement, DisableableElement):
                 match = find_autocompletion()
                 if match:
                     self.set_value(match)
-                self.props(f'shadow-text=""')
+                self.props('shadow-text=""')
 
             self.on('keyup', autocomplete_input)
             self.on('keydown.tab', complete_input)
-
-    def on_value_change(self, value: Any) -> None:
-        super().on_value_change(value)
-        for message, check in self.validation.items():
-            if not check(value):
-                self.props(f'error error-message="{message}"')
-                break
-        else:
-            self.props(remove='error')

+ 27 - 0
nicegui/elements/mixins/validation_element.py

@@ -0,0 +1,27 @@
+from typing import Any, Callable, Dict, Optional
+
+from .value_element import ValueElement
+
+
+class ValidationElement(ValueElement):
+
+    def __init__(self, validation: Dict[str, Callable[..., bool]], **kwargs: Any) -> None:
+        super().__init__(**kwargs)
+        self.validation = validation
+        self._error: Optional[str] = None
+
+    @property
+    def error(self) -> Optional[str]:
+        """The latest error message from the validation functions."""
+        return self._error
+
+    def on_value_change(self, value: Any) -> None:
+        super().on_value_change(value)
+        for message, check in self.validation.items():
+            if not check(value):
+                self._error = message
+                self.props(f'error error-message="{message}"')
+                break
+        else:
+            self._error = None
+            self.props(remove='error')

+ 4 - 14
nicegui/elements/number.py

@@ -1,10 +1,10 @@
 from typing import Any, Callable, Dict, Optional
 
 from .mixins.disableable_element import DisableableElement
-from .mixins.value_element import ValueElement
+from .mixins.validation_element import ValidationElement
 
 
-class Number(ValueElement, DisableableElement):
+class Number(ValidationElement, DisableableElement):
     LOOPBACK = False
 
     def __init__(self,
@@ -37,10 +37,10 @@ class Number(ValueElement, 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 small!': lambda value: value < 3}``
+        :param validation: dictionary of validation rules, e.g. ``{'Too large!': lambda value: value < 3}``
         """
         self.format = format
-        super().__init__(tag='q-input', value=value, on_value_change=on_change)
+        super().__init__(tag='q-input', value=value, on_value_change=on_change, validation=validation)
         self._props['type'] = 'number'
         if label is not None:
             self._props['label'] = label
@@ -56,7 +56,6 @@ class Number(ValueElement, DisableableElement):
             self._props['prefix'] = prefix
         if suffix is not None:
             self._props['suffix'] = suffix
-        self.validation = validation
         self.on('blur', self.sanitize)
 
     def sanitize(self) -> None:
@@ -65,15 +64,6 @@ class Number(ValueElement, DisableableElement):
         value = min(value, self._props.get('max', float('inf')))
         self.set_value(float(self.format % value) if self.format else value)
 
-    def on_value_change(self, value: Any) -> None:
-        super().on_value_change(value)
-        for message, check in self.validation.items():
-            if not check(value):
-                self.props(f'error error-message="{message}"')
-                break
-        else:
-            self.props(remove='error')
-
     def _msg_to_value(self, msg: Dict) -> Any:
         return float(msg['args']) if msg['args'] else None
 

+ 4 - 1
nicegui/elements/textarea.py

@@ -17,11 +17,14 @@ class Textarea(Input):
         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.
+        The key of the first rule that fails will be displayed as an 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 short!': lambda value: len(value) < 3}``
+        :param validation: dictionary of validation rules, e.g. ``{'Too long!': lambda value: len(value) < 3}``
         """
         super().__init__(label, placeholder=placeholder, value=value, on_change=on_change, validation=validation)
         self._props['type'] = 'textarea'

+ 3 - 1
tests/test_input.py

@@ -55,7 +55,7 @@ def test_toggle_button(screen: Screen):
 
 
 def test_input_validation(screen: Screen):
-    ui.input('Name', validation={'Too short': lambda value: len(value) >= 5})
+    input = ui.input('Name', validation={'Too short': lambda value: len(value) >= 5})
 
     screen.open('/')
     screen.should_contain('Name')
@@ -63,10 +63,12 @@ def test_input_validation(screen: Screen):
     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(' Doe')
     screen.wait(0.5)
     screen.should_not_contain('Too short')
+    assert input.error is None
 
 
 def test_input_with_multi_word_error_message(screen: Screen):