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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [UNRELEASED]
### Added
- [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior.
- [#1201](https://github.com/plotly/dash/pull/1201) New attribute `app.validation_layout` allows you to create a multi-page app without `suppress_callback_exceptions=True` or layout function tricks. Set this to a component layout containing the superset of all IDs on all pages in your app.
- [#1078](https://github.com/plotly/dash/pull/1078) Permit usage of arbitrary file extensions for assets within component libraries

Expand Down
15 changes: 10 additions & 5 deletions dash-renderer/src/actions/dependencies.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -1238,6 +1238,9 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
) {
// This callback should trigger even with no changedProps,
// since the props that changed no longer exist.
// We're kind of abusing the `initialCall` flag here, it's
// more like a "final call" for the removed inputs, but
// this case is not subject to `prevent_initial_call`.
if (flatten(cb.getOutputs(newPaths)).length) {
cb.initialCall = true;
cb.changedPropIds = {};
Expand All@@ -1255,10 +1258,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
const cb = getCallbackByOutput(graphs, paths, id, property);
if (cb) {
// callbacks found in the layout by output should always run
// unless specifically requested not to.
// ie this is the initial call of this callback even if it's
// not the page initialization but just a new layout chunk
cb.initialCall = true;
addCallback(cb);
if (!cb.callback.prevent_initial_call) {
cb.initialCall = true;
addCallback(cb);
}
}
}
}
Expand All@@ -1270,14 +1276,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
if (chunkPath) {
handleThisCallback = cb => {
if (
all(
!all(
startsWith(chunkPath),
pluck('path', flatten(cb.getOutputs(paths)))
)
) {
cb.changedPropIds = {};

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I'm not sure what I was thinking here with clearing out changedPropIds, but in this case (we've found a callback by its inputs due to a layout change, and just determined that all its outputs are in the same layout chunk) we know the callback is already included in the queue via its outputs so the best thing to do is simply not re-include it at all. This extra addCallback was harmless before prevent_initial_call, just a slight bit of extra work; but now that we have prevent_initial_call it's possible this callback would not be added based on its outputs.

If this condition isn't met, there are outputs outside the new layout chunk and it's not in fact an initial call anyway - so it's appropriate to add the callback and that's what the new logic does.

maybeAddCallback(cb);
}
maybeAddCallback(cb);
};
}
for (const property in inIdCallbacks) {
Expand Down
42 changes: 37 additions & 5 deletions dash/dash.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -211,6 +211,15 @@ class Dash(object):
env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
:type suppress_callback_exceptions: boolean

:param prevent_initial_callbacks: Default ``False``: Sets the default value
of ``prevent_initial_call`` for all callbacks added to the app.
Normally all callbacks are fired when the associated outputs are first
added to the page. You can disable this for individual callbacks by
setting ``prevent_initial_call`` in their definitions, or set it
``True`` here in which case you must explicitly set it ``False`` for
those callbacks you wish to have an initial call. This setting has no
effect on triggering callbacks when their inputs change later on.

:param show_undo_redo: Default ``False``, set to ``True`` to enable undo
and redo buttons for stepping through the history of the app state.
:type show_undo_redo: boolean
Expand DownExpand Up@@ -241,6 +250,7 @@ def __init__(
external_scripts=None,
external_stylesheets=None,
suppress_callback_exceptions=None,
prevent_initial_callbacks=False,
show_undo_redo=False,
plugins=None,
**obsolete
Expand DownExpand Up@@ -288,6 +298,7 @@ def __init__(
suppress_callback_exceptions=get_combined_config(
"suppress_callback_exceptions", suppress_callback_exceptions, False
),
prevent_initial_callbacks=prevent_initial_callbacks,
show_undo_redo=show_undo_redo,
)
self.config.set_read_only(
Expand DownExpand Up@@ -813,14 +824,18 @@ def interpolate_index(self, **kwargs):
def dependencies(self):
return flask.jsonify(self._callback_list)

def _insert_callback(self, output, inputs, state):
def _insert_callback(self, output, inputs, state, prevent_initial_call):
if prevent_initial_call is None:
prevent_initial_call = self.config.prevent_initial_callbacks

_validate.validate_callback(output, inputs, state)
callback_id = create_callback_id(output)
callback_spec = {
"output": callback_id,
"inputs": [c.to_dict() for c in inputs],
"state": [c.to_dict() for c in state],
"clientside_function": None,
"prevent_initial_call": prevent_initial_call,
}
self.callback_map[callback_id] = {
"inputs": callback_spec["inputs"],
Expand All@@ -830,7 +845,9 @@ def _insert_callback(self, output, inputs, state):

return callback_id

def clientside_callback(self, clientside_function, output, inputs, state=()):
def clientside_callback(
self, clientside_function, output, inputs, state=(), prevent_initial_call=None
):
"""Create a callback that updates the output by calling a clientside
(JavaScript) function instead of a Python function.

Expand DownExpand Up@@ -890,8 +907,12 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
Input('another-input', 'value')]
)
```

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
self._insert_callback(output, inputs, state)
self._insert_callback(output, inputs, state, prevent_initial_call)

# If JS source is explicitly given, create a namespace and function
# name, then inject the code.
Expand DownExpand Up@@ -922,8 +943,19 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
"function_name": function_name,
}

def callback(self, output, inputs, state=()):
callback_id = self._insert_callback(output, inputs, state)
def callback(self, output, inputs, state=(), prevent_initial_call=None):
"""
Normally used as a decorator, `@app.callback` provides a server-side
callback relating the values of one or more `output` items to one or
more `input` items which will trigger the callback when they change,
and optionally `state` items which provide additional information but
do not trigger the callback directly.

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
callback_id = self._insert_callback(output, inputs, state, prevent_initial_call)
multi = isinstance(output, (list, tuple))

def wrap_func(func):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [UNRELEASED]
### Added
- [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior.
- [#1201](https://github.com/plotly/dash/pull/1201) New attribute `app.validation_layout` allows you to create a multi-page app without `suppress_callback_exceptions=True` or layout function tricks. Set this to a component layout containing the superset of all IDs on all pages in your app.
- [#1078](https://github.com/plotly/dash/pull/1078) Permit usage of arbitrary file extensions for assets within component libraries

Expand Down
15 changes: 10 additions & 5 deletions dash-renderer/src/actions/dependencies.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -1238,6 +1238,9 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
) {
// This callback should trigger even with no changedProps,
// since the props that changed no longer exist.
// We're kind of abusing the `initialCall` flag here, it's
// more like a "final call" for the removed inputs, but
// this case is not subject to `prevent_initial_call`.
if (flatten(cb.getOutputs(newPaths)).length) {
cb.initialCall = true;
cb.changedPropIds = {};
Expand All@@ -1255,10 +1258,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
const cb = getCallbackByOutput(graphs, paths, id, property);
if (cb) {
// callbacks found in the layout by output should always run
// unless specifically requested not to.
// ie this is the initial call of this callback even if it's
// not the page initialization but just a new layout chunk
cb.initialCall = true;
addCallback(cb);
if (!cb.callback.prevent_initial_call) {
cb.initialCall = true;
addCallback(cb);
}
}
}
}
Expand All@@ -1270,14 +1276,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
if (chunkPath) {
handleThisCallback = cb => {
if (
all(
!all(
startsWith(chunkPath),
pluck('path', flatten(cb.getOutputs(paths)))
)
) {
cb.changedPropIds = {};

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I'm not sure what I was thinking here with clearing out changedPropIds, but in this case (we've found a callback by its inputs due to a layout change, and just determined that all its outputs are in the same layout chunk) we know the callback is already included in the queue via its outputs so the best thing to do is simply not re-include it at all. This extra addCallback was harmless before prevent_initial_call, just a slight bit of extra work; but now that we have prevent_initial_call it's possible this callback would not be added based on its outputs.

If this condition isn't met, there are outputs outside the new layout chunk and it's not in fact an initial call anyway - so it's appropriate to add the callback and that's what the new logic does.

maybeAddCallback(cb);
}
maybeAddCallback(cb);
};
}
for (const property in inIdCallbacks) {
Expand Down
42 changes: 37 additions & 5 deletions dash/dash.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -211,6 +211,15 @@ class Dash(object):
env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
:type suppress_callback_exceptions: boolean

:param prevent_initial_callbacks: Default ``False``: Sets the default value
of ``prevent_initial_call`` for all callbacks added to the app.
Normally all callbacks are fired when the associated outputs are first
added to the page. You can disable this for individual callbacks by
setting ``prevent_initial_call`` in their definitions, or set it
``True`` here in which case you must explicitly set it ``False`` for
those callbacks you wish to have an initial call. This setting has no
effect on triggering callbacks when their inputs change later on.

:param show_undo_redo: Default ``False``, set to ``True`` to enable undo
and redo buttons for stepping through the history of the app state.
:type show_undo_redo: boolean
Expand DownExpand Up@@ -241,6 +250,7 @@ def __init__(
external_scripts=None,
external_stylesheets=None,
suppress_callback_exceptions=None,
prevent_initial_callbacks=False,
show_undo_redo=False,
plugins=None,
**obsolete
Expand DownExpand Up@@ -288,6 +298,7 @@ def __init__(
suppress_callback_exceptions=get_combined_config(
"suppress_callback_exceptions", suppress_callback_exceptions, False
),
prevent_initial_callbacks=prevent_initial_callbacks,
show_undo_redo=show_undo_redo,
)
self.config.set_read_only(
Expand DownExpand Up@@ -813,14 +824,18 @@ def interpolate_index(self, **kwargs):
def dependencies(self):
return flask.jsonify(self._callback_list)

def _insert_callback(self, output, inputs, state):
def _insert_callback(self, output, inputs, state, prevent_initial_call):
if prevent_initial_call is None:
prevent_initial_call = self.config.prevent_initial_callbacks

_validate.validate_callback(output, inputs, state)
callback_id = create_callback_id(output)
callback_spec = {
"output": callback_id,
"inputs": [c.to_dict() for c in inputs],
"state": [c.to_dict() for c in state],
"clientside_function": None,
"prevent_initial_call": prevent_initial_call,
}
self.callback_map[callback_id] = {
"inputs": callback_spec["inputs"],
Expand All@@ -830,7 +845,9 @@ def _insert_callback(self, output, inputs, state):

return callback_id

def clientside_callback(self, clientside_function, output, inputs, state=()):
def clientside_callback(
self, clientside_function, output, inputs, state=(), prevent_initial_call=None
):
"""Create a callback that updates the output by calling a clientside
(JavaScript) function instead of a Python function.

Expand DownExpand Up@@ -890,8 +907,12 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
Input('another-input', 'value')]
)
```

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
self._insert_callback(output, inputs, state)
self._insert_callback(output, inputs, state, prevent_initial_call)

# If JS source is explicitly given, create a namespace and function
# name, then inject the code.
Expand DownExpand Up@@ -922,8 +943,19 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
"function_name": function_name,
}

def callback(self, output, inputs, state=()):
callback_id = self._insert_callback(output, inputs, state)
def callback(self, output, inputs, state=(), prevent_initial_call=None):
"""
Normally used as a decorator, `@app.callback` provides a server-side
callback relating the values of one or more `output` items to one or
more `input` items which will trigger the callback when they change,
and optionally `state` items which provide additional information but
do not trigger the callback directly.

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
callback_id = self._insert_callback(output, inputs, state, prevent_initial_call)
multi = isinstance(output, (list, tuple))

def wrap_func(func):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [UNRELEASED]
### Added
- [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior.
- [#1201](https://github.com/plotly/dash/pull/1201) New attribute `app.validation_layout` allows you to create a multi-page app without `suppress_callback_exceptions=True` or layout function tricks. Set this to a component layout containing the superset of all IDs on all pages in your app.
- [#1078](https://github.com/plotly/dash/pull/1078) Permit usage of arbitrary file extensions for assets within component libraries

Expand Down
15 changes: 10 additions & 5 deletions dash-renderer/src/actions/dependencies.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -1238,6 +1238,9 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
) {
// This callback should trigger even with no changedProps,
// since the props that changed no longer exist.
// We're kind of abusing the `initialCall` flag here, it's
// more like a "final call" for the removed inputs, but
// this case is not subject to `prevent_initial_call`.
if (flatten(cb.getOutputs(newPaths)).length) {
cb.initialCall = true;
cb.changedPropIds = {};
Expand All@@ -1255,10 +1258,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
const cb = getCallbackByOutput(graphs, paths, id, property);
if (cb) {
// callbacks found in the layout by output should always run
// unless specifically requested not to.
// ie this is the initial call of this callback even if it's
// not the page initialization but just a new layout chunk
cb.initialCall = true;
addCallback(cb);
if (!cb.callback.prevent_initial_call) {
cb.initialCall = true;
addCallback(cb);
}
}
}
}
Expand All@@ -1270,14 +1276,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
if (chunkPath) {
handleThisCallback = cb => {
if (
all(
!all(
startsWith(chunkPath),
pluck('path', flatten(cb.getOutputs(paths)))
)
) {
cb.changedPropIds = {};

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I'm not sure what I was thinking here with clearing out changedPropIds, but in this case (we've found a callback by its inputs due to a layout change, and just determined that all its outputs are in the same layout chunk) we know the callback is already included in the queue via its outputs so the best thing to do is simply not re-include it at all. This extra addCallback was harmless before prevent_initial_call, just a slight bit of extra work; but now that we have prevent_initial_call it's possible this callback would not be added based on its outputs.

If this condition isn't met, there are outputs outside the new layout chunk and it's not in fact an initial call anyway - so it's appropriate to add the callback and that's what the new logic does.

maybeAddCallback(cb);
}
maybeAddCallback(cb);
};
}
for (const property in inIdCallbacks) {
Expand Down
42 changes: 37 additions & 5 deletions dash/dash.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -211,6 +211,15 @@ class Dash(object):
env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
:type suppress_callback_exceptions: boolean

:param prevent_initial_callbacks: Default ``False``: Sets the default value
of ``prevent_initial_call`` for all callbacks added to the app.
Normally all callbacks are fired when the associated outputs are first
added to the page. You can disable this for individual callbacks by
setting ``prevent_initial_call`` in their definitions, or set it
``True`` here in which case you must explicitly set it ``False`` for
those callbacks you wish to have an initial call. This setting has no
effect on triggering callbacks when their inputs change later on.

:param show_undo_redo: Default ``False``, set to ``True`` to enable undo
and redo buttons for stepping through the history of the app state.
:type show_undo_redo: boolean
Expand DownExpand Up@@ -241,6 +250,7 @@ def __init__(
external_scripts=None,
external_stylesheets=None,
suppress_callback_exceptions=None,
prevent_initial_callbacks=False,
show_undo_redo=False,
plugins=None,
**obsolete
Expand DownExpand Up@@ -288,6 +298,7 @@ def __init__(
suppress_callback_exceptions=get_combined_config(
"suppress_callback_exceptions", suppress_callback_exceptions, False
),
prevent_initial_callbacks=prevent_initial_callbacks,
show_undo_redo=show_undo_redo,
)
self.config.set_read_only(
Expand DownExpand Up@@ -813,14 +824,18 @@ def interpolate_index(self, **kwargs):
def dependencies(self):
return flask.jsonify(self._callback_list)

def _insert_callback(self, output, inputs, state):
def _insert_callback(self, output, inputs, state, prevent_initial_call):
if prevent_initial_call is None:
prevent_initial_call = self.config.prevent_initial_callbacks

_validate.validate_callback(output, inputs, state)
callback_id = create_callback_id(output)
callback_spec = {
"output": callback_id,
"inputs": [c.to_dict() for c in inputs],
"state": [c.to_dict() for c in state],
"clientside_function": None,
"prevent_initial_call": prevent_initial_call,
}
self.callback_map[callback_id] = {
"inputs": callback_spec["inputs"],
Expand All@@ -830,7 +845,9 @@ def _insert_callback(self, output, inputs, state):

return callback_id

def clientside_callback(self, clientside_function, output, inputs, state=()):
def clientside_callback(
self, clientside_function, output, inputs, state=(), prevent_initial_call=None
):
"""Create a callback that updates the output by calling a clientside
(JavaScript) function instead of a Python function.

Expand DownExpand Up@@ -890,8 +907,12 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
Input('another-input', 'value')]
)
```

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
self._insert_callback(output, inputs, state)
self._insert_callback(output, inputs, state, prevent_initial_call)

# If JS source is explicitly given, create a namespace and function
# name, then inject the code.
Expand DownExpand Up@@ -922,8 +943,19 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
"function_name": function_name,
}

def callback(self, output, inputs, state=()):
callback_id = self._insert_callback(output, inputs, state)
def callback(self, output, inputs, state=(), prevent_initial_call=None):
"""
Normally used as a decorator, `@app.callback` provides a server-side
callback relating the values of one or more `output` items to one or
more `input` items which will trigger the callback when they change,
and optionally `state` items which provide additional information but
do not trigger the callback directly.

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
callback_id = self._insert_callback(output, inputs, state, prevent_initial_call)
multi = isinstance(output, (list, tuple))

def wrap_func(func):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [UNRELEASED]
### Added
- [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior.
- [#1201](https://github.com/plotly/dash/pull/1201) New attribute `app.validation_layout` allows you to create a multi-page app without `suppress_callback_exceptions=True` or layout function tricks. Set this to a component layout containing the superset of all IDs on all pages in your app.
- [#1078](https://github.com/plotly/dash/pull/1078) Permit usage of arbitrary file extensions for assets within component libraries

Expand Down
15 changes: 10 additions & 5 deletions dash-renderer/src/actions/dependencies.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -1238,6 +1238,9 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
) {
// This callback should trigger even with no changedProps,
// since the props that changed no longer exist.
// We're kind of abusing the `initialCall` flag here, it's
// more like a "final call" for the removed inputs, but
// this case is not subject to `prevent_initial_call`.
if (flatten(cb.getOutputs(newPaths)).length) {
cb.initialCall = true;
cb.changedPropIds = {};
Expand All@@ -1255,10 +1258,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
const cb = getCallbackByOutput(graphs, paths, id, property);
if (cb) {
// callbacks found in the layout by output should always run
// unless specifically requested not to.
// ie this is the initial call of this callback even if it's
// not the page initialization but just a new layout chunk
cb.initialCall = true;
addCallback(cb);
if (!cb.callback.prevent_initial_call) {
cb.initialCall = true;
addCallback(cb);
}
}
}
}
Expand All@@ -1270,14 +1276,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
if (chunkPath) {
handleThisCallback = cb => {
if (
all(
!all(
startsWith(chunkPath),
pluck('path', flatten(cb.getOutputs(paths)))
)
) {
cb.changedPropIds = {};

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I'm not sure what I was thinking here with clearing out changedPropIds, but in this case (we've found a callback by its inputs due to a layout change, and just determined that all its outputs are in the same layout chunk) we know the callback is already included in the queue via its outputs so the best thing to do is simply not re-include it at all. This extra addCallback was harmless before prevent_initial_call, just a slight bit of extra work; but now that we have prevent_initial_call it's possible this callback would not be added based on its outputs.

If this condition isn't met, there are outputs outside the new layout chunk and it's not in fact an initial call anyway - so it's appropriate to add the callback and that's what the new logic does.

maybeAddCallback(cb);
}
maybeAddCallback(cb);
};
}
for (const property in inIdCallbacks) {
Expand Down
42 changes: 37 additions & 5 deletions dash/dash.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -211,6 +211,15 @@ class Dash(object):
env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
:type suppress_callback_exceptions: boolean

:param prevent_initial_callbacks: Default ``False``: Sets the default value
of ``prevent_initial_call`` for all callbacks added to the app.
Normally all callbacks are fired when the associated outputs are first
added to the page. You can disable this for individual callbacks by
setting ``prevent_initial_call`` in their definitions, or set it
``True`` here in which case you must explicitly set it ``False`` for
those callbacks you wish to have an initial call. This setting has no
effect on triggering callbacks when their inputs change later on.

:param show_undo_redo: Default ``False``, set to ``True`` to enable undo
and redo buttons for stepping through the history of the app state.
:type show_undo_redo: boolean
Expand DownExpand Up@@ -241,6 +250,7 @@ def __init__(
external_scripts=None,
external_stylesheets=None,
suppress_callback_exceptions=None,
prevent_initial_callbacks=False,
show_undo_redo=False,
plugins=None,
**obsolete
Expand DownExpand Up@@ -288,6 +298,7 @@ def __init__(
suppress_callback_exceptions=get_combined_config(
"suppress_callback_exceptions", suppress_callback_exceptions, False
),
prevent_initial_callbacks=prevent_initial_callbacks,
show_undo_redo=show_undo_redo,
)
self.config.set_read_only(
Expand DownExpand Up@@ -813,14 +824,18 @@ def interpolate_index(self, **kwargs):
def dependencies(self):
return flask.jsonify(self._callback_list)

def _insert_callback(self, output, inputs, state):
def _insert_callback(self, output, inputs, state, prevent_initial_call):
if prevent_initial_call is None:
prevent_initial_call = self.config.prevent_initial_callbacks

_validate.validate_callback(output, inputs, state)
callback_id = create_callback_id(output)
callback_spec = {
"output": callback_id,
"inputs": [c.to_dict() for c in inputs],
"state": [c.to_dict() for c in state],
"clientside_function": None,
"prevent_initial_call": prevent_initial_call,
}
self.callback_map[callback_id] = {
"inputs": callback_spec["inputs"],
Expand All@@ -830,7 +845,9 @@ def _insert_callback(self, output, inputs, state):

return callback_id

def clientside_callback(self, clientside_function, output, inputs, state=()):
def clientside_callback(
self, clientside_function, output, inputs, state=(), prevent_initial_call=None
):
"""Create a callback that updates the output by calling a clientside
(JavaScript) function instead of a Python function.

Expand DownExpand Up@@ -890,8 +907,12 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
Input('another-input', 'value')]
)
```

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
self._insert_callback(output, inputs, state)
self._insert_callback(output, inputs, state, prevent_initial_call)

# If JS source is explicitly given, create a namespace and function
# name, then inject the code.
Expand DownExpand Up@@ -922,8 +943,19 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
"function_name": function_name,
}

def callback(self, output, inputs, state=()):
callback_id = self._insert_callback(output, inputs, state)
def callback(self, output, inputs, state=(), prevent_initial_call=None):
"""
Normally used as a decorator, `@app.callback` provides a server-side
callback relating the values of one or more `output` items to one or
more `input` items which will trigger the callback when they change,
and optionally `state` items which provide additional information but
do not trigger the callback directly.

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
callback_id = self._insert_callback(output, inputs, state, prevent_initial_call)
multi = isinstance(output, (list, tuple))

def wrap_func(func):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [UNRELEASED]
### Added
- [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior.
- [#1201](https://github.com/plotly/dash/pull/1201) New attribute `app.validation_layout` allows you to create a multi-page app without `suppress_callback_exceptions=True` or layout function tricks. Set this to a component layout containing the superset of all IDs on all pages in your app.
- [#1078](https://github.com/plotly/dash/pull/1078) Permit usage of arbitrary file extensions for assets within component libraries

Expand Down
15 changes: 10 additions & 5 deletions dash-renderer/src/actions/dependencies.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -1238,6 +1238,9 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
) {
// This callback should trigger even with no changedProps,
// since the props that changed no longer exist.
// We're kind of abusing the `initialCall` flag here, it's
// more like a "final call" for the removed inputs, but
// this case is not subject to `prevent_initial_call`.
if (flatten(cb.getOutputs(newPaths)).length) {
cb.initialCall = true;
cb.changedPropIds = {};
Expand All@@ -1255,10 +1258,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
const cb = getCallbackByOutput(graphs, paths, id, property);
if (cb) {
// callbacks found in the layout by output should always run
// unless specifically requested not to.
// ie this is the initial call of this callback even if it's
// not the page initialization but just a new layout chunk
cb.initialCall = true;
addCallback(cb);
if (!cb.callback.prevent_initial_call) {
cb.initialCall = true;
addCallback(cb);
}
}
}
}
Expand All@@ -1270,14 +1276,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
if (chunkPath) {
handleThisCallback = cb => {
if (
all(
!all(
startsWith(chunkPath),
pluck('path', flatten(cb.getOutputs(paths)))
)
) {
cb.changedPropIds = {};

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I'm not sure what I was thinking here with clearing out changedPropIds, but in this case (we've found a callback by its inputs due to a layout change, and just determined that all its outputs are in the same layout chunk) we know the callback is already included in the queue via its outputs so the best thing to do is simply not re-include it at all. This extra addCallback was harmless before prevent_initial_call, just a slight bit of extra work; but now that we have prevent_initial_call it's possible this callback would not be added based on its outputs.

If this condition isn't met, there are outputs outside the new layout chunk and it's not in fact an initial call anyway - so it's appropriate to add the callback and that's what the new logic does.

maybeAddCallback(cb);
}
maybeAddCallback(cb);
};
}
for (const property in inIdCallbacks) {
Expand Down
42 changes: 37 additions & 5 deletions dash/dash.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -211,6 +211,15 @@ class Dash(object):
env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
:type suppress_callback_exceptions: boolean

:param prevent_initial_callbacks: Default ``False``: Sets the default value
of ``prevent_initial_call`` for all callbacks added to the app.
Normally all callbacks are fired when the associated outputs are first
added to the page. You can disable this for individual callbacks by
setting ``prevent_initial_call`` in their definitions, or set it
``True`` here in which case you must explicitly set it ``False`` for
those callbacks you wish to have an initial call. This setting has no
effect on triggering callbacks when their inputs change later on.

:param show_undo_redo: Default ``False``, set to ``True`` to enable undo
and redo buttons for stepping through the history of the app state.
:type show_undo_redo: boolean
Expand DownExpand Up@@ -241,6 +250,7 @@ def __init__(
external_scripts=None,
external_stylesheets=None,
suppress_callback_exceptions=None,
prevent_initial_callbacks=False,
show_undo_redo=False,
plugins=None,
**obsolete
Expand DownExpand Up@@ -288,6 +298,7 @@ def __init__(
suppress_callback_exceptions=get_combined_config(
"suppress_callback_exceptions", suppress_callback_exceptions, False
),
prevent_initial_callbacks=prevent_initial_callbacks,
show_undo_redo=show_undo_redo,
)
self.config.set_read_only(
Expand DownExpand Up@@ -813,14 +824,18 @@ def interpolate_index(self, **kwargs):
def dependencies(self):
return flask.jsonify(self._callback_list)

def _insert_callback(self, output, inputs, state):
def _insert_callback(self, output, inputs, state, prevent_initial_call):
if prevent_initial_call is None:
prevent_initial_call = self.config.prevent_initial_callbacks

_validate.validate_callback(output, inputs, state)
callback_id = create_callback_id(output)
callback_spec = {
"output": callback_id,
"inputs": [c.to_dict() for c in inputs],
"state": [c.to_dict() for c in state],
"clientside_function": None,
"prevent_initial_call": prevent_initial_call,
}
self.callback_map[callback_id] = {
"inputs": callback_spec["inputs"],
Expand All@@ -830,7 +845,9 @@ def _insert_callback(self, output, inputs, state):

return callback_id

def clientside_callback(self, clientside_function, output, inputs, state=()):
def clientside_callback(
self, clientside_function, output, inputs, state=(), prevent_initial_call=None
):
"""Create a callback that updates the output by calling a clientside
(JavaScript) function instead of a Python function.

Expand DownExpand Up@@ -890,8 +907,12 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
Input('another-input', 'value')]
)
```

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
self._insert_callback(output, inputs, state)
self._insert_callback(output, inputs, state, prevent_initial_call)

# If JS source is explicitly given, create a namespace and function
# name, then inject the code.
Expand DownExpand Up@@ -922,8 +943,19 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
"function_name": function_name,
}

def callback(self, output, inputs, state=()):
callback_id = self._insert_callback(output, inputs, state)
def callback(self, output, inputs, state=(), prevent_initial_call=None):
"""
Normally used as a decorator, `@app.callback` provides a server-side
callback relating the values of one or more `output` items to one or
more `input` items which will trigger the callback when they change,
and optionally `state` items which provide additional information but
do not trigger the callback directly.

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
callback_id = self._insert_callback(output, inputs, state, prevent_initial_call)
multi = isinstance(output, (list, tuple))

def wrap_func(func):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [UNRELEASED]
### Added
- [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior.
- [#1201](https://github.com/plotly/dash/pull/1201) New attribute `app.validation_layout` allows you to create a multi-page app without `suppress_callback_exceptions=True` or layout function tricks. Set this to a component layout containing the superset of all IDs on all pages in your app.
- [#1078](https://github.com/plotly/dash/pull/1078) Permit usage of arbitrary file extensions for assets within component libraries

Expand Down
15 changes: 10 additions & 5 deletions dash-renderer/src/actions/dependencies.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -1238,6 +1238,9 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
) {
// This callback should trigger even with no changedProps,
// since the props that changed no longer exist.
// We're kind of abusing the `initialCall` flag here, it's
// more like a "final call" for the removed inputs, but
// this case is not subject to `prevent_initial_call`.
if (flatten(cb.getOutputs(newPaths)).length) {
cb.initialCall = true;
cb.changedPropIds = {};
Expand All@@ -1255,10 +1258,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
const cb = getCallbackByOutput(graphs, paths, id, property);
if (cb) {
// callbacks found in the layout by output should always run
// unless specifically requested not to.
// ie this is the initial call of this callback even if it's
// not the page initialization but just a new layout chunk
cb.initialCall = true;
addCallback(cb);
if (!cb.callback.prevent_initial_call) {
cb.initialCall = true;
addCallback(cb);
}
}
}
}
Expand All@@ -1270,14 +1276,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
if (chunkPath) {
handleThisCallback = cb => {
if (
all(
!all(
startsWith(chunkPath),
pluck('path', flatten(cb.getOutputs(paths)))
)
) {
cb.changedPropIds = {};

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I'm not sure what I was thinking here with clearing out changedPropIds, but in this case (we've found a callback by its inputs due to a layout change, and just determined that all its outputs are in the same layout chunk) we know the callback is already included in the queue via its outputs so the best thing to do is simply not re-include it at all. This extra addCallback was harmless before prevent_initial_call, just a slight bit of extra work; but now that we have prevent_initial_call it's possible this callback would not be added based on its outputs.

If this condition isn't met, there are outputs outside the new layout chunk and it's not in fact an initial call anyway - so it's appropriate to add the callback and that's what the new logic does.

maybeAddCallback(cb);
}
maybeAddCallback(cb);
};
}
for (const property in inIdCallbacks) {
Expand Down
42 changes: 37 additions & 5 deletions dash/dash.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -211,6 +211,15 @@ class Dash(object):
env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
:type suppress_callback_exceptions: boolean

:param prevent_initial_callbacks: Default ``False``: Sets the default value
of ``prevent_initial_call`` for all callbacks added to the app.
Normally all callbacks are fired when the associated outputs are first
added to the page. You can disable this for individual callbacks by
setting ``prevent_initial_call`` in their definitions, or set it
``True`` here in which case you must explicitly set it ``False`` for
those callbacks you wish to have an initial call. This setting has no
effect on triggering callbacks when their inputs change later on.

:param show_undo_redo: Default ``False``, set to ``True`` to enable undo
and redo buttons for stepping through the history of the app state.
:type show_undo_redo: boolean
Expand DownExpand Up@@ -241,6 +250,7 @@ def __init__(
external_scripts=None,
external_stylesheets=None,
suppress_callback_exceptions=None,
prevent_initial_callbacks=False,
show_undo_redo=False,
plugins=None,
**obsolete
Expand DownExpand Up@@ -288,6 +298,7 @@ def __init__(
suppress_callback_exceptions=get_combined_config(
"suppress_callback_exceptions", suppress_callback_exceptions, False
),
prevent_initial_callbacks=prevent_initial_callbacks,
show_undo_redo=show_undo_redo,
)
self.config.set_read_only(
Expand DownExpand Up@@ -813,14 +824,18 @@ def interpolate_index(self, **kwargs):
def dependencies(self):
return flask.jsonify(self._callback_list)

def _insert_callback(self, output, inputs, state):
def _insert_callback(self, output, inputs, state, prevent_initial_call):
if prevent_initial_call is None:
prevent_initial_call = self.config.prevent_initial_callbacks

_validate.validate_callback(output, inputs, state)
callback_id = create_callback_id(output)
callback_spec = {
"output": callback_id,
"inputs": [c.to_dict() for c in inputs],
"state": [c.to_dict() for c in state],
"clientside_function": None,
"prevent_initial_call": prevent_initial_call,
}
self.callback_map[callback_id] = {
"inputs": callback_spec["inputs"],
Expand All@@ -830,7 +845,9 @@ def _insert_callback(self, output, inputs, state):

return callback_id

def clientside_callback(self, clientside_function, output, inputs, state=()):
def clientside_callback(
self, clientside_function, output, inputs, state=(), prevent_initial_call=None
):
"""Create a callback that updates the output by calling a clientside
(JavaScript) function instead of a Python function.

Expand DownExpand Up@@ -890,8 +907,12 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
Input('another-input', 'value')]
)
```

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
self._insert_callback(output, inputs, state)
self._insert_callback(output, inputs, state, prevent_initial_call)

# If JS source is explicitly given, create a namespace and function
# name, then inject the code.
Expand DownExpand Up@@ -922,8 +943,19 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
"function_name": function_name,
}

def callback(self, output, inputs, state=()):
callback_id = self._insert_callback(output, inputs, state)
def callback(self, output, inputs, state=(), prevent_initial_call=None):
"""
Normally used as a decorator, `@app.callback` provides a server-side
callback relating the values of one or more `output` items to one or
more `input` items which will trigger the callback when they change,
and optionally `state` items which provide additional information but
do not trigger the callback directly.

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
callback_id = self._insert_callback(output, inputs, state, prevent_initial_call)
multi = isinstance(output, (list, tuple))

def wrap_func(func):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [UNRELEASED]
### Added
- [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior.
- [#1201](https://github.com/plotly/dash/pull/1201) New attribute `app.validation_layout` allows you to create a multi-page app without `suppress_callback_exceptions=True` or layout function tricks. Set this to a component layout containing the superset of all IDs on all pages in your app.
- [#1078](https://github.com/plotly/dash/pull/1078) Permit usage of arbitrary file extensions for assets within component libraries

Expand Down
15 changes: 10 additions & 5 deletions dash-renderer/src/actions/dependencies.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -1238,6 +1238,9 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
) {
// This callback should trigger even with no changedProps,
// since the props that changed no longer exist.
// We're kind of abusing the `initialCall` flag here, it's
// more like a "final call" for the removed inputs, but
// this case is not subject to `prevent_initial_call`.
if (flatten(cb.getOutputs(newPaths)).length) {
cb.initialCall = true;
cb.changedPropIds = {};
Expand All@@ -1255,10 +1258,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
const cb = getCallbackByOutput(graphs, paths, id, property);
if (cb) {
// callbacks found in the layout by output should always run
// unless specifically requested not to.
// ie this is the initial call of this callback even if it's
// not the page initialization but just a new layout chunk
cb.initialCall = true;
addCallback(cb);
if (!cb.callback.prevent_initial_call) {
cb.initialCall = true;
addCallback(cb);
}
}
}
}
Expand All@@ -1270,14 +1276,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
if (chunkPath) {
handleThisCallback = cb => {
if (
all(
!all(
startsWith(chunkPath),
pluck('path', flatten(cb.getOutputs(paths)))
)
) {
cb.changedPropIds = {};

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I'm not sure what I was thinking here with clearing out changedPropIds, but in this case (we've found a callback by its inputs due to a layout change, and just determined that all its outputs are in the same layout chunk) we know the callback is already included in the queue via its outputs so the best thing to do is simply not re-include it at all. This extra addCallback was harmless before prevent_initial_call, just a slight bit of extra work; but now that we have prevent_initial_call it's possible this callback would not be added based on its outputs.

If this condition isn't met, there are outputs outside the new layout chunk and it's not in fact an initial call anyway - so it's appropriate to add the callback and that's what the new logic does.

maybeAddCallback(cb);
}
maybeAddCallback(cb);
};
}
for (const property in inIdCallbacks) {
Expand Down
42 changes: 37 additions & 5 deletions dash/dash.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -211,6 +211,15 @@ class Dash(object):
env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
:type suppress_callback_exceptions: boolean

:param prevent_initial_callbacks: Default ``False``: Sets the default value
of ``prevent_initial_call`` for all callbacks added to the app.
Normally all callbacks are fired when the associated outputs are first
added to the page. You can disable this for individual callbacks by
setting ``prevent_initial_call`` in their definitions, or set it
``True`` here in which case you must explicitly set it ``False`` for
those callbacks you wish to have an initial call. This setting has no
effect on triggering callbacks when their inputs change later on.

:param show_undo_redo: Default ``False``, set to ``True`` to enable undo
and redo buttons for stepping through the history of the app state.
:type show_undo_redo: boolean
Expand DownExpand Up@@ -241,6 +250,7 @@ def __init__(
external_scripts=None,
external_stylesheets=None,
suppress_callback_exceptions=None,
prevent_initial_callbacks=False,
show_undo_redo=False,
plugins=None,
**obsolete
Expand DownExpand Up@@ -288,6 +298,7 @@ def __init__(
suppress_callback_exceptions=get_combined_config(
"suppress_callback_exceptions", suppress_callback_exceptions, False
),
prevent_initial_callbacks=prevent_initial_callbacks,
show_undo_redo=show_undo_redo,
)
self.config.set_read_only(
Expand DownExpand Up@@ -813,14 +824,18 @@ def interpolate_index(self, **kwargs):
def dependencies(self):
return flask.jsonify(self._callback_list)

def _insert_callback(self, output, inputs, state):
def _insert_callback(self, output, inputs, state, prevent_initial_call):
if prevent_initial_call is None:
prevent_initial_call = self.config.prevent_initial_callbacks

_validate.validate_callback(output, inputs, state)
callback_id = create_callback_id(output)
callback_spec = {
"output": callback_id,
"inputs": [c.to_dict() for c in inputs],
"state": [c.to_dict() for c in state],
"clientside_function": None,
"prevent_initial_call": prevent_initial_call,
}
self.callback_map[callback_id] = {
"inputs": callback_spec["inputs"],
Expand All@@ -830,7 +845,9 @@ def _insert_callback(self, output, inputs, state):

return callback_id

def clientside_callback(self, clientside_function, output, inputs, state=()):
def clientside_callback(
self, clientside_function, output, inputs, state=(), prevent_initial_call=None
):
"""Create a callback that updates the output by calling a clientside
(JavaScript) function instead of a Python function.

Expand DownExpand Up@@ -890,8 +907,12 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
Input('another-input', 'value')]
)
```

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
self._insert_callback(output, inputs, state)
self._insert_callback(output, inputs, state, prevent_initial_call)

# If JS source is explicitly given, create a namespace and function
# name, then inject the code.
Expand DownExpand Up@@ -922,8 +943,19 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
"function_name": function_name,
}

def callback(self, output, inputs, state=()):
callback_id = self._insert_callback(output, inputs, state)
def callback(self, output, inputs, state=(), prevent_initial_call=None):
"""
Normally used as a decorator, `@app.callback` provides a server-side
callback relating the values of one or more `output` items to one or
more `input` items which will trigger the callback when they change,
and optionally `state` items which provide additional information but
do not trigger the callback directly.

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
callback_id = self._insert_callback(output, inputs, state, prevent_initial_call)
multi = isinstance(output, (list, tuple))

def wrap_func(func):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [UNRELEASED]
### Added
- [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior.
- [#1201](https://github.com/plotly/dash/pull/1201) New attribute `app.validation_layout` allows you to create a multi-page app without `suppress_callback_exceptions=True` or layout function tricks. Set this to a component layout containing the superset of all IDs on all pages in your app.
- [#1078](https://github.com/plotly/dash/pull/1078) Permit usage of arbitrary file extensions for assets within component libraries

Expand Down
15 changes: 10 additions & 5 deletions dash-renderer/src/actions/dependencies.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -1238,6 +1238,9 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
) {
// This callback should trigger even with no changedProps,
// since the props that changed no longer exist.
// We're kind of abusing the `initialCall` flag here, it's
// more like a "final call" for the removed inputs, but
// this case is not subject to `prevent_initial_call`.
if (flatten(cb.getOutputs(newPaths)).length) {
cb.initialCall = true;
cb.changedPropIds = {};
Expand All@@ -1255,10 +1258,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
const cb = getCallbackByOutput(graphs, paths, id, property);
if (cb) {
// callbacks found in the layout by output should always run
// unless specifically requested not to.
// ie this is the initial call of this callback even if it's
// not the page initialization but just a new layout chunk
cb.initialCall = true;
addCallback(cb);
if (!cb.callback.prevent_initial_call) {
cb.initialCall = true;
addCallback(cb);
}
}
}
}
Expand All@@ -1270,14 +1276,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
if (chunkPath) {
handleThisCallback = cb => {
if (
all(
!all(
startsWith(chunkPath),
pluck('path', flatten(cb.getOutputs(paths)))
)
) {
cb.changedPropIds = {};

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

I'm not sure what I was thinking here with clearing out changedPropIds, but in this case (we've found a callback by its inputs due to a layout change, and just determined that all its outputs are in the same layout chunk) we know the callback is already included in the queue via its outputs so the best thing to do is simply not re-include it at all. This extra addCallback was harmless before prevent_initial_call, just a slight bit of extra work; but now that we have prevent_initial_call it's possible this callback would not be added based on its outputs.

If this condition isn't met, there are outputs outside the new layout chunk and it's not in fact an initial call anyway - so it's appropriate to add the callback and that's what the new logic does.

maybeAddCallback(cb);
}
maybeAddCallback(cb);
};
}
for (const property in inIdCallbacks) {
Expand Down
42 changes: 37 additions & 5 deletions dash/dash.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -211,6 +211,15 @@ class Dash(object):
env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
:type suppress_callback_exceptions: boolean

:param prevent_initial_callbacks: Default ``False``: Sets the default value
of ``prevent_initial_call`` for all callbacks added to the app.
Normally all callbacks are fired when the associated outputs are first
added to the page. You can disable this for individual callbacks by
setting ``prevent_initial_call`` in their definitions, or set it
``True`` here in which case you must explicitly set it ``False`` for
those callbacks you wish to have an initial call. This setting has no
effect on triggering callbacks when their inputs change later on.

:param show_undo_redo: Default ``False``, set to ``True`` to enable undo
and redo buttons for stepping through the history of the app state.
:type show_undo_redo: boolean
Expand DownExpand Up@@ -241,6 +250,7 @@ def __init__(
external_scripts=None,
external_stylesheets=None,
suppress_callback_exceptions=None,
prevent_initial_callbacks=False,
show_undo_redo=False,
plugins=None,
**obsolete
Expand DownExpand Up@@ -288,6 +298,7 @@ def __init__(
suppress_callback_exceptions=get_combined_config(
"suppress_callback_exceptions", suppress_callback_exceptions, False
),
prevent_initial_callbacks=prevent_initial_callbacks,
show_undo_redo=show_undo_redo,
)
self.config.set_read_only(
Expand DownExpand Up@@ -813,14 +824,18 @@ def interpolate_index(self, **kwargs):
def dependencies(self):
return flask.jsonify(self._callback_list)

def _insert_callback(self, output, inputs, state):
def _insert_callback(self, output, inputs, state, prevent_initial_call):
if prevent_initial_call is None:
prevent_initial_call = self.config.prevent_initial_callbacks

_validate.validate_callback(output, inputs, state)
callback_id = create_callback_id(output)
callback_spec = {
"output": callback_id,
"inputs": [c.to_dict() for c in inputs],
"state": [c.to_dict() for c in state],
"clientside_function": None,
"prevent_initial_call": prevent_initial_call,
}
self.callback_map[callback_id] = {
"inputs": callback_spec["inputs"],
Expand All@@ -830,7 +845,9 @@ def _insert_callback(self, output, inputs, state):

return callback_id

def clientside_callback(self, clientside_function, output, inputs, state=()):
def clientside_callback(
self, clientside_function, output, inputs, state=(), prevent_initial_call=None
):
"""Create a callback that updates the output by calling a clientside
(JavaScript) function instead of a Python function.

Expand DownExpand Up@@ -890,8 +907,12 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
Input('another-input', 'value')]
)
```

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
self._insert_callback(output, inputs, state)
self._insert_callback(output, inputs, state, prevent_initial_call)

# If JS source is explicitly given, create a namespace and function
# name, then inject the code.
Expand DownExpand Up@@ -922,8 +943,19 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
"function_name": function_name,
}

def callback(self, output, inputs, state=()):
callback_id = self._insert_callback(output, inputs, state)
def callback(self, output, inputs, state=(), prevent_initial_call=None):
"""
Normally used as a decorator, `@app.callback` provides a server-side
callback relating the values of one or more `output` items to one or
more `input` items which will trigger the callback when they change,
and optionally `state` items which provide additional information but
do not trigger the callback directly.

The last, optional argument `prevent_initial_call` causes the callback
not to fire when its outputs are first added to the page. Defaults to
`False` unless `prevent_initial_callbacks=True` at the app level.
"""
callback_id = self._insert_callback(output, inputs, state, prevent_initial_call)
multi = isinstance(output, (list, tuple))

def wrap_func(func):
Expand Down
Loading