Repository: plotly/dash

Issue:
Clientside PreventUpdate and no_update
It would be helpful for clientside callbacks to be able to conditionally update components. This is not supported right now (as far as I can tell). This should be pretty simple, my idea would be to define

```js
window.dash_clientside.no_update = {};
```

then in `updateClientsideOutput` just conditionally block updating by adding

```js
if(window.dash_clientside.no_update && outputValue === window.dash_clientside.no_update)
    return;
```

Similarly we could define `window.dash_clientside.PreventUpdate` and allow the use of
```js
throw window.dash_clientside.PreventUpdate;
``` 
If this seems reasonable I could put together a PR.



Discussion:
Good idea! Yes, I think everything you're proposing makes sense, feel free to make a PR. One thing I'd add then is that `no_update` and `PreventUpdate` need to be forbidden as namespaces in `dash_clientside`.
Yes, this is what I wanted to get feedback on first. I'm not sure that `dash_clientside` is actually the best place to put these as we expect it to be modified by users. The only way I see to do this reliably is to insert `no_update` and `PreventUpdate` as non-writable attributes after all user callbacks are loaded (practically, just insert them before invoking any callbacks), but then what do we do if a user has already defined them? I guess just gracefully proceed as if `no_update` and `PreventUpdate` didn't exist, otherwise it could be a breaking change.

Is there a better namespace to put these in?
I think that’s the right place. Yes, you could argue that forbidding those two names is a breaking change, but it seems to me extremely unlikely anyone has used those as a namespace so far. I’d put a check on the Python side that errors if those namespaces are referenced, and add them to `dash_clientside` during callback invocation. 
Sounds like a plan. I'll put one together shortly.

Resolving patch:
diff --git a/dash/_utils.py b/dash/_utils.py
--- a/dash/_utils.py
+++ b/dash/_utils.py
@@ -128,7 +128,8 @@ def create_callback_id(output):
 
 
 def run_command_with_process(cmd):
-    proc = subprocess.Popen(shlex.split(cmd, posix=sys.platform != "win32"))
+    is_win = sys.platform == "win32"
+    proc = subprocess.Popen(shlex.split(cmd, posix=is_win), shell=is_win)
     proc.wait()
     if proc.poll() is None:
         logger.warning("🚨 trying to terminate subprocess in safe way")
diff --git a/dash/dependencies.py b/dash/dependencies.py
--- a/dash/dependencies.py
+++ b/dash/dependencies.py
@@ -35,6 +35,11 @@ class State(DashDependency):  # pylint: disable=too-few-public-methods
 class ClientsideFunction:
     # pylint: disable=too-few-public-methods
     def __init__(self, namespace=None, function_name=None):
+
+        if namespace in ['PreventUpdate', 'no_update']:
+            raise ValueError('"{}" is a forbidden namespace in'
+                             ' dash_clientside.'.format(namespace))
+
         self.namespace = namespace
         self.function_name = function_name
 


Test patch:
diff --git a/tests/integration/clientside/assets/clientside.js b/tests/integration/clientside/assets/clientside.js
--- a/tests/integration/clientside/assets/clientside.js
+++ b/tests/integration/clientside/assets/clientside.js
@@ -22,6 +22,20 @@ window.dash_clientside.clientside = {
         return parseInt(value, 10) + 1;
     },
 
+    add1_prevent_at_11: function (value1, value2) {
+        if (parseInt(value1, 10) === 11) {
+            throw window.dash_clientside.PreventUpdate;
+        }
+        return parseInt(value2, 10) + 1;
+    },
+
+    add1_no_update_at_11: function (value1, value2, value3) {
+        if (parseInt(value1, 10) === 11) {
+            return [window.dash_clientside.no_update, parseInt(value3, 10) + 1];
+        }
+        return [parseInt(value2, 10) + 1, parseInt(value3, 10) + 1];
+    },
+
     add_to_four_outputs: function(value) {
         return [
             parseInt(value) + 1,
diff --git a/tests/integration/clientside/test_clientside.py b/tests/integration/clientside/test_clientside.py
--- a/tests/integration/clientside/test_clientside.py
+++ b/tests/integration/clientside/test_clientside.py
@@ -4,7 +4,7 @@
 import dash_html_components as html
 import dash_core_components as dcc
 from dash import Dash
-from dash.dependencies import Input, Output, ClientsideFunction
+from dash.dependencies import Input, Output, State, ClientsideFunction
 
 
 def test_clsd001_simple_clientside_serverside_callback(dash_duo):
@@ -261,3 +261,86 @@ def test_clsd005_clientside_fails_when_returning_a_promise(dash_duo):
     dash_duo.wait_for_text_to_equal("#input", "hello")
     dash_duo.wait_for_text_to_equal("#side-effect", "side effect")
     dash_duo.wait_for_text_to_equal("#output", "output")
+
+
+def test_clsd006_PreventUpdate(dash_duo):
+    app = Dash(__name__, assets_folder="assets")
+
+    app.layout = html.Div(
+        [
+            dcc.Input(id="first", value=1),
+            dcc.Input(id="second", value=1),
+            dcc.Input(id="third", value=1)
+        ]
+    )
+
+    app.clientside_callback(
+        ClientsideFunction(namespace="clientside", function_name="add1_prevent_at_11"),
+        Output("second", "value"),
+        [Input("first", "value")],
+        [State("second", "value")]
+    )
+
+    app.clientside_callback(
+        ClientsideFunction(namespace="clientside", function_name="add1_prevent_at_11"),
+        Output("third", "value"),
+        [Input("second", "value")],
+        [State("third", "value")]
+    )
+
+    dash_duo.start_server(app)
+
+    dash_duo.wait_for_text_to_equal("#first", '1')
+    dash_duo.wait_for_text_to_equal("#second", '2')
+    dash_duo.wait_for_text_to_equal("#third", '2')
+
+    dash_duo.find_element("#first").send_keys("1")
+
+    dash_duo.wait_for_text_to_equal("#first", '11')
+    dash_duo.wait_for_text_to_equal("#second", '2')
+    dash_duo.wait_for_text_to_equal("#third", '2')
+
+    dash_duo.find_element("#first").send_keys("1")
+
+    dash_duo.wait_for_text_to_equal("#first", '111')
+    dash_duo.wait_for_text_to_equal("#second", '3')
+    dash_duo.wait_for_text_to_equal("#third", '3')
+
+
+def test_clsd006_no_update(dash_duo):
+    app = Dash(__name__, assets_folder="assets")
+
+    app.layout = html.Div(
+        [
+            dcc.Input(id="first", value=1),
+            dcc.Input(id="second", value=1),
+            dcc.Input(id="third", value=1)
+        ]
+    )
+
+    app.clientside_callback(
+        ClientsideFunction(namespace="clientside", function_name="add1_no_update_at_11"),
+        [Output("second", "value"),
+         Output("third", "value")],
+        [Input("first", "value")],
+        [State("second", "value"),
+         State("third", "value")]
+    )
+
+    dash_duo.start_server(app)
+
+    dash_duo.wait_for_text_to_equal("#first", '1')
+    dash_duo.wait_for_text_to_equal("#second", '2')
+    dash_duo.wait_for_text_to_equal("#third", '2')
+
+    dash_duo.find_element("#first").send_keys("1")
+
+    dash_duo.wait_for_text_to_equal("#first", '11')
+    dash_duo.wait_for_text_to_equal("#second", '2')
+    dash_duo.wait_for_text_to_equal("#third", '3')
+
+    dash_duo.find_element("#first").send_keys("1")
+
+    dash_duo.wait_for_text_to_equal("#first", '111')
+    dash_duo.wait_for_text_to_equal("#second", '3')
+    dash_duo.wait_for_text_to_equal("#third", '4')