diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dc3c1a607..2ec6c79825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased +### Added +- [#964](https://github.com/plotly/dash/pull/964) Adds support for preventing +updates in clientside functions. + - Reject all updates with `throw window.dash_clientside.PreventUpdate;` + - Reject a single output by returning `window.dash_clientside.no_update` + ## [1.4.1] - 2019-10-17 ### Fixed - [#969](https://github.com/plotly/dash/pull/969) Fix warnings emitted by react devtools coming from our own devtools components. diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 0d16bdfffa..18ce4d4d1d 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -565,6 +565,27 @@ function updateOutput( // Clientside hook if (clientside_function) { let returnValue; + + /* + * Create the dash_clientside namespace if it doesn't exist and inject + * no_update and PreventUpdate. + */ + if (!window.dash_clientside) { + window.dash_clientside = {}; + } + + if (!window.dash_clientside.no_update) { + Object.defineProperty(window.dash_clientside, 'no_update', { + value: {description: 'Return to prevent updating an Output.'}, + writable: false, + }); + + Object.defineProperty(window.dash_clientside, 'PreventUpdate', { + value: {description: 'Throw to prevent updating all Outputs.'}, + writable: false, + }); + } + try { returnValue = window.dash_clientside[clientside_function.namespace][ clientside_function.function_name @@ -573,6 +594,14 @@ function updateOutput( ...(has('state', payload) ? pluck('value', payload.state) : []) ); } catch (e) { + /* + * Prevent all updates. + */ + if (e === window.dash_clientside.PreventUpdate) { + updateRequestQueue(true, STATUS.PREVENT_UPDATE); + return; + } + /* eslint-disable no-console */ console.error( `The following error occurred while executing ${clientside_function.namespace}.${clientside_function.function_name} ` + @@ -618,6 +647,13 @@ function updateOutput( */ updateRequestQueue(false, STATUS.OK); + /* + * Prevent update. + */ + if (outputValue === window.dash_clientside.no_update) { + return; + } + // Update the layout with the new result const appliedProps = doUpdateProps(outputId, updatedProps); diff --git a/dash/_utils.py b/dash/_utils.py index da0a79da1b..4816864385 100644 --- 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 index 9ba9a9fbff..314cf7db75 100644 --- 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 diff --git a/tests/integration/clientside/assets/clientside.js b/tests/integration/clientside/assets/clientside.js index 2a02278c0b..067d7b4a1f 100644 --- 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 index c83a5ef5b9..83d8057ecf 100644 --- 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')