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

don't override document.title if set to None #1343

Merged
merged 18 commits into from
Jul 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).

## Unreleased
### Added
- [#1343](https://github.com/plotly/dash/pull/1343) Add `title` parameter to set the
document title. This is the recommended alternative to setting app.title or overriding
the index HTML.
- [#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)

## [1.13.4] - 2020-06-25
Expand Down
19 changes: 15 additions & 4 deletions dash-renderer/src/components/core/DocumentTitle.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,27 @@ class DocumentTitle extends Component {
super(props);
const {update_title} = props.config;
this.state = {
initialTitle: document.title,
title: document.title,
update_title,
};
}

UNSAFE_componentWillReceiveProps(props) {
if (this.state.update_title && props.isLoading) {
document.title = this.state.update_title;
if (!this.state.update_title) {
// Let callbacks or other components have full control over title
return;
}
if (props.isLoading) {
this.setState({title: document.title});
if (this.state.update_title) {
document.title = this.state.update_title;
}
} else {
Copy link
Contributor

@Marc-Andre-Rivet Marc-Andre-Rivet Jul 24, 2020

Choose a reason for hiding this comment

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

While this allows the clientside callback to set the value, it seems to break existing tests / behavior. Would a better verification be to only revert back to inititalTitle if the value is equal to update_title instead? Possibly the same chekc for update_title - only update if the value is currently initialTitle.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks! I totally misinterpreted what update_title meant. I believe e190cc9 is the logic we're looking for

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it'd be safer to bail out entirely (before even the props.isLoading test) if !update_title. Then this entire component would be a noop, as it should be in that case; callbacks can do whatever they want with document.title and we won't interfere.

When you do have an update_title, I may agree with @Marc-Andre-Rivet that there needs to be a test when we're reverting back to the non-updating title. Consider a callback chain, where the first callback changes document.title, but we're still in a loading state so DocumentTitle doesn't receive new props and we don't pick up the changed title into this.state. Then after the second callback we reset document.title to this.state.title, losing the callback-provided title.

Seems to me the logic you have right now is fine during loading, but when reverting to this.state.title below it would be better to do something like:

if (document.title === this.state.update_title) {
    document.title = this.state.title;
} else {
    this.setState({title: document.title});
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks @alexcjohnson @Marc-Andre-Rivet ! I believe I have addressed your comments and added the appropriate tests now.

document.title = this.state.initialTitle;
if (document.title === this.state.update_title) {
document.title = this.state.title;
} else {
this.setState({title: document.title});
}
}
}

Expand Down
18 changes: 17 additions & 1 deletion dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,15 @@ class Dash(object):
with a ``plug`` method, taking a single argument: this app, which will
be called after the Flask server is attached.
:type plugins: list of objects

:param title: Default ``Dash``. Configures the document.title
(the text that appears in a browser tab).

:param update_title: Default ``Updating...``. Configures the document.title
(the text that appears in a browser tab) text when a callback is being run.
Set to None or '' if you don't want the document.title to change or if you
want to control the document.title through a separate component or
clientside callback.
"""

def __init__(
Expand All @@ -252,6 +261,7 @@ def __init__(
prevent_initial_callbacks=False,
show_undo_redo=False,
plugins=None,
title='Dash',
update_title="Updating...",
**obsolete
):
Expand Down Expand Up @@ -300,6 +310,7 @@ def __init__(
),
prevent_initial_callbacks=prevent_initial_callbacks,
show_undo_redo=show_undo_redo,
title=title,
update_title=update_title,
)
self.config.set_read_only(
Expand All @@ -321,6 +332,9 @@ def __init__(
"via the Dash constructor"
)

# keep title as a class property for backwards compatability
self.title = title

# list of dependencies - this one is used by the back end for dispatching
self.callback_map = {}
# same deps as a list to catch duplicate outputs, and to send to the front end
Expand Down Expand Up @@ -725,7 +739,9 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument
config = self._generate_config_html()
metas = self._generate_meta_html()
renderer = self._generate_renderer()
title = getattr(self, "title", "Dash")

# use self.title instead of app.config.title for backwards compatibility
Copy link
Collaborator

Choose a reason for hiding this comment

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

In principle we could make title into a @property that syncs app.title with app.config.title - but not a big deal. Most people should just use the kwarg regardless.

title = self.title

if self._favicon:
favicon_mod_time = os.path.getmtime(
Expand Down
3 changes: 2 additions & 1 deletion requires-dev.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
dash_flow_example==0.0.5
dash-dangerously-set-inner-html
isort==4.3.21
mock==4.0.1;python_version>="3.0"
mock==3.0.5;python_version=="2.7"
flake8==3.7.9
Expand All @@ -10,4 +11,4 @@ astroid==2.2.5;python_version=="3.7"
black==19.10b0;python_version>="3.0"
virtualenv==20.0.10;python_version=="2.7"
fire==0.2.1
coloredlogs==14.0
coloredlogs==14.0
118 changes: 104 additions & 14 deletions tests/integration/renderer/test_loading_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,21 @@ def find_text(spec):


@pytest.mark.parametrize(
"kwargs, expected_update_title",
"kwargs, expected_update_title, clientside_title",
[
({}, "Updating..."),
({"update_title": None}, "Dash"),
({"update_title": ""}, "Dash"),
({"update_title": "Hello World"}, "Hello World"),
]
({}, "Updating...", False),
({"update_title": None}, "Dash", False),
({"update_title": ""}, "Dash", False),
({"update_title": "Hello World"}, "Hello World", False),
({}, "Updating...", True),
({"update_title": None}, "Dash", True),
({"update_title": ""}, "Dash", True),
({"update_title": "Hello World"}, "Hello World", True),
],
)
def test_rdls003_update_title(dash_duo, kwargs, expected_update_title):
def test_rdls003_update_title(
dash_duo, kwargs, expected_update_title, clientside_title
):
app = dash.Dash("Dash", **kwargs)
lock = Lock()

Expand All @@ -189,26 +195,110 @@ def test_rdls003_update_title(dash_duo, kwargs, expected_update_title):
html.H3("Press button see document title updating"),
html.Div(id="output"),
html.Button("Update", id="button", n_clicks=0),
html.Button("Update Page", id="page", n_clicks=0),
html.Div(id="dummy"),
]
)
if clientside_title:
app.clientside_callback(
"""
function(n_clicks) {
document.title = 'Page ' + n_clicks;
return 'Page ' + n_clicks;
}
""",
Output("dummy", "children"),
[Input("page", "n_clicks")],
)

@app.callback(
Output("output", "children"),
[Input("button", "n_clicks")]
)
@app.callback(Output("output", "children"), [Input("button", "n_clicks")])
def update(n):
with lock:
return n

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

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

with lock:
dash_duo.find_element("#button").click()
# check for update-title while processing callback
until(lambda: dash_duo.driver.title == expected_update_title, timeout=1)
if clientside_title and not kwargs.get('update_title', True):
until(lambda: dash_duo.driver.title == 'Page 0', timeout=1)
else:
until(lambda: dash_duo.driver.title == expected_update_title, timeout=1)

if clientside_title:
dash_duo.find_element("#page").click()
dash_duo.wait_for_text_to_equal("#dummy", "Page 1")
until(lambda: dash_duo.driver.title == "Page 1", timeout=1)

# verify that when a separate callback runs, the page title gets restored
dash_duo.find_element("#button").click()
dash_duo.wait_for_text_to_equal("#output", "2")
if clientside_title:
until(lambda: dash_duo.driver.title == "Page 1", timeout=1)
else:
until(lambda: dash_duo.driver.title == "Dash", timeout=1)


@pytest.mark.parametrize(
"update_title",
[
None,
'Custom Update Title',
],
)
def test_rdls004_update_title_chained_callbacks(dash_duo, update_title):
initial_title = 'Initial Title'
app = dash.Dash("Dash", title=initial_title, update_title=update_title)
lock = Lock()

app.layout = html.Div(
children=[
html.Button(id="page-title", n_clicks=0, children='Page Title'),
html.Div(id="page-output"),
html.Div(id="final-output")
]
)
app.clientside_callback(
"""
function(n_clicks) {
if (n_clicks > 0) {
document.title = 'Page ' + n_clicks;
}
return n_clicks;
}
""",
Output("page-output", "children"),
[Input("page-title", "n_clicks")],
)

@app.callback(
Output("final-output", "children"),
[Input("page-output", "children")])
def update(n):
with lock:
return n

# check for original title after loading
dash_duo.start_server(app)
dash_duo.wait_for_text_to_equal("#final-output", "0")
until(lambda: dash_duo.driver.title == initial_title, timeout=1)

with lock:
dash_duo.find_element("#page-title").click()
# check for update-title while processing the serverside callback
if update_title:
until(lambda: dash_duo.driver.title == update_title, timeout=1)
else:
until(lambda: dash_duo.driver.title == 'Page 1', timeout=1)

dash_duo.wait_for_text_to_equal("#final-output", "1")
until(lambda: dash_duo.driver.title == 'Page 1', timeout=1)
10 changes: 10 additions & 0 deletions tests/unit/test_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,13 @@ def test_proxy_failure(mocker, empty_environ):
)
assert "port: 8055 is incompatible with the proxy" in excinfo.exconly()
assert "you must use port: 8155" in excinfo.exconly()


def test_title():
app = Dash()
assert '<title>Dash</title>' in app.index()
app = Dash()
app.title = 'Hello World'
assert '<title>Hello World</title>' in app.index()
app = Dash(title='Custom Title')
assert '<title>Custom Title</title>' in app.index()