snehilvj / dash-mantine-components

Plotly Dash components based on Mantine React Components
https://www.dash-mantine-components.com
MIT License
561 stars 56 forks source link

Not able to preserve Select value when the data is come from a dropdown #313

Closed CooperZhao1998 closed 1 month ago

CooperZhao1998 commented 1 month ago

I am creating an app that can add/remove records while preserving the selected value. And the data of the records are depends on the value selected from the topic dropdown. The issue is after I select the topic value, I am not able to select the record value, it will always set to none no matter which value I select.

This issue only happed in 0.14.4. And the code works fine in 0.12 and 0.14.2.

import dash_mantine_components as dmc
from dash import dcc, _dash_renderer
import dash
from dash import Output, Input, State, html, ctx, ALL, callback, dcc, callback_context, clientside_callback
from dash.exceptions import PreventUpdate
from dash_iconify import DashIconify

_dash_renderer._set_react_version("18.2.0")

app = dash.Dash()

def create_record(instance):
    content = html.Div([
                    html.Div(f"{instance['index']}.", 
                        style = {'flex': '0 0 3%', 'display': 'flex', 
                                 'textAlign': 'right',
                                 'alignItems': 'center', 
                                 'marginBottom': '0.2rem'
                                }),
                    dmc.Select(id={'type': "record_select",
                                            'index': instance['id']},
                        searchable=True, 
                        placeholder='Choose the record',
                        value=instance['record_select'],
                        maxDropdownHeight = '10rem',
                        size='xs',
                        style = {'flex': '0 0 15%', 'marginRight': '1rem',}
                    ),
                    dmc.Button(id={'type': "remove_button",'index': instance['id']}, 
                               leftSection=DashIconify(icon="streamline:delete-1-solid"), variant="subtle", 
                               style = {'flex': '0 0 2%', 'color':"#00485E", 'marginLeft': '1rem'}),
                ], 
                style={'display':'flex', 'alignItems': 'center', 'marginBottom': '0.2rem'},
        )
    return content

main_content = html.Div([dmc.Group([
                                dmc.Text('Topic:'),
                                dmc.Select(id="topic_select",
                                        data=[{'label': f"topic_{str(i)}", 'value': f"topic_{str(i)}"} for i in range(5)],
                                        searchable=True, 
                                        value='',
                                        ),
                            dmc.Button("Add Data", id = 'add_button', size = 'sm', 
                                        leftSection = DashIconify(icon='material-symbols:add-box-outline'),
                                    style={"backgroundColor": "#2d9bad"}),
                        ], style = {'marginBottom': '2rem'}),
                        dcc.Store(id="records", data={'records': [], 'next_id': 1}),
                                                dmc.Card(withBorder=True, shadow="md", radius="md", 
                                                    children = ([html.Div(id="record_container",
                                                                        style={'display': 'flex', 'flexDirection': 'column'},
                                                                        className='sidebar',)]),
                                                    style={'backgroundColor':'#F5F6F7', 'height': '20rem', 
                                                            'resize': 'vertical'},),
                                                ],style = {"padding":'2rem'})

app.layout = dmc.MantineProvider(
        theme={
            'fontFamily': '"Inter", sans-serif',
        },
        children=[
            dmc.Container([
                dmc.AppShell(children = [
                    dmc.AppShellMain(children=main_content),
                    ],
            ),], 
            id="page-container",
            p=0,
            fluid=True,
            #size = '100%',
            style={'backgroundColor':'#f4f6f9', 'height': '96vh',}
        )
        ],)

@callback(
    Output('record_container', 'children'),
    Output('records', 'data'),
    Input('add_button', 'n_clicks'),
    Input({'type': 'remove_button', 'index': ALL}, 'n_clicks'),

    ### this record_select need to be stay in the callback for other purpose
    Input({'type': "record_select", 'index': ALL}, 'value'),
    State('records', 'data')
    )
def add_instance(add_clicks, remove_clicks, 
               selected_record, data):
    triggered_id = ctx.triggered_id
    triggered_value = ctx.triggered[0]['value']
    records = data['records']
    next_id = data['next_id']
    if not triggered_id:
        return dash.no_update, data
    elif triggered_id == 'add_button':
        records.append({'id': next_id, 
                          'index': len(records)+1, 
                          'record_select': None
                        })
        next_id += 1
    elif triggered_id['type'] == 'remove_button':
        index_to_remove = triggered_id['index']
        records = [instance for instance in records if instance['id'] != index_to_remove]
        for i, instance in enumerate(records):
            instance['index'] = i+1
    else:
        instance_id = triggered_id['index']
        for instance in records:
            if instance['id'] == instance_id:
                instance[triggered_id['type']] = triggered_value
    return [create_record(instance) for instance in records], {'records': records, 'next_id': next_id}

@callback(
    Output({'type': "record_select", 'index': ALL}, 'data'),
    Input('topic_select', 'value'),
)
def update_dir_link(value):
    if not value and not ctx.triggered_id:
        raise PreventUpdate
    num_instance = len(callback_context.outputs_list)
    record_list = [{'label': f"{value}_record_{str(i)}", 'value': f"{value}_record_{str(i)}"} for i in range(5)]

    return [record_list for i in range(num_instance)]

if __name__ == "__main__":
    app.run(debug=True)
AnnMarieW commented 1 month ago

Hi @CooperZhao1998

Thanks for reporting. It looks like this is caused by the update in 14.4 that made it so that when you have chained Select components, when you change the first Select, then the second Select should only contain values based on the updated data(options).

In your scenario, it appears that you are updating the value of the second Select before the data(options) are updated, so the value is removed.

You could probably solve this by updating both the values and the data when you add the second Select component.

CooperZhao1998 commented 1 month ago

Hi @AnnMarieW

Thanks for your prompt answer! So you mean I should combine the two callback together, so the data(options) should will be update when there is record_select been added? Also I wondering why this will work both in 0.12 and 0.14.2, but not in 0.14.4?

AnnMarieW commented 1 month ago

I didn't dive into the code to see if there was a simpler way to do this - maybe with a different type of pattern matching callback? But yes, combining the callbacks is the first thing I'd try.

It doesn't work in 14.4 because there was a change made to verify that the value was valid based on the options data. This step was missing in previous versions.

CooperZhao1998 commented 1 month ago

Got it! Thanks!