Skip to content

Commit 3435639

Browse files
authored
Don't override document.title if set to None & add title to Dash constructor
1 parent 1c92d1a commit 3435639

File tree

6 files changed

+151
-20
lines changed

6 files changed

+151
-20
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).
44

55
## Unreleased
66
### Added
7+
- [#1343](https://github.com/plotly/dash/pull/1343) Add `title` parameter to set the
8+
document title. This is the recommended alternative to setting app.title or overriding
9+
the index HTML.
710
- [#1315](https://github.com/plotly/dash/pull/1315) Add `update_title` parameter to set or disable the "Updating...." document title during updates. Closes [#856](https://github.com/plotly/dash/issues/856) and [#732](https://github.com/plotly/dash/issues/732)
811

912
## [1.13.4] - 2020-06-25

dash-renderer/src/components/core/DocumentTitle.react.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,27 @@ class DocumentTitle extends Component {
77
super(props);
88
const {update_title} = props.config;
99
this.state = {
10-
initialTitle: document.title,
10+
title: document.title,
1111
update_title,
1212
};
1313
}
1414

1515
UNSAFE_componentWillReceiveProps(props) {
16-
if (this.state.update_title && props.isLoading) {
17-
document.title = this.state.update_title;
16+
if (!this.state.update_title) {
17+
// Let callbacks or other components have full control over title
18+
return;
19+
}
20+
if (props.isLoading) {
21+
this.setState({title: document.title});
22+
if (this.state.update_title) {
23+
document.title = this.state.update_title;
24+
}
1825
} else {
19-
document.title = this.state.initialTitle;
26+
if (document.title === this.state.update_title) {
27+
document.title = this.state.title;
28+
} else {
29+
this.setState({title: document.title});
30+
}
2031
}
2132
}
2233

dash/dash.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,15 @@ class Dash(object):
227227
with a ``plug`` method, taking a single argument: this app, which will
228228
be called after the Flask server is attached.
229229
:type plugins: list of objects
230+
231+
:param title: Default ``Dash``. Configures the document.title
232+
(the text that appears in a browser tab).
233+
234+
:param update_title: Default ``Updating...``. Configures the document.title
235+
(the text that appears in a browser tab) text when a callback is being run.
236+
Set to None or '' if you don't want the document.title to change or if you
237+
want to control the document.title through a separate component or
238+
clientside callback.
230239
"""
231240

232241
def __init__(
@@ -252,6 +261,7 @@ def __init__(
252261
prevent_initial_callbacks=False,
253262
show_undo_redo=False,
254263
plugins=None,
264+
title='Dash',
255265
update_title="Updating...",
256266
**obsolete
257267
):
@@ -300,6 +310,7 @@ def __init__(
300310
),
301311
prevent_initial_callbacks=prevent_initial_callbacks,
302312
show_undo_redo=show_undo_redo,
313+
title=title,
303314
update_title=update_title,
304315
)
305316
self.config.set_read_only(
@@ -321,6 +332,9 @@ def __init__(
321332
"via the Dash constructor"
322333
)
323334

335+
# keep title as a class property for backwards compatability
336+
self.title = title
337+
324338
# list of dependencies - this one is used by the back end for dispatching
325339
self.callback_map = {}
326340
# same deps as a list to catch duplicate outputs, and to send to the front end
@@ -725,7 +739,9 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument
725739
config = self._generate_config_html()
726740
metas = self._generate_meta_html()
727741
renderer = self._generate_renderer()
728-
title = getattr(self, "title", "Dash")
742+
743+
# use self.title instead of app.config.title for backwards compatibility
744+
title = self.title
729745

730746
if self._favicon:
731747
favicon_mod_time = os.path.getmtime(

requires-dev.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
dash_flow_example==0.0.5
22
dash-dangerously-set-inner-html
3+
isort==4.3.21
34
mock==4.0.1;python_version>="3.0"
45
mock==3.0.5;python_version=="2.7"
56
flake8==3.7.9
@@ -10,4 +11,4 @@ astroid==2.2.5;python_version=="3.7"
1011
black==19.10b0;python_version>="3.0"
1112
virtualenv==20.0.10;python_version=="2.7"
1213
fire==0.2.1
13-
coloredlogs==14.0
14+
coloredlogs==14.0

tests/integration/renderer/test_loading_states.py

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,21 @@ def find_text(spec):
172172

173173

174174
@pytest.mark.parametrize(
175-
"kwargs, expected_update_title",
175+
"kwargs, expected_update_title, clientside_title",
176176
[
177-
({}, "Updating..."),
178-
({"update_title": None}, "Dash"),
179-
({"update_title": ""}, "Dash"),
180-
({"update_title": "Hello World"}, "Hello World"),
181-
]
177+
({}, "Updating...", False),
178+
({"update_title": None}, "Dash", False),
179+
({"update_title": ""}, "Dash", False),
180+
({"update_title": "Hello World"}, "Hello World", False),
181+
({}, "Updating...", True),
182+
({"update_title": None}, "Dash", True),
183+
({"update_title": ""}, "Dash", True),
184+
({"update_title": "Hello World"}, "Hello World", True),
185+
],
182186
)
183-
def test_rdls003_update_title(dash_duo, kwargs, expected_update_title):
187+
def test_rdls003_update_title(
188+
dash_duo, kwargs, expected_update_title, clientside_title
189+
):
184190
app = dash.Dash("Dash", **kwargs)
185191
lock = Lock()
186192

@@ -189,26 +195,110 @@ def test_rdls003_update_title(dash_duo, kwargs, expected_update_title):
189195
html.H3("Press button see document title updating"),
190196
html.Div(id="output"),
191197
html.Button("Update", id="button", n_clicks=0),
198+
html.Button("Update Page", id="page", n_clicks=0),
199+
html.Div(id="dummy"),
192200
]
193201
)
202+
if clientside_title:
203+
app.clientside_callback(
204+
"""
205+
function(n_clicks) {
206+
document.title = 'Page ' + n_clicks;
207+
return 'Page ' + n_clicks;
208+
}
209+
""",
210+
Output("dummy", "children"),
211+
[Input("page", "n_clicks")],
212+
)
194213

195-
@app.callback(
196-
Output("output", "children"),
197-
[Input("button", "n_clicks")]
198-
)
214+
@app.callback(Output("output", "children"), [Input("button", "n_clicks")])
199215
def update(n):
200216
with lock:
201217
return n
202218

203219
with lock:
204220
dash_duo.start_server(app)
205221
# check for update-title during startup
206-
until(lambda: dash_duo.driver.title == expected_update_title, timeout=1)
222+
# the clientside callback isn't blocking so it may update the title
223+
if not clientside_title:
224+
until(lambda: dash_duo.driver.title == expected_update_title, timeout=1)
207225

208226
# check for original title after loading
209-
until(lambda: dash_duo.driver.title == "Dash", timeout=1)
227+
until(lambda: dash_duo.driver.title == "Page 0" if clientside_title else "Dash", timeout=1)
210228

211229
with lock:
212230
dash_duo.find_element("#button").click()
213231
# check for update-title while processing callback
214-
until(lambda: dash_duo.driver.title == expected_update_title, timeout=1)
232+
if clientside_title and not kwargs.get('update_title', True):
233+
until(lambda: dash_duo.driver.title == 'Page 0', timeout=1)
234+
else:
235+
until(lambda: dash_duo.driver.title == expected_update_title, timeout=1)
236+
237+
if clientside_title:
238+
dash_duo.find_element("#page").click()
239+
dash_duo.wait_for_text_to_equal("#dummy", "Page 1")
240+
until(lambda: dash_duo.driver.title == "Page 1", timeout=1)
241+
242+
# verify that when a separate callback runs, the page title gets restored
243+
dash_duo.find_element("#button").click()
244+
dash_duo.wait_for_text_to_equal("#output", "2")
245+
if clientside_title:
246+
until(lambda: dash_duo.driver.title == "Page 1", timeout=1)
247+
else:
248+
until(lambda: dash_duo.driver.title == "Dash", timeout=1)
249+
250+
251+
@pytest.mark.parametrize(
252+
"update_title",
253+
[
254+
None,
255+
'Custom Update Title',
256+
],
257+
)
258+
def test_rdls004_update_title_chained_callbacks(dash_duo, update_title):
259+
initial_title = 'Initial Title'
260+
app = dash.Dash("Dash", title=initial_title, update_title=update_title)
261+
lock = Lock()
262+
263+
app.layout = html.Div(
264+
children=[
265+
html.Button(id="page-title", n_clicks=0, children='Page Title'),
266+
html.Div(id="page-output"),
267+
html.Div(id="final-output")
268+
]
269+
)
270+
app.clientside_callback(
271+
"""
272+
function(n_clicks) {
273+
if (n_clicks > 0) {
274+
document.title = 'Page ' + n_clicks;
275+
}
276+
return n_clicks;
277+
}
278+
""",
279+
Output("page-output", "children"),
280+
[Input("page-title", "n_clicks")],
281+
)
282+
283+
@app.callback(
284+
Output("final-output", "children"),
285+
[Input("page-output", "children")])
286+
def update(n):
287+
with lock:
288+
return n
289+
290+
# check for original title after loading
291+
dash_duo.start_server(app)
292+
dash_duo.wait_for_text_to_equal("#final-output", "0")
293+
until(lambda: dash_duo.driver.title == initial_title, timeout=1)
294+
295+
with lock:
296+
dash_duo.find_element("#page-title").click()
297+
# check for update-title while processing the serverside callback
298+
if update_title:
299+
until(lambda: dash_duo.driver.title == update_title, timeout=1)
300+
else:
301+
until(lambda: dash_duo.driver.title == 'Page 1', timeout=1)
302+
303+
dash_duo.wait_for_text_to_equal("#final-output", "1")
304+
until(lambda: dash_duo.driver.title == 'Page 1', timeout=1)

tests/unit/test_configs.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,13 @@ def test_proxy_failure(mocker, empty_environ):
314314
)
315315
assert "port: 8055 is incompatible with the proxy" in excinfo.exconly()
316316
assert "you must use port: 8155" in excinfo.exconly()
317+
318+
319+
def test_title():
320+
app = Dash()
321+
assert '<title>Dash</title>' in app.index()
322+
app = Dash()
323+
app.title = 'Hello World'
324+
assert '<title>Hello World</title>' in app.index()
325+
app = Dash(title='Custom Title')
326+
assert '<title>Custom Title</title>' in app.index()

0 commit comments

Comments
 (0)