Skip to content

Commit

Permalink
Merge pull request #2647 from RenaudLN/feature/routing-callback-states
Browse files Browse the repository at this point in the history
Routing callback inputs
  • Loading branch information
T4rk1n authored Oct 6, 2023
2 parents f73f758 + 1765276 commit c729ef8
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 30 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).

## [UNRELEASED]

## Fixed
## Fixed

- [#2634](https://github.com/plotly/dash/pull/2634) Fix deprecation warning on pkg_resources, fix [#2631](https://github.com/plotly/dash/issues/2631)

Expand All @@ -25,6 +25,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## Added

- [#2630](https://github.com/plotly/dash/pull/2630) New layout hooks in the renderer
- [#2647](https://github.com/plotly/dash/pull/2647) `routing_callback_inputs` allowing to pass more Input and/or State arguments to the pages routing callback


## [2.12.1] - 2023-08-16
Expand Down
34 changes: 25 additions & 9 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import base64
import traceback
from urllib.parse import urlparse
from typing import Union
from typing import Dict, Optional, Union

import flask

Expand All @@ -29,8 +29,9 @@
from .fingerprint import build_fingerprint, check_fingerprint
from .resources import Scripts, Css
from .dependencies import (
Output,
Input,
Output,
State,
)
from .development.base_component import ComponentRegistry
from .exceptions import (
Expand Down Expand Up @@ -347,6 +348,12 @@ class Dash:
:param hooks: Extend Dash renderer functionality by passing a dictionary of
javascript functions. To hook into the layout, use dict keys "layout_pre" and
"layout_post". To hook into the callbacks, use keys "request_pre" and "request_post"
:param routing_callback_inputs: When using Dash pages (use_pages=True), allows to
add new States to the routing callback, to pass additional data to the layout
functions. The syntax for this parameter is a dict of State objects:
`routing_callback_inputs={"language": Input("language", "value")}`
NOTE: the keys "pathname_" and "search_" are reserved for internal use.
"""

_plotlyjs_url: str
Expand Down Expand Up @@ -384,6 +391,7 @@ def __init__( # pylint: disable=too-many-statements
background_callback_manager=None,
add_log_handler=True,
hooks: Union[RendererHooks, None] = None,
routing_callback_inputs: Optional[Dict[str, Union[Input, State]]] = None,
**obsolete,
):
_validate.check_obsolete(obsolete)
Expand Down Expand Up @@ -461,6 +469,7 @@ def __init__( # pylint: disable=too-many-statements

self.pages_folder = str(pages_folder)
self.use_pages = (pages_folder != "pages") if use_pages is None else use_pages
self.routing_callback_inputs = routing_callback_inputs or {}

# keep title as a class property for backwards compatibility
self.title = title
Expand Down Expand Up @@ -2078,21 +2087,28 @@ def router():
return
self._got_first_request["pages"] = True

inputs = {
"pathname_": Input(_ID_LOCATION, "pathname"),
"search_": Input(_ID_LOCATION, "search"),
}
inputs.update(self.routing_callback_inputs)

@self.callback(
Output(_ID_CONTENT, "children"),
Output(_ID_STORE, "data"),
Input(_ID_LOCATION, "pathname"),
Input(_ID_LOCATION, "search"),
inputs=inputs,
prevent_initial_call=True,
)
def update(pathname, search):
def update(pathname_, search_, **states):
"""
Updates dash.page_container layout on page navigation.
Updates the stored page title which will trigger the clientside callback to update the app title
"""

query_parameters = _parse_query_string(search)
page, path_variables = _path_to_page(self.strip_relative_path(pathname))
query_parameters = _parse_query_string(search_)
page, path_variables = _path_to_page(
self.strip_relative_path(pathname_)
)

# get layout
if page == {}:
Expand All @@ -2110,9 +2126,9 @@ def update(pathname, search):

if callable(layout):
layout = (
layout(**path_variables, **query_parameters)
layout(**path_variables, **query_parameters, **states)
if path_variables
else layout(**query_parameters)
else layout(**query_parameters, **states)
)
if callable(title):
title = title(**path_variables) if path_variables else title()
Expand Down
1 change: 1 addition & 0 deletions requires-install.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ dash_html_components==2.0.0
dash_core_components==2.0.0
dash_table==5.0.0
importlib-metadata==4.8.3;python_version<"3.7"
importlib-metadata;python_version>="3.7"
contextvars==2.4;python_version<"3.7"
typing_extensions>=4.1.1
requests
Expand Down
96 changes: 76 additions & 20 deletions tests/integration/multi_page/test_pages_layout.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
import dash
from dash import Dash, dcc, html
from dash import Dash, Input, State, dcc, html
from dash.dash import _ID_LOCATION
from dash.exceptions import NoLayoutException


Expand Down Expand Up @@ -59,38 +60,33 @@ def test_pala001_layout(dash_duo, clear_pages_state):
assert dash_duo.driver.title == page["title"], "check that page title updates"

# test redirects
dash_duo.wait_for_page(url=f"http://localhost:{dash_duo.server.port}/v2")
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/v2")
dash_duo.wait_for_text_to_equal("#text_redirect", "text for redirect")
dash_duo.wait_for_page(url=f"http://localhost:{dash_duo.server.port}/old-home-page")
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/old-home-page")
dash_duo.wait_for_text_to_equal("#text_redirect", "text for redirect")
assert (
dash_duo.driver.current_url
== f"http://localhost:{dash_duo.server.port}/redirect"
)
assert dash_duo.driver.current_url == f"{dash_duo.server_url}/redirect"

# test redirect with button and user defined dcc.Location
# note: dcc.Location must be defined in app.py
dash_duo.wait_for_page(url=f"http://localhost:{dash_duo.server.port}/page1")
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/page1")
dash_duo.find_element("#btn1").click()
dash_duo.wait_for_text_to_equal("#text_page2", "text for page2")

# test query strings
dash_duo.wait_for_page(
url=f"http://localhost:{dash_duo.server.port}/query-string?velocity=10"
)
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/query-string?velocity=10")
assert (
dash_duo.find_element("#velocity").get_attribute("value") == "10"
), "query string passed to layout"

# test path variables
dash_duo.wait_for_page(url=f"http://localhost:{dash_duo.server.port}/a/none/b/none")
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/a/none/b/none")
dash_duo.wait_for_text_to_equal("#path_vars", "variables from pathname:none none")

dash_duo.wait_for_page(url=f"http://localhost:{dash_duo.server.port}/a/var1/b/var2")
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/a/var1/b/var2")
dash_duo.wait_for_text_to_equal("#path_vars", "variables from pathname:var1 var2")

# test page not found
dash_duo.wait_for_page(url=f"http://localhost:{dash_duo.server.port}/find_me")
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/find_me")
dash_duo.wait_for_text_to_equal("#text_not_found_404", "text for not_found_404")

# test `validation_layout` exists when suppress_callback_exceptions=False`
Expand Down Expand Up @@ -121,20 +117,20 @@ def test_pala002_meta_tags_default(dash_duo, clear_pages_state):
{"property": "twitter:card", "content": "summary_large_image"},
{
"property": "twitter:url",
"content": f"http://localhost:{dash_duo.server.port}/",
"content": f"{dash_duo.server_url}/",
},
{"property": "twitter:title", "content": "Multi layout2"},
{"property": "twitter:description", "content": ""},
{
"property": "twitter:image",
"content": f"http://localhost:{dash_duo.server.port}/assets/app.jpeg",
"content": f"{dash_duo.server_url}/assets/app.jpeg",
},
{"property": "og:title", "content": "Multi layout2"},
{"property": "og:type", "content": "website"},
{"property": "og:description", "content": ""},
{
"property": "og:image",
"content": f"http://localhost:{dash_duo.server.port}/assets/app.jpeg",
"content": f"{dash_duo.server_url}/assets/app.jpeg",
},
]

Expand All @@ -149,7 +145,7 @@ def test_pala003_meta_tags_custom(dash_duo, clear_pages_state):
{"property": "twitter:card", "content": "summary_large_image"},
{
"property": "twitter:url",
"content": f"http://localhost:{dash_duo.server.port}/",
"content": f"{dash_duo.server_url}/",
},
{"property": "twitter:title", "content": "Supplied Title"},
{
Expand All @@ -158,14 +154,14 @@ def test_pala003_meta_tags_custom(dash_duo, clear_pages_state):
},
{
"property": "twitter:image",
"content": f"http://localhost:{dash_duo.server.port}/assets/birds.jpeg",
"content": f"{dash_duo.server_url}/assets/birds.jpeg",
},
{"property": "og:title", "content": "Supplied Title"},
{"property": "og:type", "content": "website"},
{"property": "og:description", "content": "This is the supplied description"},
{
"property": "og:image",
"content": f"http://localhost:{dash_duo.server.port}/assets/birds.jpeg",
"content": f"{dash_duo.server_url}/assets/birds.jpeg",
},
]

Expand All @@ -179,3 +175,63 @@ def test_pala004_no_layout_exception(clear_pages_state):
Dash(__name__, use_pages=True, pages_folder="pages_error")

assert error_msg in err.value.args[0]


def get_routing_inputs_app():
app = Dash(
__name__,
use_pages=True,
routing_callback_inputs={
"hash": State(_ID_LOCATION, "hash"),
"language": Input("language", "value"),
},
)
# Page with layout from a variable: should render and not be impacted
# by routing callback inputs
dash.register_page(
"home",
layout=html.Div("Home", id="contents"),
path="/",
)

# Page with a layout function, should see the routing callback inputs
# as keyword arguments
def layout1(hash: str = None, language: str = "en", **kwargs):
translations = {
"en": "Hash says: {}",
"fr": "Le hash dit: {}",
}
return html.Div(translations[language].format(hash), id="contents")

dash.register_page(
"function_layout",
path="/function-layout",
layout=layout1,
)
app.layout = html.Div(
[
dcc.Dropdown(id="language", options=["en", "fr"], value="en"),
dash.page_container,
]
)
return app


def test_pala005_routing_inputs(dash_duo, clear_pages_state):
dash_duo.start_server(get_routing_inputs_app())
dash_duo.wait_for_page(url=f"{dash_duo.server_url}#123")
dash_duo.wait_for_text_to_equal("#contents", "Home")
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/")
dash_duo.wait_for_text_to_equal("#contents", "Home")
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/function-layout")
dash_duo.wait_for_text_to_equal("#contents", "Hash says:")
# hash is a State therefore navigating to the same page with hash will not
# re-render the layout function
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/function-layout#123")
dash_duo.wait_for_text_to_equal("#contents", "Hash says:")
# Refreshing the page re-runs the layout function
dash_duo.driver.refresh()
dash_duo.wait_for_text_to_equal("#contents", "Hash says: #123")
# Changing the language Input re-runs the layout function
dash_duo.select_dcc_dropdown("#language", "fr")
dash_duo.wait_for_text_to_equal("#contents", "Le hash dit: #123")

0 comments on commit c729ef8

Please sign in to comment.