[go: nahoru, domu]

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Dropdown: programatically set value isn't persisted #1252

Open
MM-Lehmann opened this issue May 15, 2020 · 13 comments
Open

[BUG] Dropdown: programatically set value isn't persisted #1252

MM-Lehmann opened this issue May 15, 2020 · 13 comments

Comments

@MM-Lehmann
Copy link
MM-Lehmann commented May 15, 2020

with dash==1.12.0 (haven't tested old versions) and persistence_type='session':
setting a dcc.dropdown's value prop programatically, will update the GUI and callbacks alright, but the resulting persistence storage value (browser dev console) behaves very buggy.
some observations:

  1. When setting the value for the first time (no persisted value yet), it would not appear in the storage list at all.
    • (re)setting it manually in the GUI comes up with a scrambled list containing both the manually set value (T) and the programatically set value (null): ["T",null]
    • the issue is only to be resolved when refreshing the page, deleting the storage entry is not sufficient
  2. When having set the value in the GUI first, and then repeating the process, the value disappears with the same result
  3. the persistence entry will keep the two items list until page refresh, even when clearing the dropdown: [null,null]
  4. the programatically set values are not recalled upon page refresh
  5. this goes for both multi=True and False

Expected behaviour:
There should only be one item in the list at all times: [["T"]] or [[null]]

I can run more tests if required.

@MM-Lehmann MM-Lehmann changed the title [BUG] Dropdown persistence fails when programatically setting value [BUG] Dropdown: programatically set value isn't persisted May 15, 2020
@alexcjohnson
Copy link
Collaborator

scrambled list containing both the manually set value (T) and the programatically set value (null)

You're looking at what gets stored in window.sessionStorage? The back end for persistence does need to store both the programmatic and manual values, so it knows to bring back the manual value only if recreating the element with the same programmatic value.

So I wouldn't worry about the internals at this point, the question is whether there's a bug in the visible behavior. Can you give a concrete example of what you're seeing (app code, user action, result) that doesn't seem right?

@MM-Lehmann
Copy link
Author
app.layout = html.Div([dcc.Dropdown(
    id='dropdown',
    multi=True,
    options=[{...}],
    persistence=True,
    persistence_type='session',
),
    dbc.Button("All", id="button"),
]),

@dash.callback(Output("dropdown", "value"),
               [Input("button", "n_clicks")],
               [State("dropdown", "options")])
def select_params(_all, params):
    return [par['value'] for par in params]

I've tried to extract the basic features of the dash control I'm having issues with.

user action:
I manually select a few parameters in the dropdown, then I click the "all" button which fills the dropdown with all available options.
result:
upon page refresh, the values are not persisted and the dropdown stays empty. The same happens even after manually modifying the selection again after hitting "All".

Only a page refresh resolves the issue (until I hit "All" again).

@MM-Lehmann
Copy link
Author

One further issue which is even more serious: (I don't know if that only started occurring with dash=1.13 or before)
When a value (e.g. a checklist) is set programatically, I can't seem to reset it via the UI component. The callback always reads True for a persisted value that looks like this in the dev-panel (chrome): [[],[1]]

@MM-Lehmann
Copy link
Author

Another Observation (may or may not be related):
When a previously selected value in a dropdown becomes invalid (not contained in the options anymore after update), it would disappear from the UI element but is still included in the callback "value". This is weird and causes many issues in my use case.
Proposal: "value" should always reflect what the user sees in the UI and not some orphan, invalid state.

@anders-kiaer
Copy link
Contributor

Another Observation (may or may not be related):
When a previously selected value in a dropdown becomes invalid (not contained in the options anymore after update), it would disappear from the UI element but is still included in the callback "value". This is weird and causes many issues in my use case.
Proposal: "value" should always reflect what the user sees in the UI and not some orphan, invalid state.

We see the observation in this comment as well, see #1373. This particular observation is not related to the dcc.Dropdown component alone, but for persistence in general (one question raised in #1373 is if components already can implement a persistence check on the JavaScript/client side, or if there are some changes needed in the core persistence machinery first).

@JiZur
Copy link
JiZur commented Nov 30, 2020

More generally, Dash components lose their persistency if they are modified by callbacks instead of direct user clicking (see: https://community.plotly.com/t/losing-persistence-value-on-refresh-when-input-value-updated-using-callback/39813 ). It would be very beneficial to keep the persistency even with callbacks

@ManInFez
Copy link

I am experiencing the same issue. Neither value nor options are persisted when the content are created dynamically.

@alexveden
Copy link

I'm experiencing the same issue, but with other types of components. Inputs and data-tables are also affected by this bug in the case when their contents filled via callbacks. Persistence saving only triggered after direct user input action.

@alexcjohnson
Copy link
Collaborator

Persistence saving only triggered after direct user input action.

That's as intended: if a value is set via callback, then the thing to be persisted should be the user input that led to that value, and that callback-set value will flow from the persisted user input. But I'm curious what use case you had in mind for callback outputs to be persisted?

@alexveden
Copy link
alexveden commented Sep 8, 2021

@alexcjohnson

But I'm curious what use case you had in mind for callback outputs to be persisted?

This thing is mostly annoying in multi-tab environments, so all my below examples are about erasing tab components state/values after user switched between tabs.

Case 1: data-table is not persistent when filled by callback (e.g. after Load button press)

When we fill data-table by callback (say on button or interval timer) the table data get erased on tab switch.

In the example below, are two identical tables one has static data, and another one can be filled by callback:

def create_table(tid, n=3):
    return dash_table.DataTable(
            id=tid,
            columns=[
                {'name': 'Filter', 'id': 'name'},
            ],
            data=[{'name': f'test{i}'} for i in range(n)],
            filter_action='native',
            page_action='none',
            row_deletable=True,
            style_table={
                'margin': '10px',
                'width': '100%',
            },
            persistence=True,
            persisted_props=['data'],
            persistence_type='session',
    )

tab_layout = [
    dbc.Row(create_table('tab1-table', 2)),
        dbc.Row(create_table('tab1-table2',5)),
        dbc.Button('Fill data', id='fill-table')
]

@app.callback(
        Output('tab1-table2', 'data'),
        Input('fill-table', 'n_clicks'),
)
def set_text(n_clicks):
    if n_clicks is None:
        raise PreventUpdate()
    return [{'name': f'btn_filler{i}'} for i in range(5)]

dash_table

Case 2: Input is not persistent when filled by callback

Particularly this affects hidden inputs that used to store some temporary data, but text inputs work the same. For example, if you need to store some temporary state of the tab components, which is built using multiple component inputs or results of callbacks.

Maybe I'm using a wrong tool for this task and should store state in dcc.Store somehow. But it seems more natural to me reusing persisted components values, it sounds more elegant to me.

layout = [
    html.Div([
        # Query panel
        dbc.Row([
                dbc.Input(id='tab2-input', type='text', persistence=True),
                dbc.Button('Fill by callback', id='tab2-set-text'),
            ]
        ),
    ])
]

@app.callback(
        Output('tab2-input', 'value'),
        Input('tab2-set-text', 'n_clicks'),
)
def set_text(n_clicks):
    if n_clicks is None:
        raise PreventUpdate()
    return 'Filled by callback'

dash_input

Simple Calculator App

The result doesn't persist after tab switch. This becomes a bit more annoying when calculation takes minutes or so.

layout = [
    html.Div([
        # Query panel
        dbc.Row([
                html.Label('Calculate expression'),
                dcc.Input(id='tab2-expr', persistence=True),
                ]),
        dbc.Row([
                html.Label('Result'),
                dbc.Input(id='tab2-input', type='text', persistence=True),
            ]),
        dbc.Row([

                dbc.Button('Calculate', id='tab2-set-text'),
            ]
        ),
    ])
]

@app.callback(
        Output('tab2-input', 'value'),
        Input('tab2-set-text', 'n_clicks'),
        State('tab2-expr', 'value'),
)
def set_text(n_clicks, expression):
    if n_clicks is None or not expression:
        raise PreventUpdate()
    
    return str(eval(expression))

dash_calculator

@rodelrod
Copy link
rodelrod commented Mar 8, 2022

I'm bumping into the same problem

That's as intended: if a value is set via callback, then the thing to be persisted should be the user input that led to that value, and that callback-set value will flow from the persisted user input. But I'm curious what use case you had in mind for callback outputs to be persisted?

In my case the thing to be persisted is a data file upload. A callback loads the file to a textarea field, where it can be modified by hand. The contents of the textarea must be persisted in the front. The contents of the textarea are then used by another callback to populate a chart.

For data security reasons we can't store that data in the backend, so the only way to share data is to share the data files securely.

The current behavior:

  • If we enter the data manually, it's persisted in the front
  • If we upload a data file, the data is not persisted, even if we edit it manually afterwards.

The workaround would be to update the chart in the same callback as the data file is uploaded to the textarea. Since I have multiple textareas feeding into the same chart, each with its own upload button, the resulting callback code is truly atrocious and borderline incomprehensible.

@pwan2991
Copy link
pwan2991 commented Aug 27, 2022

One unfortunately rather hacky way to get persistence in these cases is to use a combination of dcc.Store (cache the value to be persisted), enrich's MultiplexerTransform (allow two callbacks on same output), and a hidden button triggered upon loading of component (fetch stored value when refreshing page).

Here's an example:

from dash_extensions.enrich import (
    DashProxy, MultiplexerTransform, TriggerTransform,
    html, dcc, Input, Output, State, Trigger
)

app = DashProxy(__name__, transforms=[MultiplexerTransform(),
                                      TriggerTransform()])

app.layout = html.Div([
    dcc.Input(id="input", type="text", persistence=True),
    html.Button(id="change-text-button", children="Change Input Text"),
    html.Button(id="hidden-button", style={"display": "none"}),
    dcc.Store(id="store", storage_type="session")
])


@app.callback(
    Output("store", "data"),
    Input("input", "value")
)
def update_store_on_input(value):
    """Store input value in dcc.Store for later recovery"""
    return value


@app.callback(
    Output("input", "value"),
    Trigger("change-text-button", "n_clicks"),
    prevent_initial_call=True
)
def change_input_text():
    """Change input text on button click"""
    return "Ex Machina"


@app.callback(
    Output("input", "value"),
    Trigger("hidden-button", "n_clicks"),
    State("store", "data")
)
def fetch_stored_input_on_reload(value):
    """Reload input value from dcc.Store upon reload of page: hidden button
    triggers callback only upon loading of page"""
    return value


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

@aGitForEveryone
Copy link
Contributor

@pwan2991, You can also achieve that with a circular callback and the callback context. In that case you don't need to have the MultiplexerTransform in your application, and you don't need to trigger or work with hidden component to fill the component upon loading. This is the approach I use in my applications, and works without relying on extensions. A consequence of this, of course, is that you have to combine everything in one callback.

Though I find it very cumbersome to have to do this for all components in my application. So I am really hoping that this functionality gets integrated in dash at some point.

from dash import Dash, Output, Input, State, ctx, dcc, html, callback

app = Dash(__name__)

app.layout = html.Div([
    dcc.Input(id="input", type="text", persistence=True),
    html.Button(id="change-text-button", children="Change Input Text"),
    dcc.Store(id="store", storage_type="session")
])


@callback(
    Output("store", "data"),
    Output("input", "value"),
    Input("store", "modified_timestamp"),
    Input("input", "value"),
    Input("change-text-button", "n_clicks"),
    State("store", "data")
)
def update_store_on_input(_, text_input, manual_text_reset, stored_text):
    """ Handle all logic related to the input field"""

    if ctx.triggered_id == 'input':
        # The user filled in a value in the input field
        new_text_to_store = text_input
    elif ctx.triggered_id == 'change-text-button' and manual_text_reset:
        # The user pressed the button
        new_text_to_store = 'Ex Machina'
    else:
        # Something else happened like page reload, so we just fetch the data
        # that is stored
        new_text_to_store = stored_text

    # Store the new data in the dcc.Store and fill the input field with the
    # new data
    return new_text_to_store, new_text_to_store


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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants