Synchronize components bidirectionally

I have a (seemingly simple) use case where i need two components to be synchronized bidirectionally as they represent the same input and are both editable. As simple example, consider a slider and text input field. If i move the slider, the text input field should update. If i edit the text field, the slider should move. The following code illustrates the point,

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Output, Input

app = dash.Dash(__name__)
app.layout = html.Div(children=[dcc.Slider(id="slider", min=0, max=10, value=5), dcc.Input(id="input", value=5)])
app.callback(Output("slider", "value"), [Input("input", "value")])(lambda x: x)  # update slider to match input
app.callback(Output("input", "value"), [Input("slider", "value")])(lambda x: x)  # update input to match slider

if __name__ == '__main__':
    app.run_server(debug=True)

but it does not work since it contains a cyclic dependency. My question is now, if there is any (simple) way to achieve the desired behaviour?

1 Like

I’m running into this problem also. I have two user ID dropdowns, one which looks up IDs by user name, and the other which looks them up based on user names. So these need to be synchronised, but I can’t think of a way to do this without the circular dependency.

Would something like this wont work?
Creating two callbacks:

  1. from two Inputs (dropdown1 and dropdown2) to hidden div.
  2. From Hidden div to two outputs (dropdown1 and dropdown2)

Yeah, this is a limitation in Dash. We need something like a “Synced Inputs” feature. Although the syncing isn’t necessary 1-1, there might be a function between the two inputs. Think of having two inputs “Weight (lbs)” and “Weight (kgs)” - the user might update one and not the other, but their values should represent the same weight. So, you’d need to write function in both directions, and Dash would fire one of the callbacks depending on which one was changed.

I actually tried just this, but it falls afoul of the circular dependency issue sadly.

Thanks @chriddyp, good to know my intuitions about this limitation were right.

A related synchronisation issue that I’ve come across is that I’ve setup up URL query parameters in my Dash router such that in the URL http://localhost:8050/home?user_id=foo the parameters and their values are supplied to functions generating layouts for each page as kwargs.

Now I want the Dropdown for the user_id field to automatically update the URL with the corresponding query param string, but this will trigger the Location callback to update the page unnecessarily. There’s probably a way to detect this in the callback using some clever flag that I can’t think of right now, but it would be great to be able to keep the values synchronised without triggering a callback in the first place.

For the location sync, I’ve been using this hack for quite a while: https://github.com/plotly/dash/issues/188#issuecomment-360313359

Hello, is this still a Dash limitation?

It is, but something we’re hoping to address this summer. In the meantime, we’re working on providing support for some of the common use cases on the component level. For example, a common use case for bidirectional syncing is a slider with a text box - we’re planning on just building out a new property for dcc.Slider to include a synchronized text box.

So, it’d be helpful to know what your use case for this feature is. We may be able to provide support for it on the component level before we do it on the framework level.

4 Likes

Thanks for the fast answer. In my case I have a numeric input where the user can insert one integer from 0 to a number that varies between 100 and 1000 (depending which subset of the data the user is working on).
I then have a slider to quickly move around and give immediately an idea of how many samples are present (one for each of the above described integers). Currently I have set the slider as disabled to avoid the problem of bidirectional syncing, but of course it would be cool if the user could also use that and have the numeric input automatically updated.

Since my original post, the possibility to skip component updates has been introduced, which makes it possible to achieve the desired behavior. Here is a small example,

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Output, Input, State
from dash_extensions.callback import DashCallbackBlueprint

# Create app.
min_value, max_value = 100, 1000
app = dash.Dash(external_stylesheets=['https://codepen.io/chriddyp/pen/bWLwgP.css'])
app.layout = html.Div([
    dcc.Input(id="input", type="numeric", min=min_value, max=max_value),
    dcc.Slider(id="slider", min=min_value, max=max_value),
    dcc.Store(id="sync")
])

# Create callbacks.
dcb = DashCallbackBlueprint()

@dcb.callback(Output("sync", "data"), [Input("input", "value")])
def sync_input_value(value):
    return value

@dcb.callback(Output("sync", "data"), [Input("slider", "value")])
def sync_slider_value(value):
    return value

@dcb.callback([Output("input", "value"), Output("slider", "value")], [Input("sync", "data")],
              [State("input", "value"), State("slider", "value")])
def update_components(current_value, input_prev, slider_prev):
    # Update only inputs that are out of sync (this step "breaks" the circular dependency).
    input_value = current_value if current_value != input_prev else dash.no_update
    slider_value = current_value if current_value != slider_prev else dash.no_update
    return [input_value, slider_value]

dcb.register(app)

if __name__ == '__main__':
    app.run_server(debug=False)

This approach takes up some lines of code though, and it’s no way near as efficient as the component level solution that @chriddyp is referring to.

3 Likes

@chriddyp My use case (which is specific to Location and Href) is enabling the user to modify some components on the page, and then copy the URL and share it with someone else. I’m surprised this isn’t a more common use case actually.

Is there another way to do that now? I haven’t kept up with all the latest developments. I see that the discussion in the github thread (https://github.com/plotly/dash/issues/188) is still going on.

3 Likes

Chiming in again to agree with @chubukov.

Being unable to keep the URL synchronised with the complete state of the app (to enable deep linking) is a limitation of Dash that I bump up against every other Dash app I write. Would love to see this enhancement @chriddyp!

1 Like

Hi chriddyp.

I’m only on my first Dash application but my use case is for a range slider with numeric inputs.

It’s summer time, any update on these new components or on synchronizing components?

By the way, Dash is a terrific product. Extremely useful with a gentle learning curve. I’ve really liked using it.

I couldn’t find a workaround for this in the current version I tried using a dcc.Store as an intermediate but it still gets detected a cyclic dependency.
To test I even added raise dash.exceptions.PreventUpdate to the beginning of my two callbacks but without sucess.

What I wanted to achieve sounded so simple a range slider with two input boxes left and right to give exact values if the slider is not accurate enough. I give up for now on this but this feature will be really really useful.

This was my try to adjust @Emil answer from 2014 ;).

@app.callback(
    dash.dependencies.Output({'type': 'filter-min-sync', 'index': dash.dependencies.MATCH}, 'data'),
    dash.dependencies.Output({'type': 'filter-max-sync', 'index': dash.dependencies.MATCH}, 'data'),
    dash.dependencies.Input({'type': 'filter-bound-min', 'index': dash.dependencies.MATCH}, 'value'),
    dash.dependencies.Input({'type': 'filter-bound-max', 'index': dash.dependencies.MATCH}, 'value'),
    dash.dependencies.Input({'type': 'filter-range', 'index': dash.dependencies.MATCH}, 'value'),
)
def sync_filter_bounds(min_bound, max_bound, min_max_value):
    if dash.callback_context.triggered[0]["prop_id"].contains("filter-bound"):
        return min_bound, max_bound
    elif dash.callback_context.triggered[0]["prop_id"].contains("filter-range"):
        return min_max_value

    return dash.no_update, dash.no_update


@app.callback(
    dash.dependencies.Output({'type': 'filter-bound-min', 'index': dash.dependencies.MATCH}, 'value'),
    dash.dependencies.Output({'type': 'filter-bound-max', 'index': dash.dependencies.MATCH}, 'value'),
    dash.dependencies.Output({'type': 'filter-range', 'index': dash.dependencies.MATCH}, 'value'),
    dash.dependencies.Input({'type': 'filter-min-sync', 'index': dash.dependencies.MATCH}, 'data'),
    dash.dependencies.Input({'type': 'filter-max-sync', 'index': dash.dependencies.MATCH}, 'data'),
    dash.dependencies.State({'type': 'filter-bound-min', 'index': dash.dependencies.MATCH}, 'value'),
    dash.dependencies.State({'type': 'filter-bound-max', 'index': dash.dependencies.MATCH}, 'value'),
    dash.dependencies.State({'type': 'filter-range', 'index': dash.dependencies.MATCH}, 'value'),
)
def update_components(current_min, current_max, bound_min_prev, bound_max_prev, range_prev):
    min_value = current_min if current_min != bound_min_prev else dash.no_update
    max_value = current_max if current_max != bound_max_prev else dash.no_update
    current_range = [current_min, current_max]
    range_value = current_range if current_range != range_prev else dash.no_update
    return min_value, max_value, range_value

I made a Monitor component that makes it possible. It is not super elegant, but it works.

import dash_core_components as dcc
from dash import Dash
from dash.dependencies import Input, Output
from dash_extensions import Monitor

app = Dash()
app.layout = Monitor([
    dcc.Input(id="input", autoComplete="off", type="number", min=0, max=100, value=50),
    dcc.Slider(id="slider", min=0, max=100, value=50)],
    probes=dict(probe=[dict(id="input", prop="value"), dict(id="slider", prop="value")]), id="monitor")


@app.callback([Output("input", "value"), Output("slider", "value")], [Input("monitor", "data")])
def sync(data):
    probe = data["probe"]
    return probe["value"], probe["value"]


if __name__ == '__main__':
    app.run_server()

Thanks I will try this, and see if I can make it work with my setup.

Btw. for others searching, Dash now supports circular callbacks, so these workarounds are probably not required any more (for most use-cases).

1 Like

As I remember, the support is limited to dependencies resolved within the same callback. If that fits your usecase, I would definitly recommend the “official” solution. In other cases, the extensions may be an option :slight_smile:

1 Like