From 79f8c24e9f055c9cb6572f20014d67b670ba14fb Mon Sep 17 00:00:00 2001 From: Chris P Date: Sat, 30 Mar 2019 13:13:26 -0400 Subject: [PATCH 1/7] :racehorse: clientside callback interface see associated PR & examples in https://github.com/plotly/dash-renderer/pull/143 --- dash/dash.py | 13 +++++++++++-- dash/dependencies.py | 13 +++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 040152f29b..ed1460fab2 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -631,6 +631,7 @@ def dependencies(self): 'output': k, 'inputs': v['inputs'], 'state': v['state'], + 'client_function': v['client_function'] } for k, v in self.callback_map.items() ]) @@ -946,7 +947,7 @@ def _validate_value(val, index=None): # TODO - Check this map for recursive or other ill-defined non-tree # relationships # pylint: disable=dangerous-default-value - def callback(self, output, inputs=[], state=[]): + def callback(self, output, inputs=[], state=[], client_function=None): self._validate_callback(output, inputs, state) callback_id = _create_callback_id(output) @@ -960,8 +961,16 @@ def callback(self, output, inputs=[], state=[]): 'state': [ {'id': c.component_id, 'property': c.component_property} for c in state - ] + ], } + if client_function is not None: + self.callback_map[callback_id]['client_function'] = { + 'namespace': client_function.namespace, + 'function_name': client_function.function_name + } + else: + self.callback_map[callback_id]['client_function'] = {} + def wrap_func(func): @wraps(func) diff --git a/dash/dependencies.py b/dash/dependencies.py index 3f946f1ebf..1e7985c4cc 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -32,3 +32,16 @@ class Input(DashDependency): # pylint: disable=too-few-public-methods class State(DashDependency): """Use the value of a state in a callback but don't trigger updates.""" + + +# pylint: disable=too-few-public-methods +class ClientFunction: + def __init__(self, namespace=None, function_name=None): + self.namespace = namespace + self.function_name = function_name + + def __repr__(self): + return 'ClientFunction({}, {})'.format( + self.namespace, + self.function_name + ) From acd1e33fa120404329fc2ca3badae6cccf61aae6 Mon Sep 17 00:00:00 2001 From: Chris P Date: Sat, 30 Mar 2019 18:24:38 -0400 Subject: [PATCH 2/7] :pencil: don't overload `callback` with `clientside` and rename `client_function` to `clientside_function --- dash/dash.py | 71 +++++++++++++++++++++++++++++++++++++------- dash/dependencies.py | 4 +-- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index ed1460fab2..1b66150d66 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -631,7 +631,7 @@ def dependencies(self): 'output': k, 'inputs': v['inputs'], 'state': v['state'], - 'client_function': v['client_function'] + 'clientside_function': v.get('clientside_function', {}) } for k, v in self.callback_map.items() ]) @@ -935,6 +935,65 @@ def _validate_value(val, index=None): else: _validate_value(output_value) + + def clientside_callback(self, clientside_function, output, inputs=[], state=[]): + """ + Create a callback that updates the output by calling a clientside + (JavaScript) function instead of a Python function. + + Unlike `@app.calllback`, `clientside_callback` is not a decorator: + it takes a `dash.dependencies.ClientsideFunction(namespace, function_name)` + argument that describes which JavaScript function to call + (Dash will look for the JavaScript function at `window[namespace][function_name]`). + + For example: + ``` + app.clientside_callback( + ClientsideFunction('my_clientside_library', 'my_function'), + Output('my-div' 'children'), + [Input('my-input', 'value'), + Input('another-input', 'value')] + ) + ``` + + With this signature, Dash's front-end will call + `window.my_clientside_library.my_function` with the current values of + the `value` properties of the components `my-input` and `another-input` + whenver those values change. + + Include a JavaScript file by including it your `assets/` folder. + The file can be named anything but you'll need to assign the + function's namespace to the `window`. For example, this file might + look like: + ``` + window.my_clientside_library = { + my_function: function(input_value_1, input_value_2) { + return ( + parseFloat(input_value_1, 10) + + parseFloat(input_value_2, 10) + ); + } + } + ``` + """ + self._validate_callback(output, inputs, state) + callback_id = _create_callback_id(output) + + self.callback_map[callback_id] = { + 'inputs': [ + {'id': c.component_id, 'property': c.component_property} + for c in inputs + ], + 'state': [ + {'id': c.component_id, 'property': c.component_property} + for c in state + ], + 'clientside_function': { + 'namespace': clientside_function.namespace, + 'function_name': clientside_function.function_name + } + } + # TODO - Update nomenclature. # "Parents" and "Children" should refer to the DOM tree # and not the dependency tree. @@ -947,7 +1006,7 @@ def _validate_value(val, index=None): # TODO - Check this map for recursive or other ill-defined non-tree # relationships # pylint: disable=dangerous-default-value - def callback(self, output, inputs=[], state=[], client_function=None): + def callback(self, output, inputs=[], state=[]): self._validate_callback(output, inputs, state) callback_id = _create_callback_id(output) @@ -963,14 +1022,6 @@ def callback(self, output, inputs=[], state=[], client_function=None): for c in state ], } - if client_function is not None: - self.callback_map[callback_id]['client_function'] = { - 'namespace': client_function.namespace, - 'function_name': client_function.function_name - } - else: - self.callback_map[callback_id]['client_function'] = {} - def wrap_func(func): @wraps(func) diff --git a/dash/dependencies.py b/dash/dependencies.py index 1e7985c4cc..f84f7da98f 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -35,13 +35,13 @@ class State(DashDependency): # pylint: disable=too-few-public-methods -class ClientFunction: +class ClientsideFunction: def __init__(self, namespace=None, function_name=None): self.namespace = namespace self.function_name = function_name def __repr__(self): - return 'ClientFunction({}, {})'.format( + return 'ClientsideFunction({}, {})'.format( self.namespace, self.function_name ) From b9886d9ccbe36b8e7e9f830e68cdca56d3fc797f Mon Sep 17 00:00:00 2001 From: Chris P Date: Mon, 8 Apr 2019 11:17:44 -0700 Subject: [PATCH 3/7] :white_check_mark: straighten up the pylint directives as per https://github.com/plotly/dash/pull/672#discussion_r272797620 --- dash/dependencies.py | 12 +++++------- dash/version.py | 2 +- pbcopy | 1 + 3 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 pbcopy diff --git a/dash/dependencies.py b/dash/dependencies.py index f84f7da98f..9ba9a9fbff 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -1,4 +1,5 @@ class DashDependency: + # pylint: disable=too-few-public-methods def __init__(self, component_id, component_property): self.component_id = component_id self.component_property = component_property @@ -19,23 +20,20 @@ def __hash__(self): return hash(str(self)) -# pylint: disable=too-few-public-methods -class Output(DashDependency): +class Output(DashDependency): # pylint: disable=too-few-public-methods """Output of a callback.""" -# pylint: disable=too-few-public-methods -class Input(DashDependency): +class Input(DashDependency): # pylint: disable=too-few-public-methods """Input of callback trigger an update when it is updated.""" -# pylint: disable=too-few-public-methods -class State(DashDependency): +class State(DashDependency): # pylint: disable=too-few-public-methods """Use the value of a state in a callback but don't trigger updates.""" -# pylint: disable=too-few-public-methods class ClientsideFunction: + # pylint: disable=too-few-public-methods def __init__(self, namespace=None, function_name=None): self.namespace = namespace self.function_name = function_name diff --git a/dash/version.py b/dash/version.py index eb9b6f12e8..6c87873231 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1 +1 @@ -__version__ = '0.40.0' +__version__ = '0.40.0rc1' diff --git a/pbcopy b/pbcopy new file mode 100644 index 0000000000..2f5d09b7bc --- /dev/null +++ b/pbcopy @@ -0,0 +1 @@ +/Users/chriddyp/Repos/dash-stuff/dash From 7281b7d5c12eb13dd93cdee20ce762d1e7053466 Mon Sep 17 00:00:00 2001 From: Chris P Date: Mon, 8 Apr 2019 11:23:23 -0700 Subject: [PATCH 4/7] :zap: No clientside function = `None` --- dash/dash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 1b66150d66..fc7db45425 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -631,7 +631,7 @@ def dependencies(self): 'output': k, 'inputs': v['inputs'], 'state': v['state'], - 'clientside_function': v.get('clientside_function', {}) + 'clientside_function': v.get('clientside_function', None) } for k, v in self.callback_map.items() ]) From 9b05b627a77dbdb4bb0437445103a2bfcc2a44f8 Mon Sep 17 00:00:00 2001 From: Chris P Date: Mon, 8 Apr 2019 11:26:19 -0700 Subject: [PATCH 5/7] :pencil: CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 705a66a95d..99057b6966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased +### Added +- Support for "Clientside Callbacks" - an escape hatch to execute your callbacks in JavaScript instead of Python [#672](https://github.com/plotly/dash/pull/672) + ## [0.40.0] - 2019-03-25 ### Changed - Bumped dash-core-components version from 0.44.0 to [0.45.0](https://github.com/plotly/dash-core-components/blob/master/CHANGELOG.md#0450---2019-03-25) From e1e5dde18568293dcd4edebf5624bb801a8a1ec2 Mon Sep 17 00:00:00 2001 From: Chris P Date: Mon, 8 Apr 2019 11:48:28 -0700 Subject: [PATCH 6/7] :pencil: pylint --- dash/dash.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index fc7db45425..14b2b4d4aa 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -935,16 +935,19 @@ def _validate_value(val, index=None): else: _validate_value(output_value) - - def clientside_callback(self, clientside_function, output, inputs=[], state=[]): + # pylint: disable=dangerous-default-value + def clientside_callback( + self, clientside_function, output, inputs=[], state=[]): """ Create a callback that updates the output by calling a clientside (JavaScript) function instead of a Python function. Unlike `@app.calllback`, `clientside_callback` is not a decorator: - it takes a `dash.dependencies.ClientsideFunction(namespace, function_name)` + it takes a + `dash.dependencies.ClientsideFunction(namespace, function_name)` argument that describes which JavaScript function to call - (Dash will look for the JavaScript function at `window[namespace][function_name]`). + (Dash will look for the JavaScript function at + `window[namespace][function_name]`). For example: ``` @@ -957,9 +960,9 @@ def clientside_callback(self, clientside_function, output, inputs=[], state=[]): ``` With this signature, Dash's front-end will call - `window.my_clientside_library.my_function` with the current values of - the `value` properties of the components `my-input` and `another-input` - whenver those values change. + `window.my_clientside_library.my_function` with the current + values of the `value` properties of the components + `my-input` and `another-input` whenever those values change. Include a JavaScript file by including it your `assets/` folder. The file can be named anything but you'll need to assign the From ff92f226c9636f17a527b0a8500834168d9ca7db Mon Sep 17 00:00:00 2001 From: Chris P Date: Mon, 8 Apr 2019 11:59:21 -0700 Subject: [PATCH 7/7] :zap: unused files --- dash/version.py | 2 +- pbcopy | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 pbcopy diff --git a/dash/version.py b/dash/version.py index 6c87873231..eb9b6f12e8 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1 +1 @@ -__version__ = '0.40.0rc1' +__version__ = '0.40.0' diff --git a/pbcopy b/pbcopy deleted file mode 100644 index 2f5d09b7bc..0000000000 --- a/pbcopy +++ /dev/null @@ -1 +0,0 @@ -/Users/chriddyp/Repos/dash-stuff/dash