Prevent Advancing to Next Step Until Current Step Complete Dash Mantine Stepper

Hello!

I’m trying to create a user walk through type app with Dash. I’m using the Dash Mantine Stepper component to walk the user through a few steps.

How can I prevent the user from advancing to the next step until the current (and all prior steps) are complete? In this example app, the user first needs to upload a csv or excel file. They should not be able to continue until they have uploaded a valid file. The task they will need to complete at each step will of course be different though.

Here’s some example code:

import dash_mantine_components as dmc
from dash import Dash, dcc, html, dash_table, Input, Output, State, callback, ctx

import base64
import datetime
import io

import pandas as pd

min_step = 0
max_step = 3
active = 0

app = Dash(
    __name__,
    external_stylesheets=[
        # include google fonts
        "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;900&display=swap"
    ],
)

app.layout = html.Div(
    [   
        dmc.Stepper(
            id="stepper-basic-usage",
            active=active,
            breakpoint="sm",
            children=[
                dmc.StepperStep(
                    label="First step",
                    description="Upload a file",
                    children=html.Div([
                        dmc.Text("Step 1: Upload file", align="center"),
                        dcc.Upload(
                            id='upload-data',
                            children=html.Div([
                                'Drag and Drop or ',
                                html.A('Select Files')
                            ]),
                            style={
                                
                                'width': '60%',
                                'height': '60px',
                                'lineHeight': '60px',
                                'borderWidth': '1px',
                                'borderStyle': 'dashed',
                                'borderRadius': '5px',
                                'textAlign': 'center',
                                'margin-left': '20%',
                                'margin-right': '20%'
                            },
                            # Allow multiple files to be uploaded
                            multiple=True
                        ),
                        html.Div(id='output-data-upload'),
                        ]),
                ),
                dmc.StepperStep(
                    label="Second step",
                    description="Verify email",
                    children=dmc.Text("Step 2 content: Verify email", align="center"),
                ),
                dmc.StepperStep(
                    label="Final step",
                    description="Get full access",
                    children=dmc.Text(
                        "Step 3 content: Get full access", align="center"
                    ),
                ),
                dmc.StepperCompleted(
                    children=dmc.Text(
                        "Completed, click back button to get to previous step",
                        align="center",
                    )
                ),
            ],
        ),
        dmc.Group(
            position="center",
            mt="xl",
            children=[
                dmc.Button("Back", id="back-basic-usage", variant="default"),
                dmc.Button("Next step", id="next-basic-usage"),
            ],
        ),
    ]
)

def parse_contents(contents, filename, date):
    content_type, content_string = contents.split(',')

    decoded = base64.b64decode(content_string)
    try:
        if 'csv' in filename:
            # Assume that the user uploaded a CSV file
            df = pd.read_csv(
                io.StringIO(decoded.decode('utf-8')))
        elif 'xls' in filename:
            # Assume that the user uploaded an excel file
            df = pd.read_excel(io.BytesIO(decoded))
    except Exception as e:
        print(e)
        return html.Div([
            'There was an error processing this file.'
        ])

    return html.Div([
        html.H5(filename),
        html.H6(datetime.datetime.fromtimestamp(date)),

        dash_table.DataTable(
            df.to_dict('records'),
            [{'name': i, 'id': i} for i in df.columns]
        ),

        html.Hr(),  # horizontal line

        # For debugging, display the raw contents provided by the web browser
        html.Div('Raw Content'),
        html.Pre(contents[0:200] + '...', style={
            'whiteSpace': 'pre-wrap',
            'wordBreak': 'break-all'
        })
    ])

@callback(
    Output("stepper-basic-usage", "active"),
    Input("back-basic-usage", "n_clicks"),
    Input("next-basic-usage", "n_clicks"),
    State("stepper-basic-usage", "active"),
    prevent_initial_call=True,
)
def update(back, next_, current):
    button_id = ctx.triggered_id
    step = current if current is not None else active
    if button_id == "back-basic-usage":
        step = step - 1 if step > min_step else step
    else:
        step = step + 1 if step < max_step else step
    return step

@callback(
    Output('output-data-upload', 'children'),
    Input('upload-data', 'contents'),
    State('upload-data', 'filename'),
    State('upload-data', 'last_modified'),
    prevent_initial_call=True,
)
def update_output(list_of_contents, list_of_names, list_of_dates):
    if list_of_contents is not None:
        children = [
            parse_contents(c, n, d) for c, n, d in
            zip(list_of_contents, list_of_names, list_of_dates)]
        return children

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

Thank you!

Hello @s-etty,

Here is an example of how you would do this:

import dash_mantine_components as dmc
from dash import Dash, dcc, html, dash_table, Input, Output, State, callback, ctx

import base64
import datetime
import io

import pandas as pd

min_step = 0
max_step = 3
active = 0

app = Dash(
    __name__,
    external_stylesheets=[
        # include google fonts
        "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;900&display=swap"
    ],
)

app.layout = html.Div(
    [
        dmc.Stepper(
            id="stepper-basic-usage",
            active=active,
            breakpoint="sm",
            children=[
                dmc.StepperStep(
                    label="First step",
                    description="Upload a file",
                    children=html.Div([
                        dmc.Text("Step 1: Upload file", align="center"),
                        dcc.Upload(
                            id='upload-data',
                            children=html.Div([
                                'Drag and Drop or ',
                                html.A('Select Files')
                            ]),
                            style={

                                'width': '60%',
                                'height': '60px',
                                'lineHeight': '60px',
                                'borderWidth': '1px',
                                'borderStyle': 'dashed',
                                'borderRadius': '5px',
                                'textAlign': 'center',
                                'margin-left': '20%',
                                'margin-right': '20%'
                            },
                            # Allow multiple files to be uploaded
                            multiple=True
                        ),
                        html.Div(id='output-data-upload'),
                    ]),
                ),
                dmc.StepperStep(
                    label="Second step",
                    description="Verify email",
                    children=dmc.Text("Step 2 content: Verify email", align="center"),
                ),
                dmc.StepperStep(
                    label="Final step",
                    description="Get full access",
                    children=dmc.Text(
                        "Step 3 content: Get full access", align="center"
                    ),
                ),
                dmc.StepperCompleted(
                    children=dmc.Text(
                        "Completed, click back button to get to previous step",
                        align="center",
                    )
                ),
            ],
        ),
        dmc.Group(
            position="center",
            mt="xl",
            children=[
                dmc.Button("Back", id="back-basic-usage", variant="default"),
                dmc.Button("Next step", id="next-basic-usage"),
            ],
        ),
    ]
)


def parse_contents(contents, filename, date):
    content_type, content_string = contents.split(',')

    decoded = base64.b64decode(content_string)
    try:
        if 'csv' in filename:
            # Assume that the user uploaded a CSV file
            df = pd.read_csv(
                io.StringIO(decoded.decode('utf-8')))
        elif 'xls' in filename:
            # Assume that the user uploaded an excel file
            df = pd.read_excel(io.BytesIO(decoded))
    except Exception as e:
        print(e)
        return html.Div([
            'There was an error processing this file.'
        ])

    return html.Div([
        html.H5(filename),
        html.H6(datetime.datetime.fromtimestamp(date)),

        dash_table.DataTable(
            df.to_dict('records'),
            [{'name': i, 'id': i} for i in df.columns]
        ),

        html.Hr(),  # horizontal line

        # For debugging, display the raw contents provided by the web browser
        html.Div('Raw Content'),
        html.Pre(contents[0:200] + '...', style={
            'whiteSpace': 'pre-wrap',
            'wordBreak': 'break-all'
        })
    ])


@callback(
    Output("stepper-basic-usage", "active"),
    Input("back-basic-usage", "n_clicks"),
    Input("next-basic-usage", "n_clicks"),
    State('upload-data', 'contents'),
    State("stepper-basic-usage", "active"),
    State('output-data-upload', 'children'),
    prevent_initial_call=True,
)
def update(back, next_, f, current, msg):
    button_id = ctx.triggered_id
    step = current if current is not None else 0
    if button_id == "back-basic-usage":
        step = step - 1 if step > min_step else step
    elif f and msg != [{'props': {'children': ['There was an error processing this file.']}, 'type': 'Div', 'namespace': 'dash_html_components'}]:
        step = step + 1 if step < max_step else step
    return step


@callback(
    Output('output-data-upload', 'children'),
    Input('upload-data', 'contents'),
    State('upload-data', 'filename'),
    State('upload-data', 'last_modified'),
    prevent_initial_call=True,
)
def update_output(list_of_contents, list_of_names, list_of_dates):
    if list_of_contents is not None:
        children = [
            parse_contents(c, n, d) for c, n, d in
            zip(list_of_contents, list_of_names, list_of_dates)]
        return children


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

You could also remove the buttons if the conditions arent met, not 100% to keep it from advancing, but would definitely help.

I’m newer to Dash/Python, but can you clarify if I’m understanding this correctly?

The update function for the stepper is waiting on the state of the upload-data component to be changed. Is that correct?

How would I extend this to account for additional steps? Would I need a new callback for each step?

Thanks!

This is correct.

Potentially, yes.

You could make it so that at each step, you can hide or show the “Next” button dependent upon criteria changing, and make sure you us the allow_duplicate=True