Repository: zauberzeug/nicegui
Pull request #2661 (2024-03-04 14:13:47)

Allow disabling the auto-validation on value change
This PR introduces a `.without_auto_validation()` builder method to avoid validating the value on every value change as requested in #2618:

```py
i = ui.input(validation={'Input too long': lambda value: len(value) < 3}).without_auto_validation()
ui.button('Validate', on_click=i.validate)
```


Patch:
diff --git a/nicegui/elements/input.py b/nicegui/elements/input.py
index 9f35ea10..4077dedd 100644
--- a/nicegui/elements/input.py
+++ b/nicegui/elements/input.py
@@ -31,6 +31,7 @@ class Input(ValidationElement, DisableableElement, component='input.js'):
         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.
+        To disable the automatic validation on every value change, you can use the `without_auto_validation` method.
 
         Note about styling the input:
         Quasar's `QInput` component is a wrapper around a native `input` element.
diff --git a/nicegui/elements/mixins/validation_element.py b/nicegui/elements/mixins/validation_element.py
index 4718da88..ff3d9f7e 100644
--- a/nicegui/elements/mixins/validation_element.py
+++ b/nicegui/elements/mixins/validation_element.py
@@ -1,5 +1,7 @@
 from typing import Any, Callable, Dict, Optional, Union
 
+from typing_extensions import Self
+
 from .value_element import ValueElement
 
 
@@ -7,6 +9,7 @@ class ValidationElement(ValueElement):
 
     def __init__(self, validation: Optional[Union[Callable[..., Optional[str]], Dict[str, Callable[..., bool]]]], **kwargs: Any) -> None:
         self.validation = validation if validation is not None else {}
+        self._auto_validation = True
         self._error: Optional[str] = None
         super().__init__(**kwargs)
 
@@ -45,6 +48,12 @@ class ValidationElement(ValueElement):
         self.error = None
         return True
 
+    def without_auto_validation(self) -> Self:
+        """Disable automatic validation on value change."""
+        self._auto_validation = False
+        return self
+
     def _handle_value_change(self, value: Any) -> None:
         super()._handle_value_change(value)
-        self.validate()
+        if self._auto_validation:
+            self.validate()
diff --git a/nicegui/elements/number.py b/nicegui/elements/number.py
index 5912a068..d16f86ed 100644
--- a/nicegui/elements/number.py
+++ b/nicegui/elements/number.py
@@ -30,6 +30,7 @@ class Number(ValidationElement, DisableableElement):
         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.
+        To disable the automatic validation on every value change, you can use the `without_auto_validation` method.
 
         :param label: displayed name for the number input
         :param placeholder: text to show if no value is entered
diff --git a/nicegui/elements/select.py b/nicegui/elements/select.py
index ad7cb2cc..11d192b3 100644
--- a/nicegui/elements/select.py
+++ b/nicegui/elements/select.py
@@ -37,6 +37,7 @@ class Select(ValidationElement, ChoiceElement, DisableableElement, component='se
         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.
+        To disable the automatic validation on every value change, you can use the `without_auto_validation` method.
 
         :param options: a list ['value1', ...] or dictionary `{'value1':'label1', ...}` specifying the options
         :param label: the label to display above the selection
diff --git a/nicegui/elements/textarea.py b/nicegui/elements/textarea.py
index 3a96bced..c29a5cac 100644
--- a/nicegui/elements/textarea.py
+++ b/nicegui/elements/textarea.py
@@ -21,6 +21,7 @@ class Textarea(Input, component='input.js'):
         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.
+        To disable the automatic validation on every value change, you can use the `without_auto_validation` method.
 
         :param label: displayed name for the textarea
         :param placeholder: text to show if no value is entered
diff --git a/website/documentation/content/stepper_documentation.py b/website/documentation/content/stepper_documentation.py
index 008e7008..c3152dd1 100644
--- a/website/documentation/content/stepper_documentation.py
+++ b/website/documentation/content/stepper_documentation.py
@@ -22,31 +22,4 @@ def main_demo() -> None:
                 ui.button('Back', on_click=stepper.previous).props('flat')
 
 
-@doc.demo('Dynamic Stepper', '''
-    Steps can be added dynamically and positioned via `ui.move()`.
-''')
-def dynamic_stepper() -> None:
-    def next_step() -> None:
-        if extra_step.value and len(stepper.default_slot.children) == 2:
-            with stepper:
-                with ui.step('extra') as extra:
-                    ui.label('Extra')
-                    with ui.stepper_navigation():
-                        ui.button('Back', on_click=stepper.previous).props('flat')
-                        ui.button('Next', on_click=stepper.next)
-                extra.move(target_index=1)
-        stepper.next()
-
-    with ui.stepper().props('vertical').classes('w-full') as stepper:
-        with ui.step('start'):
-            ui.label('Start')
-            extra_step = ui.checkbox('do extra step')
-            with ui.stepper_navigation():
-                ui.button('Next', on_click=next_step)
-        with ui.step('finish'):
-            ui.label('Finish')
-            with ui.stepper_navigation():
-                ui.button('Back', on_click=stepper.previous).props('flat')
-
-
 doc.reference(ui.stepper)


Test patch:
diff --git a/tests/test_select.py b/tests/test_select.py
index edec7fbe..ef7b3aeb 100644
--- a/tests/test_select.py
+++ b/tests/test_select.py
@@ -172,10 +172,17 @@ def test_keep_filtered_options(multiple: bool, screen: Screen):
         screen.should_contain('B2')
 
 
-def test_select_validation(screen: Screen):
-    ui.select(['A', 'BC', 'DEF'], value='A', validation={'Too long': lambda v: len(v) < 3})
+@pytest.mark.parametrize('auto_validation', [True, False])
+def test_select_validation(auto_validation: bool, screen: Screen):
+    select = ui.select(['A', 'BC', 'DEF'], value='A', validation={'Too long': lambda v: len(v) < 3})
+    if not auto_validation:
+        select.without_auto_validation()
 
     screen.open('/')
     screen.click('A')
     screen.click('DEF')
-    screen.should_contain('Too long')
+    screen.wait(0.5)
+    if auto_validation:
+        screen.should_contain('Too long')
+    else:
+        screen.should_not_contain('Too long')