Skip to content

Commit c729ef8

Browse files
authored
Merge pull request #2647 from RenaudLN/feature/routing-callback-states
Routing callback inputs
2 parents f73f758 + 1765276 commit c729ef8

File tree

4 files changed

+104
-30
lines changed

4 files changed

+104
-30
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
44

55
## [UNRELEASED]
66

7-
## Fixed
7+
## Fixed
88

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

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

2727
- [#2630](https://github.com/plotly/dash/pull/2630) New layout hooks in the renderer
28+
- [#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
2829

2930

3031
## [2.12.1] - 2023-08-16

dash/dash.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import base64
1717
import traceback
1818
from urllib.parse import urlparse
19-
from typing import Union
19+
from typing import Dict, Optional, Union
2020

2121
import flask
2222

@@ -29,8 +29,9 @@
2929
from .fingerprint import build_fingerprint, check_fingerprint
3030
from .resources import Scripts, Css
3131
from .dependencies import (
32-
Output,
3332
Input,
33+
Output,
34+
State,
3435
)
3536
from .development.base_component import ComponentRegistry
3637
from .exceptions import (
@@ -347,6 +348,12 @@ class Dash:
347348
:param hooks: Extend Dash renderer functionality by passing a dictionary of
348349
javascript functions. To hook into the layout, use dict keys "layout_pre" and
349350
"layout_post". To hook into the callbacks, use keys "request_pre" and "request_post"
351+
352+
:param routing_callback_inputs: When using Dash pages (use_pages=True), allows to
353+
add new States to the routing callback, to pass additional data to the layout
354+
functions. The syntax for this parameter is a dict of State objects:
355+
`routing_callback_inputs={"language": Input("language", "value")}`
356+
NOTE: the keys "pathname_" and "search_" are reserved for internal use.
350357
"""
351358

352359
_plotlyjs_url: str
@@ -384,6 +391,7 @@ def __init__( # pylint: disable=too-many-statements
384391
background_callback_manager=None,
385392
add_log_handler=True,
386393
hooks: Union[RendererHooks, None] = None,
394+
routing_callback_inputs: Optional[Dict[str, Union[Input, State]]] = None,
387395
**obsolete,
388396
):
389397
_validate.check_obsolete(obsolete)
@@ -461,6 +469,7 @@ def __init__( # pylint: disable=too-many-statements
461469

462470
self.pages_folder = str(pages_folder)
463471
self.use_pages = (pages_folder != "pages") if use_pages is None else use_pages
472+
self.routing_callback_inputs = routing_callback_inputs or {}
464473

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

2090+
inputs = {
2091+
"pathname_": Input(_ID_LOCATION, "pathname"),
2092+
"search_": Input(_ID_LOCATION, "search"),
2093+
}
2094+
inputs.update(self.routing_callback_inputs)
2095+
20812096
@self.callback(
20822097
Output(_ID_CONTENT, "children"),
20832098
Output(_ID_STORE, "data"),
2084-
Input(_ID_LOCATION, "pathname"),
2085-
Input(_ID_LOCATION, "search"),
2099+
inputs=inputs,
20862100
prevent_initial_call=True,
20872101
)
2088-
def update(pathname, search):
2102+
def update(pathname_, search_, **states):
20892103
"""
20902104
Updates dash.page_container layout on page navigation.
20912105
Updates the stored page title which will trigger the clientside callback to update the app title
20922106
"""
20932107

2094-
query_parameters = _parse_query_string(search)
2095-
page, path_variables = _path_to_page(self.strip_relative_path(pathname))
2108+
query_parameters = _parse_query_string(search_)
2109+
page, path_variables = _path_to_page(
2110+
self.strip_relative_path(pathname_)
2111+
)
20962112

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

21112127
if callable(layout):
21122128
layout = (
2113-
layout(**path_variables, **query_parameters)
2129+
layout(**path_variables, **query_parameters, **states)
21142130
if path_variables
2115-
else layout(**query_parameters)
2131+
else layout(**query_parameters, **states)
21162132
)
21172133
if callable(title):
21182134
title = title(**path_variables) if path_variables else title()

requires-install.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ dash_html_components==2.0.0
55
dash_core_components==2.0.0
66
dash_table==5.0.0
77
importlib-metadata==4.8.3;python_version<"3.7"
8+
importlib-metadata;python_version>="3.7"
89
contextvars==2.4;python_version<"3.7"
910
typing_extensions>=4.1.1
1011
requests

tests/integration/multi_page/test_pages_layout.py

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
import dash
3-
from dash import Dash, dcc, html
3+
from dash import Dash, Input, State, dcc, html
4+
from dash.dash import _ID_LOCATION
45
from dash.exceptions import NoLayoutException
56

67

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

6162
# test redirects
62-
dash_duo.wait_for_page(url=f"http://localhost:{dash_duo.server.port}/v2")
63+
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/v2")
6364
dash_duo.wait_for_text_to_equal("#text_redirect", "text for redirect")
64-
dash_duo.wait_for_page(url=f"http://localhost:{dash_duo.server.port}/old-home-page")
65+
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/old-home-page")
6566
dash_duo.wait_for_text_to_equal("#text_redirect", "text for redirect")
66-
assert (
67-
dash_duo.driver.current_url
68-
== f"http://localhost:{dash_duo.server.port}/redirect"
69-
)
67+
assert dash_duo.driver.current_url == f"{dash_duo.server_url}/redirect"
7068

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

7775
# test query strings
78-
dash_duo.wait_for_page(
79-
url=f"http://localhost:{dash_duo.server.port}/query-string?velocity=10"
80-
)
76+
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/query-string?velocity=10")
8177
assert (
8278
dash_duo.find_element("#velocity").get_attribute("value") == "10"
8379
), "query string passed to layout"
8480

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

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

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

9692
# test `validation_layout` exists when suppress_callback_exceptions=False`
@@ -121,20 +117,20 @@ def test_pala002_meta_tags_default(dash_duo, clear_pages_state):
121117
{"property": "twitter:card", "content": "summary_large_image"},
122118
{
123119
"property": "twitter:url",
124-
"content": f"http://localhost:{dash_duo.server.port}/",
120+
"content": f"{dash_duo.server_url}/",
125121
},
126122
{"property": "twitter:title", "content": "Multi layout2"},
127123
{"property": "twitter:description", "content": ""},
128124
{
129125
"property": "twitter:image",
130-
"content": f"http://localhost:{dash_duo.server.port}/assets/app.jpeg",
126+
"content": f"{dash_duo.server_url}/assets/app.jpeg",
131127
},
132128
{"property": "og:title", "content": "Multi layout2"},
133129
{"property": "og:type", "content": "website"},
134130
{"property": "og:description", "content": ""},
135131
{
136132
"property": "og:image",
137-
"content": f"http://localhost:{dash_duo.server.port}/assets/app.jpeg",
133+
"content": f"{dash_duo.server_url}/assets/app.jpeg",
138134
},
139135
]
140136

@@ -149,7 +145,7 @@ def test_pala003_meta_tags_custom(dash_duo, clear_pages_state):
149145
{"property": "twitter:card", "content": "summary_large_image"},
150146
{
151147
"property": "twitter:url",
152-
"content": f"http://localhost:{dash_duo.server.port}/",
148+
"content": f"{dash_duo.server_url}/",
153149
},
154150
{"property": "twitter:title", "content": "Supplied Title"},
155151
{
@@ -158,14 +154,14 @@ def test_pala003_meta_tags_custom(dash_duo, clear_pages_state):
158154
},
159155
{
160156
"property": "twitter:image",
161-
"content": f"http://localhost:{dash_duo.server.port}/assets/birds.jpeg",
157+
"content": f"{dash_duo.server_url}/assets/birds.jpeg",
162158
},
163159
{"property": "og:title", "content": "Supplied Title"},
164160
{"property": "og:type", "content": "website"},
165161
{"property": "og:description", "content": "This is the supplied description"},
166162
{
167163
"property": "og:image",
168-
"content": f"http://localhost:{dash_duo.server.port}/assets/birds.jpeg",
164+
"content": f"{dash_duo.server_url}/assets/birds.jpeg",
169165
},
170166
]
171167

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

181177
assert error_msg in err.value.args[0]
178+
179+
180+
def get_routing_inputs_app():
181+
app = Dash(
182+
__name__,
183+
use_pages=True,
184+
routing_callback_inputs={
185+
"hash": State(_ID_LOCATION, "hash"),
186+
"language": Input("language", "value"),
187+
},
188+
)
189+
# Page with layout from a variable: should render and not be impacted
190+
# by routing callback inputs
191+
dash.register_page(
192+
"home",
193+
layout=html.Div("Home", id="contents"),
194+
path="/",
195+
)
196+
197+
# Page with a layout function, should see the routing callback inputs
198+
# as keyword arguments
199+
def layout1(hash: str = None, language: str = "en", **kwargs):
200+
translations = {
201+
"en": "Hash says: {}",
202+
"fr": "Le hash dit: {}",
203+
}
204+
return html.Div(translations[language].format(hash), id="contents")
205+
206+
dash.register_page(
207+
"function_layout",
208+
path="/function-layout",
209+
layout=layout1,
210+
)
211+
app.layout = html.Div(
212+
[
213+
dcc.Dropdown(id="language", options=["en", "fr"], value="en"),
214+
dash.page_container,
215+
]
216+
)
217+
return app
218+
219+
220+
def test_pala005_routing_inputs(dash_duo, clear_pages_state):
221+
dash_duo.start_server(get_routing_inputs_app())
222+
dash_duo.wait_for_page(url=f"{dash_duo.server_url}#123")
223+
dash_duo.wait_for_text_to_equal("#contents", "Home")
224+
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/")
225+
dash_duo.wait_for_text_to_equal("#contents", "Home")
226+
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/function-layout")
227+
dash_duo.wait_for_text_to_equal("#contents", "Hash says:")
228+
# hash is a State therefore navigating to the same page with hash will not
229+
# re-render the layout function
230+
dash_duo.wait_for_page(url=f"{dash_duo.server_url}/function-layout#123")
231+
dash_duo.wait_for_text_to_equal("#contents", "Hash says:")
232+
# Refreshing the page re-runs the layout function
233+
dash_duo.driver.refresh()
234+
dash_duo.wait_for_text_to_equal("#contents", "Hash says: #123")
235+
# Changing the language Input re-runs the layout function
236+
dash_duo.select_dcc_dropdown("#language", "fr")
237+
dash_duo.wait_for_text_to_equal("#contents", "Le hash dit: #123")

0 commit comments

Comments
 (0)