Repository: zauberzeug/nicegui
Pull request #3346 (2024-07-12 10:50:24)

Fix value updates in ui.editor after client-side changes
This PR solves #3217 by applying the same strategy like `ui.input`: In a custom Vue component we use an `inputValue` as data model, which keeps track of the client-side value changes when typing in the editor. This property is used to detect changes when the server sends updates.

You can try it out with this code snippet:
```py
editor = ui.editor()
ui.label().bind_text_from(editor, 'value')
ui.button('set x', on_click=lambda: editor.set_value('x'))
```

Now the "set x" button works repeatedly.


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d48dae30..75646d8c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -75,24 +75,6 @@ To view the log output, use the command
 
 ### Formatting
 
-We use [pre-commit](https://github.com/pre-commit/pre-commit) to make sure the coding style is enforced.
-You first need to install pre-commit and the corresponding git commit hooks by running the following commands:
-
-```bash
-python3 -m pip install pre-commit
-pre-commit install
-```
-
-After that you can make sure your code satisfies the coding style by running the following command:
-
-```bash
-pre-commit run --all-files
-```
-
-These checks will also run automatically before every commit.
-
-### Formatting
-
 We use [autopep8](https://github.com/hhatto/autopep8) with a 120 character line length to format our code.
 Before submitting a pull request, please run
 
@@ -109,24 +91,46 @@ There are cases where one or the other arrangement of, e.g., function arguments
 Then we like the flexibility to either put all arguments on separate lines or only put the lengthy event handler
 on a second line and leave the other arguments as they are.
 
-### Imports
+### Linting
 
-We use [ruff](https://docs.astral.sh/ruff/) to automatically sort imports:
+We use [pre-commit](https://github.com/pre-commit/pre-commit) to make sure the coding style is enforced.
+You first need to install pre-commit and the corresponding git commit hooks by running the following commands:
 
 ```bash
-ruff check . --fix
+python3 -m pip install pre-commit
+pre-commit install
 ```
 
-### Single vs Double Quotes
-
-Regarding single or double quotes: [PEP 8](https://peps.python.org/pep-0008/) doesn't give any recommendation, so we simply chose single quotes and sticked with it.
-On qwerty keyboards it's a bit easier to type, is visually less cluttered, and it works well for strings containing double quotes from the English language.
+After that you can make sure your code satisfies the coding style by running the following command:
 
-### F-Strings
+```bash
+pre-commit run --all-files
+```
 
-We use f-strings where ever possible because they are generally more readable - once you get used to them.
-There are only a few places in the code base where performance really matters and f-strings might not be the best choice.
-These places should be marked with a `# NOTE: ...` comment when diverging from f-string usage.
+> [!TIP]
+> The command may fail with
+>
+> > RuntimeError: failed to find interpreter for Builtin discover of python_spec='python3.8'
+>
+> You will need to install Python 3.8 and make sure it is available in your `PATH`.
+
+These checks will also run automatically before every commit:
+
+- Run `ruff check . --fix` to check the code and sort imports.
+- Remove trailing whitespace.
+- Fix end of files.
+- Enforce single quotes.
+
+> [!NOTE]
+>
+> **Regarding single or double quotes:** > [PEP 8](https://peps.python.org/pep-0008/) doesn't give any recommendation, so we simply chose single quotes and sticked with it.
+> On qwerty keyboards it's a bit easier to type, is visually less cluttered, and it works well for strings containing double quotes from the English language.
+
+> [!NOTE]
+>
+> **We use f-strings** where ever possible because they are generally more readable - once you get used to them.
+> There are only a few places in the code base where performance really matters and f-strings might not be the best choice.
+> These places should be marked with a `# NOTE: ...` comment when diverging from f-string usage.
 
 ## Running tests
 
diff --git a/nicegui/app/app.py b/nicegui/app/app.py
index 0f5dc941..535d706a 100644
--- a/nicegui/app/app.py
+++ b/nicegui/app/app.py
@@ -1,4 +1,7 @@
 import inspect
+import os
+import platform
+import signal
 import urllib
 from enum import Enum
 from pathlib import Path
@@ -124,12 +127,11 @@ class App(FastAPI):
         """Shut down NiceGUI.
 
         This will programmatically stop the server.
-        Only possible when auto-reload is disabled.
         """
-        if self.config.reload:
-            raise RuntimeError('calling shutdown() is not supported when auto-reload is enabled')
         if self.native.main_window:
             self.native.main_window.destroy()
+        if self.config.reload:
+            os.kill(os.getppid(), getattr(signal, 'CTRL_C_EVENT' if platform.system() == 'Windows' else 'SIGINT'))
         else:
             Server.instance.should_exit = True
 
diff --git a/nicegui/elements/editor.js b/nicegui/elements/editor.js
new file mode 100644
index 00000000..9ab25a6b
--- /dev/null
+++ b/nicegui/elements/editor.js
@@ -0,0 +1,38 @@
+export default {
+  template: `
+    <q-editor
+      ref="qRef"
+      v-bind="$attrs"
+      v-model="inputValue"
+    >
+      <template v-for="(_, slot) in $slots" v-slot:[slot]="slotProps">
+        <slot :name="slot" v-bind="slotProps || {}" />
+      </template>
+    </q-input>
+  `,
+  props: {
+    value: String,
+  },
+  data() {
+    return {
+      inputValue: this.value,
+      emitting: true,
+    };
+  },
+  watch: {
+    value(newValue) {
+      this.emitting = false;
+      this.inputValue = newValue;
+      this.$nextTick(() => (this.emitting = true));
+    },
+    inputValue(newValue) {
+      if (!this.emitting) return;
+      this.$emit("update:value", newValue);
+    },
+  },
+  methods: {
+    updateValue() {
+      this.inputValue = this.value;
+    },
+  },
+};
diff --git a/nicegui/elements/editor.py b/nicegui/elements/editor.py
index a9ce450b..aea5330e 100644
--- a/nicegui/elements/editor.py
+++ b/nicegui/elements/editor.py
@@ -4,8 +4,9 @@ from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
 
-class Editor(ValueElement, DisableableElement):
-    LOOPBACK = None
+class Editor(ValueElement, DisableableElement, component='editor.js'):
+    VALUE_PROP: str = 'value'
+    LOOPBACK = False
 
     def __init__(self,
                  *,
@@ -21,7 +22,12 @@ class Editor(ValueElement, DisableableElement):
         :param value: initial value
         :param on_change: callback to be invoked when the value changes
         """
-        super().__init__(tag='q-editor', value=value, on_value_change=on_change)
+        super().__init__(value=value, on_value_change=on_change)
         self._classes.append('nicegui-editor')
         if placeholder is not None:
             self._props['placeholder'] = placeholder
+
+    def _handle_value_change(self, value: Any) -> None:
+        super()._handle_value_change(value)
+        if self._send_update_on_value_change:
+            self.run_method('updateValue')
diff --git a/nicegui/elements/number.py b/nicegui/elements/number.py
index e4691d95..ae45487a 100644
--- a/nicegui/elements/number.py
+++ b/nicegui/elements/number.py
@@ -116,6 +116,7 @@ class Number(ValidationElement, DisableableElement):
         if self.precision is not None:
             value = float(round(value, self.precision))
         self.set_value(float(self.format % value) if self.format else value)
+        self.update()
 
     def _event_args_to_value(self, e: GenericEventArguments) -> Any:
         if not e.args:
diff --git a/poetry.lock b/poetry.lock
index 2930cb88..8f945b6f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
 
 [[package]]
 name = "aiofiles"
@@ -220,27 +220,6 @@ files = [
 pycodestyle = ">=2.9.1"
 toml = "*"
 
-[[package]]
-name = "beautifulsoup4"
-version = "4.12.3"
-description = "Screen-scraping library"
-optional = false
-python-versions = ">=3.6.0"
-files = [
-    {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
-    {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
-]
-
-[package.dependencies]
-soupsieve = ">1.2"
-
-[package.extras]
-cchardet = ["cchardet"]
-chardet = ["chardet"]
-charset-normalizer = ["charset-normalizer"]
-html5lib = ["html5lib"]
-lxml = ["lxml"]
-
 [[package]]
 name = "bidict"
 version = "0.23.1"
@@ -675,13 +654,13 @@ files = [
 
 [[package]]
 name = "exceptiongroup"
-version = "1.2.1"
+version = "1.2.2"
 description = "Backport of PEP 654 (exception groups)"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
-    {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
+    {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
+    {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
 ]
 
 [package.extras]
@@ -1245,18 +1224,19 @@ files = [
 
 [[package]]
 name = "markdown2"
-version = "2.4.13"
+version = "2.5.0"
 description = "A fast and complete Python implementation of Markdown"
 optional = false
-python-versions = ">=3.5, <4"
+python-versions = "<4,>=3.8"
 files = [
-    {file = "markdown2-2.4.13-py2.py3-none-any.whl", hash = "sha256:855bde5cbcceb9beda7c80efdf7f406c23e6079172c497fcfce22fdce998e892"},
-    {file = "markdown2-2.4.13.tar.gz", hash = "sha256:18ceb56590da77f2c22382e55be48c15b3c8f0c71d6398def387275e6c347a9f"},
+    {file = "markdown2-2.5.0-py2.py3-none-any.whl", hash = "sha256:300d4429b620ebc974ef512339a9e08bc080473f95135a91f33906e24e8280c1"},
+    {file = "markdown2-2.5.0.tar.gz", hash = "sha256:9bff02911f8b617b61eb269c4c1a5f9b2087d7ff051604f66a61b63cab30adc2"},
 ]
 
 [package.extras]
-all = ["pygments (>=2.7.3)", "wavedrom"]
+all = ["latex2mathml", "pygments (>=2.7.3)", "wavedrom"]
 code-syntax-highlighting = ["pygments (>=2.7.3)"]
+latex = ["latex2mathml"]
 wavedrom = ["wavedrom"]
 
 [[package]]
@@ -1917,13 +1897,13 @@ virtualenv = ">=20.10.0"
 
 [[package]]
 name = "prettytable"
-version = "3.10.0"
+version = "3.10.2"
 description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "prettytable-3.10.0-py3-none-any.whl", hash = "sha256:6536efaf0757fdaa7d22e78b3aac3b69ea1b7200538c2c6995d649365bddab92"},
-    {file = "prettytable-3.10.0.tar.gz", hash = "sha256:9665594d137fb08a1117518c25551e0ede1687197cf353a4fdc78d27e1073568"},
+    {file = "prettytable-3.10.2-py3-none-any.whl", hash = "sha256:1cbfdeb4bcc73976a778a0fb33cb6d752e75396f16574dcb3e2d6332fd93c76a"},
+    {file = "prettytable-3.10.2.tar.gz", hash = "sha256:29ec6c34260191d42cd4928c28d56adec360ac2b1208a26c7e4f14b90cc8bc84"},
 ]
 
 [package.dependencies]
@@ -2260,13 +2240,13 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
 
 [[package]]
 name = "pytest-asyncio"
-version = "0.23.7"
+version = "0.23.8"
 description = "Pytest support for asyncio"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
-    {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
+    {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"},
+    {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"},
 ]
 
 [package.dependencies]
@@ -2625,29 +2605,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "ruff"
-version = "0.5.1"
+version = "0.5.4"
 description = "An extremely fast Python linter and code formatter, written in Rust."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"},
-    {file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"},
-    {file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"},
-    {file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"},
-    {file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"},
-    {file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"},
-    {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"},
-    {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"},
-    {file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"},
-    {file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"},
-    {file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"},
-    {file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"},
-    {file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"},
-    {file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"},
-    {file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"},
-    {file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"},
-    {file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"},
-    {file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"},
+    {file = "ruff-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:82acef724fc639699b4d3177ed5cc14c2a5aacd92edd578a9e846d5b5ec18ddf"},
+    {file = "ruff-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:da62e87637c8838b325e65beee485f71eb36202ce8e3cdbc24b9fcb8b99a37be"},
+    {file = "ruff-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e98ad088edfe2f3b85a925ee96da652028f093d6b9b56b76fc242d8abb8e2059"},
+    {file = "ruff-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c55efbecc3152d614cfe6c2247a3054cfe358cefbf794f8c79c8575456efe19"},
+    {file = "ruff-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9b85eaa1f653abd0a70603b8b7008d9e00c9fa1bbd0bf40dad3f0c0bdd06793"},
+    {file = "ruff-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cf497a47751be8c883059c4613ba2f50dd06ec672692de2811f039432875278"},
+    {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:09c14ed6a72af9ccc8d2e313d7acf7037f0faff43cde4b507e66f14e812e37f7"},
+    {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:628f6b8f97b8bad2490240aa84f3e68f390e13fabc9af5c0d3b96b485921cd60"},
+    {file = "ruff-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3520a00c0563d7a7a7c324ad7e2cde2355733dafa9592c671fb2e9e3cd8194c1"},
+    {file = "ruff-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93789f14ca2244fb91ed481456f6d0bb8af1f75a330e133b67d08f06ad85b516"},
+    {file = "ruff-0.5.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:029454e2824eafa25b9df46882f7f7844d36fd8ce51c1b7f6d97e2615a57bbcc"},
+    {file = "ruff-0.5.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9492320eed573a13a0bc09a2957f17aa733fff9ce5bf00e66e6d4a88ec33813f"},
+    {file = "ruff-0.5.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6e1f62a92c645e2919b65c02e79d1f61e78a58eddaebca6c23659e7c7cb4ac7"},
+    {file = "ruff-0.5.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:768fa9208df2bec4b2ce61dbc7c2ddd6b1be9fb48f1f8d3b78b3332c7d71c1ff"},
+    {file = "ruff-0.5.4-py3-none-win32.whl", hash = "sha256:e1e7393e9c56128e870b233c82ceb42164966f25b30f68acbb24ed69ce9c3a4e"},
+    {file = "ruff-0.5.4-py3-none-win_amd64.whl", hash = "sha256:58b54459221fd3f661a7329f177f091eb35cf7a603f01d9eb3eb11cc348d38c4"},
+    {file = "ruff-0.5.4-py3-none-win_arm64.whl", hash = "sha256:bd53da65f1085fb5b307c38fd3c0829e76acf7b2a912d8d79cadcdb4875c1eb7"},
+    {file = "ruff-0.5.4.tar.gz", hash = "sha256:2795726d5f71c4f4e70653273d1c23a8182f07dd8e48c12de5d867bfb7557eed"},
 ]
 
 [[package]]
@@ -2837,17 +2817,6 @@ files = [
     {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
 ]
 
-[[package]]
-name = "soupsieve"
-version = "2.5"
-description = "A modern CSS selector implementation for Beautiful Soup."
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"},
-    {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"},
-]
-
 [[package]]
 name = "starlette"
 version = "0.36.3"
@@ -2983,13 +2952,13 @@ zstd = ["zstandard (>=0.18.0)"]
 
 [[package]]
 name = "uvicorn"
-version = "0.30.1"
+version = "0.30.3"
 description = "The lightning-fast ASGI server."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"},
-    {file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"},
+    {file = "uvicorn-0.30.3-py3-none-any.whl", hash = "sha256:94a3608da0e530cea8f69683aa4126364ac18e3826b6630d1a65f4638aade503"},
+    {file = "uvicorn-0.30.3.tar.gz", hash = "sha256:0d114d0831ff1adbf231d358cbf42f17333413042552a624ea6a9b4c33dcfd81"},
 ]
 
 [package.dependencies]
@@ -3482,4 +3451,4 @@ sass = ["libsass"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "169ca2630e7cc4b6a79297cb45353208b4352dd86919a9c459c01bb4cd8ba6f4"
+content-hash = "47d57584a4512c38b93c0b19597b72b42959077567f975d96e0c1a5c598f6306"
diff --git a/pyproject.toml b/pyproject.toml
index 2606f87c..603f1b61 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,7 @@ aiohttp = ">=3.9.2" # https://github.com/zauberzeug/nicegui/security/dependabot/
 libsass = { version = "^0.23.0", optional = true }
 docutils = "^0.19"
 requests = ">=2.32.0" # https://github.com/zauberzeug/nicegui/security/dependabot/33
+urllib3 = ">=1.26.18,!=2.0.0,!=2.0.1,!=2.0.2,!=2.0.3,!=2.0.4,!=2.0.5,!=2.0.6,!=2.0.7,!=2.1.0,!=2.2.0,!=2.2.1" # https://github.com/zauberzeug/nicegui/security/dependabot/34
 certifi = ">=2024.07.04" # https://github.com/zauberzeug/nicegui/security/dependabot/35
 
 [tool.poetry.extras]
@@ -59,8 +60,6 @@ numpy = [
     {version = "^1.26.0", python = ">=3.9,<3.13"}
 ]
 selenium = "^4.11.2"
-beautifulsoup4 = "^4.12.2"
-urllib3 = ">=1.26.18,^1.26 || >=2.2.2" # https://github.com/zauberzeug/nicegui/security/dependabot/34
 pyecharts = "^2.0.4"
 ruff = ">=0.3.5"
 pre-commit = ">=3.5.0"
diff --git a/website/documentation/content/input_documentation.py b/website/documentation/content/input_documentation.py
index 26eea678..b16cedf0 100644
--- a/website/documentation/content/input_documentation.py
+++ b/website/documentation/content/input_documentation.py
@@ -46,7 +46,7 @@ def styling():
     You can validate the input in two ways:
 
     - by passing a callable that returns an error message or `None`, or
-    - by passing a dictionary that maps error messages to callables that return `True` (error) or `False` (no error).
+    - by passing a dictionary that maps error messages to callables that return `True` if the input is valid.
 ''')
 def validation():
     ui.input('Name', validation=lambda value: 'Too short' if len(value) < 5 else None)


Test patch:
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 953685b3..08d6161b 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -4,4 +4,3 @@ pytest-asyncio
 selenium
 autopep8
 icecream
-beautifulsoup4
diff --git a/tests/test_editor.py b/tests/test_editor.py
index 78a90191..54e941ed 100644
--- a/tests/test_editor.py
+++ b/tests/test_editor.py
@@ -1,3 +1,4 @@
+from selenium.webdriver.common.keys import Keys
 
 from nicegui import ui
 from nicegui.testing import Screen
@@ -8,7 +9,23 @@ def test_editor(screen: Screen):
     ui.markdown().bind_content_from(editor, 'value', backward=lambda v: f'HTML code:\n```\n{v}\n```')
 
     screen.open('/')
-    screen.find_by_class('q-editor__content').click()
+    screen.find_element(editor).click()
     screen.type('Hello\nworld!')
     screen.wait(0.5)
     screen.should_contain('Hello<div>world!</div>')
+
+
+def test_setting_value_twice(screen: Screen):
+    # https://github.com/zauberzeug/nicegui/issues/3217
+    editor = ui.editor(value='X')
+    ui.button('Reset').on_click(lambda: editor.set_value('X'))
+
+    screen.open('/')
+    screen.should_contain('X')
+
+    screen.find_element(editor).click()
+    screen.type(Keys.BACKSPACE + 'ABC')
+    screen.should_contain('ABC')
+
+    screen.click('Reset')
+    screen.should_contain('X')
diff --git a/tests/test_favicon.py b/tests/test_favicon.py
index 979513ca..1ba66082 100644
--- a/tests/test_favicon.py
+++ b/tests/test_favicon.py
@@ -3,7 +3,6 @@ from typing import Union
 
 import pytest
 import requests
-from bs4 import BeautifulSoup
 
 from nicegui import favicon, ui
 from nicegui.testing import Screen
@@ -12,10 +11,8 @@ DEFAULT_FAVICON_PATH = Path(__file__).parent.parent / 'nicegui' / 'static' / 'fa
 LOGO_FAVICON_PATH = Path(__file__).parent.parent / 'website' / 'static' / 'logo_square.png'
 
 
-def assert_favicon_url_starts_with(screen: Screen, content: str):
-    soup = BeautifulSoup(screen.selenium.page_source, 'html.parser')
-    icon_link = soup.find('link', rel='icon')
-    assert icon_link['href'].startswith(content)
+def get_favicon_url(screen: Screen) -> str:
+    return screen.find_by_css('link[rel="shortcut icon"]').get_attribute('href')
 
 
 def assert_favicon(content: Union[Path, str, bytes], url_path: str = '/favicon.ico'):
@@ -44,7 +41,7 @@ def test_emoji(emoji: str, screen: Screen):
 
     screen.ui_run_kwargs['favicon'] = emoji
     screen.open('/')
-    assert_favicon_url_starts_with(screen, 'data:image/svg+xml')
+    assert get_favicon_url(screen).startswith('data:image/svg+xml')
     assert_favicon(favicon._char_to_svg(emoji))
 
 
@@ -54,7 +51,7 @@ def test_data_url(screen: Screen):
     icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
     screen.ui_run_kwargs['favicon'] = icon
     screen.open('/')
-    assert_favicon_url_starts_with(screen, 'data:image/png;base64')
+    assert get_favicon_url(screen).startswith('data:image/png;base64')
     _, bytes_ = favicon._data_url_to_bytes(icon)
     assert_favicon(bytes_)
 
@@ -64,7 +61,7 @@ def test_custom_file(screen: Screen):
 
     screen.ui_run_kwargs['favicon'] = LOGO_FAVICON_PATH
     screen.open('/')
-    assert_favicon_url_starts_with(screen, '/favicon.ico')
+    assert get_favicon_url(screen).endswith('/favicon.ico')
     assert_favicon(screen.ui_run_kwargs['favicon'])
 
 
@@ -88,6 +85,6 @@ def test_page_specific_emoji(screen: Screen):
     ui.label('Main')
 
     screen.open('/subpage')
-    assert_favicon_url_starts_with(screen, 'data:image/svg+xml')
+    assert get_favicon_url(screen).startswith('data:image/svg+xml')
     screen.open('/')
     assert_favicon(DEFAULT_FAVICON_PATH)
diff --git a/tests/test_number.py b/tests/test_number.py
index 0711591b..09114a15 100644
--- a/tests/test_number.py
+++ b/tests/test_number.py
@@ -30,6 +30,11 @@ def test_apply_format_on_blur(screen: Screen):
     screen.click('Button')
     screen.should_contain_input('3.1417')
 
+    element.click()
+    element.send_keys(Keys.BACKSPACE * 10 + '2')
+    screen.click('Button')
+    screen.should_contain_input('2.0000')
+
 
 def test_max_value(screen: Screen):
     ui.number('Number', min=0, max=10, value=5)