Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).


## UNRELEASED

## Added

- [#2630](https://github.com/plotly/dash/pull/2630) New layout hooks in the renderer


## [2.12.1] - 2023-08-16

## Fixed
Expand Down
16 changes: 15 additions & 1 deletion dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import secrets
import string
from html import escape
from functools import wraps
from functools import wraps, reduce
from typing import Union
from dash.types import RendererHooks

logger = logging.getLogger()

Expand Down Expand Up @@ -267,3 +269,15 @@ def coerce_to_list(obj):

def clean_property_name(name: str):
return name.split("@")[0]


def hooks_to_js_object(hooks: Union[RendererHooks, None]) -> str:
if hooks is None:
return ""
hook_str = reduce(
lambda reduced, hook: f"{reduced}{hook[0]}: {hook[1]},",
hooks.items(),
"",
)

return f"{{{hook_str}}}"
9 changes: 9 additions & 0 deletions dash/dash-renderer/src/APIController.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,21 @@ function storeEffect(props, events, setErrorLoading) {
dispatch,
error,
graphs,
hooks,
layout,
layoutRequest
} = props;

if (isEmpty(layoutRequest)) {
if (typeof hooks.layout_pre === 'function') {
hooks.layout_pre();
}
dispatch(apiThunk('_dash-layout', 'GET', 'layoutRequest'));
} else if (layoutRequest.status === STATUS.OK) {
if (isEmpty(layout)) {
if (typeof hooks.layout_post === 'function') {
hooks.layout_post(layoutRequest.content);
}
const finalLayout = applyPersistence(
layoutRequest.content,
dispatch
Expand Down Expand Up @@ -190,6 +197,7 @@ UnconnectedContainer.propTypes = {
dispatch: PropTypes.func,
dependenciesRequest: PropTypes.object,
graphs: PropTypes.object,
hooks: PropTypes.object,
layoutRequest: PropTypes.object,
layout: PropTypes.object,
loadingMap: PropTypes.any,
Expand All @@ -203,6 +211,7 @@ const Container = connect(
state => ({
appLifecycle: state.appLifecycle,
dependenciesRequest: state.dependenciesRequest,
hooks: state.hooks,
layoutRequest: state.layoutRequest,
layout: state.layout,
loadingMap: state.loadingMap,
Expand Down
2 changes: 2 additions & 0 deletions dash/dash-renderer/src/AppContainer.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class UnconnectedAppContainer extends React.Component {
constructor(props) {
super(props);
if (
props.hooks.layout_pre !== null ||
props.hooks.layout_post !== null ||
props.hooks.request_pre !== null ||
props.hooks.request_post !== null ||
props.hooks.callback_resolved !== null ||
Expand Down
4 changes: 4 additions & 0 deletions dash/dash-renderer/src/AppProvider.react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const AppProvider = ({hooks}: any) => {

AppProvider.propTypes = {
hooks: PropTypes.shape({
layout_pre: PropTypes.func,
layout_post: PropTypes.func,
request_pre: PropTypes.func,
request_post: PropTypes.func,
callback_resolved: PropTypes.func,
Expand All @@ -25,6 +27,8 @@ AppProvider.propTypes = {

AppProvider.defaultProps = {
hooks: {
layout_pre: null,
layout_post: null,
request_pre: null,
request_post: null,
callback_resolved: null,
Expand Down
2 changes: 2 additions & 0 deletions dash/dash-renderer/src/reducers/hooks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const customHooks = (
state = {
layout_pre: null,
layout_post: null,
request_pre: null,
request_post: null,
callback_resolved: null,
Expand Down
11 changes: 5 additions & 6 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import base64
import traceback
from urllib.parse import urlparse
from typing import Union

import flask

Expand Down Expand Up @@ -52,6 +53,7 @@
to_json,
convert_to_AttributeDict,
gen_salt,
hooks_to_js_object,
)
from . import _callback
from . import _get_paths
Expand All @@ -70,6 +72,7 @@
_import_layouts_from_pages,
)
from ._jupyter import jupyter_dash, JupyterDisplayMode
from .types import RendererHooks

# Add explicit mapping for map files
mimetypes.add_type("application/json", ".map", True)
Expand Down Expand Up @@ -134,7 +137,6 @@


def _get_traceback(secret, error: Exception):

try:
# pylint: disable=import-outside-toplevel
from werkzeug.debug import tbtools
Expand Down Expand Up @@ -373,6 +375,7 @@ def __init__( # pylint: disable=too-many-statements
long_callback_manager=None,
background_callback_manager=None,
add_log_handler=True,
hooks: Union[RendererHooks, None] = None,
**obsolete,
):
_validate.check_obsolete(obsolete)
Expand Down Expand Up @@ -466,7 +469,7 @@ def __init__( # pylint: disable=too-many-statements
self._favicon = None

# default renderer string
self.renderer = "var renderer = new DashRenderer();"
self.renderer = f"var renderer = new DashRenderer({hooks_to_js_object(hooks)});"

# static files from the packages
self.css = Css(serve_locally)
Expand Down Expand Up @@ -1301,7 +1304,6 @@ def _setup_server(self):

# Copy over global callback data structures assigned with `dash.callback`
for k in list(_callback.GLOBAL_CALLBACK_MAP):

if k in self.callback_map:
raise DuplicateCallback(
f"The callback `{k}` provided with `dash.callback` was already "
Expand All @@ -1328,7 +1330,6 @@ def _setup_server(self):

if cancels:
for cancel_input, manager in cancels.items():

# pylint: disable=cell-var-from-loop
@self.callback(
Output(cancel_input.component_id, "id"),
Expand Down Expand Up @@ -1719,7 +1720,6 @@ def enable_dev_tools(
_reload.watch_thread.start()

if debug:

if jupyter_dash.active:
jupyter_dash.configure_callback_exception_handling(
self, dev_tools.prune_errors
Expand Down Expand Up @@ -1753,7 +1753,6 @@ def _after_request(response):
dash_total["dur"] = round((time.time() - dash_total["dur"]) * 1000)

for name, info in timing_information.items():

value = name
if info.get("desc") is not None:
value += f';desc="{info["desc"]}"'
Expand Down
10 changes: 10 additions & 0 deletions dash/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing_extensions import TypedDict, NotRequired


class RendererHooks(TypedDict):
layout_pre: NotRequired[str]
layout_post: NotRequired[str]
request_pre: NotRequired[str]
request_post: NotRequired[str]
callback_resolved: NotRequired[str]
request_refresh_jwt: NotRequired[str]
31 changes: 29 additions & 2 deletions tests/integration/renderer/test_request_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest

from dash import Dash, Output, Input, html, dcc
from dash.types import RendererHooks
from werkzeug.exceptions import HTTPException


Expand Down Expand Up @@ -115,7 +116,6 @@ def update_output(value):


def test_rdrh002_with_custom_renderer_interpolated(dash_duo):

renderer = """
<script id="_dash-renderer" type="application/javascript">
console.log('firing up a custom renderer!')
Expand Down Expand Up @@ -198,7 +198,6 @@ def update_output(value):

@pytest.mark.parametrize("expiry_code", [401, 400])
def test_rdrh003_refresh_jwt(expiry_code, dash_duo):

app = Dash(__name__)

app.index_string = """<!DOCTYPE html>
Expand Down Expand Up @@ -295,3 +294,31 @@ def wrap(*args, **kwargs):
dash_duo.wait_for_text_to_equal("#output-token", "..")

assert len(dash_duo.get_logs()) == 2


def test_rdrh004_layout_hooks(dash_duo):
hooks: RendererHooks = {
"layout_pre": """
() => {
var layoutPre = document.createElement('div');
layoutPre.setAttribute('id', 'layout-pre');
layoutPre.innerHTML = 'layout_pre generated this text';
document.body.appendChild(layoutPre);
}
""",
"layout_post": """
(response) => {
response.props.children = "layout_post generated this text";
}
""",
}

app = Dash(__name__, hooks=hooks)
app.layout = html.Div(id="layout")

dash_duo.start_server(app)

dash_duo.wait_for_text_to_equal("#layout-pre", "layout_pre generated this text")
dash_duo.wait_for_text_to_equal("#layout", "layout_post generated this text")

assert dash_duo.get_logs() == []