Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
36 changes: 36 additions & 0 deletions dash-renderer/src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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} ` +
Expand Down Expand Up @@ -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);

Expand Down
3 changes: 2 additions & 1 deletion dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
5 changes: 5 additions & 0 deletions dash/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI @rpkyle - I think this namespace exclusion is the only piece of this PR that's relevant to R


self.namespace = namespace
self.function_name = function_name

Expand Down
14 changes: 14 additions & 0 deletions tests/integration/clientside/assets/clientside.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
85 changes: 84 additions & 1 deletion tests/integration/clientside/test_clientside.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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')