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

Routing callback inputs #2647

Merged
merged 10 commits into from
Oct 6, 2023
Merged

Conversation

RenaudLN
Copy link
Contributor

@RenaudLN RenaudLN commented Sep 22, 2023

Allows to add new Inputs/States to the pages routing callback, to pass additional data to the layout functions.

This allows things like (non-exhaustive list):

  • A language dropdown that will be passed to every layout function, for internationalisation
  • Serialising the state in URL hashes without reloading the page on every input update, and using the hash on first load / refresh
  • Passing a global app data store on page load

The syntax for this parameter is a dict of State objects: routing_callback_inputs={"language": Input("language", "value")}

Example 1: Internationalisation

app.py

from dash import Dash, Input, html, page_container

app = Dash(
   __name__,
   use_pages=True,
   routing_callback_inputs={
       # The language will be passed as a `layout` keyword argument to page layout functions
       "language": Input("language", "value"),
   },
)
app.layout = html.Div(
   [
       html.Div(
           [
               "My app",
               html.Div(
                   [
                       dcc.Link("Home", href="/"),
                       dcc.Link("Page 1", href="/page-1"),
                       dcc.Dropdown(
                           id="language",
                           options=[
                               {"label": "English", "value": "en"},
                               {"label": "Français", "value": "fr"},
                           ],
                       ),
                   ],
                   style={"marginLeft": "auto", "display": "flex", "gap": 10}
               )
           ],
           style={"background": "#CCC", "padding": 10, "marginBottom": 20, "display": "flex"},
       ),
       page_container,
   ]
)

pages/home.py

from dash import register_page, html

register_page(__name__, "/")

translations = {
    "en": {
        "title": "Hello world",
        "subtitle": "This is the home page.",
    },
    "fr": {
        "title": "Bonjour le monde",
        "subtitle": "Ceci est la page d'accueil.",
    }
}

def layout(language: str = "en"):
    return [
        html.H1(translations[language]["title"]),
        html.H2(translations[language]["subtitle"]),
    ]

Example 2: State serialisation in URL

app.py

from dash import Dash, State, html, page_container
from dash.dash import _ID_LOCATION

app = Dash(
    __name__,
    use_pages=True,
    routing_callback_inputs={
        # The app state is serialised in the URL hash without refreshing the page
        # This URL can be copied and then parsed on first load
        "state": State(_ID_LOCATION, "hash"),
    },
)
app.layout = html.Div(
    [
        html.Div(
            [
                "My app",
                html.Div(
                    [
                        dcc.Link("Home", href="/"),
                        dcc.Link("Page 1", href="/page-1"),
                    ],
                    style={"marginLeft": "auto", "display": "flex", "gap": 10}
                )
            ],
            style={"background": "#CCC", "padding": 10, "marginBottom": 20, "display": "flex"},
        ),
        page_container,
    ]
)

pages/home.py

import base64
import json
from dash import ALL, Input, Output, callback, html, dcc, register_page, ctx
from dash.dash import _ID_LOCATION

register_page(__name__, "/")

def layout(state: str):
    # Define default values
    defaults = {"country": "France", "year": 2020}
    # Decode the state from the hash
    state = defaults | (json.loads(base64.b64decode(state)) if state else {})

    return [
        html.Div(
            [
                dcc.Dropdown(
                    id={"type": "control", "id": "country"},
                    value=state.get("country"),
                    options=["France", "USA", "Canada"],
                ),
                dcc.Dropdown(
                    id={"type": "control", "id": "year"},
                    value=state.get("year"),
                    options=[{"label": str(y), "value": y} for y in range(1980, 2021)],
                ),
            ],
        ),
        html.Div(contents(**state), id="contents")
    ]

def contents(country: str, year: int):
    return f"Country: {country}, Year: {year}"


# Update the hash in the URL Location component
@callback(
    Output(_ID_LOCATION, "hash", allow_duplicate=True),
    Input({"type": "control", "id": ALL}, "value"),
    prevent_initial_call=True,
)
def update_hash(_values):
    return "#" + base64.b64encode(
        json.dumps(
            {inp["id"]["id"]: inp["value"] for inp in ctx.inputs_list[0]}
        )
        .encode("utf-8")
    ).decode("utf-8")


# Update the contents when the dropdowns are updated
@callback(
    Output("contents", "children"),
    Input({"type": "control", "id": "country"}, "value"),
    Input({"type": "control", "id": "year"}, "value"),
    prevent_initial_call=True,
)
def update_contents(country, year):
    return contents(country, year)

Contributor Checklist

  • I have run the tests locally and they passed. (refer to testing section in contributing)
  • I have added tests, or extended existing tests, to cover any new features or bugs fixed in this PR

optionals

  • I have added entry in the CHANGELOG.md
  • If this PR needs a follow-up in dash docs, community thread, I have mentioned the relevant URLS as follows
    • this GitHub #PR number updates the dash docs
    • here is the show and tell thread in Plotly Dash community

@RenaudLN RenaudLN changed the title Routing callback states Routing callback inputs Sep 28, 2023
Copy link
Contributor

@T4rk1n T4rk1n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just need to add a test.

dash/dash.py Outdated Show resolved Hide resolved
add importlib-metadata in requirements
add integration test
@RenaudLN
Copy link
Contributor Author

RenaudLN commented Oct 2, 2023

Test added 👍

Copy link
Contributor

@T4rk1n T4rk1n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💃 Looks good

requires-all.txt Outdated Show resolved Hide resolved
tests/integration/multi_page/test_pages_layout.py Outdated Show resolved Hide resolved
@T4rk1n T4rk1n merged commit c729ef8 into plotly:dev Oct 6, 2023
1 check passed
@RenaudLN
Copy link
Contributor Author

RenaudLN commented Nov 2, 2023

Docs update PR link: https://github.com/plotly/ddk-dash-docs/pull/2055

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

Successfully merging this pull request may close these issues.

4 participants