Skip to content

Commit e959481

Browse files
authored
Merge pull request #964 from jjaraalm/clientside_prevent_update
Clientside prevent update
2 parents 3ce895a + 14456e3 commit e959481

File tree

6 files changed

+148
-2
lines changed

6 files changed

+148
-2
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## Unreleased
6+
### Added
7+
- [#964](https://github.com/plotly/dash/pull/964) Adds support for preventing
8+
updates in clientside functions.
9+
- Reject all updates with `throw window.dash_clientside.PreventUpdate;`
10+
- Reject a single output by returning `window.dash_clientside.no_update`
11+
512
## [1.4.1] - 2019-10-17
613
### Fixed
714
- [#969](https://github.com/plotly/dash/pull/969) Fix warnings emitted by react devtools coming from our own devtools components.

dash-renderer/src/actions/index.js

+36
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,27 @@ function updateOutput(
565565
// Clientside hook
566566
if (clientside_function) {
567567
let returnValue;
568+
569+
/*
570+
* Create the dash_clientside namespace if it doesn't exist and inject
571+
* no_update and PreventUpdate.
572+
*/
573+
if (!window.dash_clientside) {
574+
window.dash_clientside = {};
575+
}
576+
577+
if (!window.dash_clientside.no_update) {
578+
Object.defineProperty(window.dash_clientside, 'no_update', {
579+
value: {description: 'Return to prevent updating an Output.'},
580+
writable: false,
581+
});
582+
583+
Object.defineProperty(window.dash_clientside, 'PreventUpdate', {
584+
value: {description: 'Throw to prevent updating all Outputs.'},
585+
writable: false,
586+
});
587+
}
588+
568589
try {
569590
returnValue = window.dash_clientside[clientside_function.namespace][
570591
clientside_function.function_name
@@ -573,6 +594,14 @@ function updateOutput(
573594
...(has('state', payload) ? pluck('value', payload.state) : [])
574595
);
575596
} catch (e) {
597+
/*
598+
* Prevent all updates.
599+
*/
600+
if (e === window.dash_clientside.PreventUpdate) {
601+
updateRequestQueue(true, STATUS.PREVENT_UPDATE);
602+
return;
603+
}
604+
576605
/* eslint-disable no-console */
577606
console.error(
578607
`The following error occurred while executing ${clientside_function.namespace}.${clientside_function.function_name} ` +
@@ -618,6 +647,13 @@ function updateOutput(
618647
*/
619648
updateRequestQueue(false, STATUS.OK);
620649

650+
/*
651+
* Prevent update.
652+
*/
653+
if (outputValue === window.dash_clientside.no_update) {
654+
return;
655+
}
656+
621657
// Update the layout with the new result
622658
const appliedProps = doUpdateProps(outputId, updatedProps);
623659

dash/_utils.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ def create_callback_id(output):
128128

129129

130130
def run_command_with_process(cmd):
131-
proc = subprocess.Popen(shlex.split(cmd, posix=sys.platform != "win32"))
131+
is_win = sys.platform == "win32"
132+
proc = subprocess.Popen(shlex.split(cmd, posix=is_win), shell=is_win)
132133
proc.wait()
133134
if proc.poll() is None:
134135
logger.warning("🚨 trying to terminate subprocess in safe way")

dash/dependencies.py

+5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ class State(DashDependency): # pylint: disable=too-few-public-methods
3535
class ClientsideFunction:
3636
# pylint: disable=too-few-public-methods
3737
def __init__(self, namespace=None, function_name=None):
38+
39+
if namespace in ['PreventUpdate', 'no_update']:
40+
raise ValueError('"{}" is a forbidden namespace in'
41+
' dash_clientside.'.format(namespace))
42+
3843
self.namespace = namespace
3944
self.function_name = function_name
4045

tests/integration/clientside/assets/clientside.js

+14
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ window.dash_clientside.clientside = {
2222
return parseInt(value, 10) + 1;
2323
},
2424

25+
add1_prevent_at_11: function (value1, value2) {
26+
if (parseInt(value1, 10) === 11) {
27+
throw window.dash_clientside.PreventUpdate;
28+
}
29+
return parseInt(value2, 10) + 1;
30+
},
31+
32+
add1_no_update_at_11: function (value1, value2, value3) {
33+
if (parseInt(value1, 10) === 11) {
34+
return [window.dash_clientside.no_update, parseInt(value3, 10) + 1];
35+
}
36+
return [parseInt(value2, 10) + 1, parseInt(value3, 10) + 1];
37+
},
38+
2539
add_to_four_outputs: function(value) {
2640
return [
2741
parseInt(value) + 1,

tests/integration/clientside/test_clientside.py

+84-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import dash_html_components as html
55
import dash_core_components as dcc
66
from dash import Dash
7-
from dash.dependencies import Input, Output, ClientsideFunction
7+
from dash.dependencies import Input, Output, State, ClientsideFunction
88

99

1010
def test_clsd001_simple_clientside_serverside_callback(dash_duo):
@@ -261,3 +261,86 @@ def test_clsd005_clientside_fails_when_returning_a_promise(dash_duo):
261261
dash_duo.wait_for_text_to_equal("#input", "hello")
262262
dash_duo.wait_for_text_to_equal("#side-effect", "side effect")
263263
dash_duo.wait_for_text_to_equal("#output", "output")
264+
265+
266+
def test_clsd006_PreventUpdate(dash_duo):
267+
app = Dash(__name__, assets_folder="assets")
268+
269+
app.layout = html.Div(
270+
[
271+
dcc.Input(id="first", value=1),
272+
dcc.Input(id="second", value=1),
273+
dcc.Input(id="third", value=1)
274+
]
275+
)
276+
277+
app.clientside_callback(
278+
ClientsideFunction(namespace="clientside", function_name="add1_prevent_at_11"),
279+
Output("second", "value"),
280+
[Input("first", "value")],
281+
[State("second", "value")]
282+
)
283+
284+
app.clientside_callback(
285+
ClientsideFunction(namespace="clientside", function_name="add1_prevent_at_11"),
286+
Output("third", "value"),
287+
[Input("second", "value")],
288+
[State("third", "value")]
289+
)
290+
291+
dash_duo.start_server(app)
292+
293+
dash_duo.wait_for_text_to_equal("#first", '1')
294+
dash_duo.wait_for_text_to_equal("#second", '2')
295+
dash_duo.wait_for_text_to_equal("#third", '2')
296+
297+
dash_duo.find_element("#first").send_keys("1")
298+
299+
dash_duo.wait_for_text_to_equal("#first", '11')
300+
dash_duo.wait_for_text_to_equal("#second", '2')
301+
dash_duo.wait_for_text_to_equal("#third", '2')
302+
303+
dash_duo.find_element("#first").send_keys("1")
304+
305+
dash_duo.wait_for_text_to_equal("#first", '111')
306+
dash_duo.wait_for_text_to_equal("#second", '3')
307+
dash_duo.wait_for_text_to_equal("#third", '3')
308+
309+
310+
def test_clsd006_no_update(dash_duo):
311+
app = Dash(__name__, assets_folder="assets")
312+
313+
app.layout = html.Div(
314+
[
315+
dcc.Input(id="first", value=1),
316+
dcc.Input(id="second", value=1),
317+
dcc.Input(id="third", value=1)
318+
]
319+
)
320+
321+
app.clientside_callback(
322+
ClientsideFunction(namespace="clientside", function_name="add1_no_update_at_11"),
323+
[Output("second", "value"),
324+
Output("third", "value")],
325+
[Input("first", "value")],
326+
[State("second", "value"),
327+
State("third", "value")]
328+
)
329+
330+
dash_duo.start_server(app)
331+
332+
dash_duo.wait_for_text_to_equal("#first", '1')
333+
dash_duo.wait_for_text_to_equal("#second", '2')
334+
dash_duo.wait_for_text_to_equal("#third", '2')
335+
336+
dash_duo.find_element("#first").send_keys("1")
337+
338+
dash_duo.wait_for_text_to_equal("#first", '11')
339+
dash_duo.wait_for_text_to_equal("#second", '2')
340+
dash_duo.wait_for_text_to_equal("#third", '3')
341+
342+
dash_duo.find_element("#first").send_keys("1")
343+
344+
dash_duo.wait_for_text_to_equal("#first", '111')
345+
dash_duo.wait_for_text_to_equal("#second", '3')
346+
dash_duo.wait_for_text_to_equal("#third", '4')

0 commit comments

Comments
 (0)