'Adding JavaScript to my Plotly Dash app (Python)

I'm building a dashboard using Dash in Python. I have configured all the graphs nicely (it's running on the server here) and the next step is to create a responsive navbar and a footer. Currently looks like this:

enter image description here

And when I shrink the width, it looks like this:

enter image description here

I want to add functionality to this button so it would hide the three links on click. I'm trying to toggle the CSS 'active' attribute using JavaScript with this piece of code:

 var toggleButton = document.getElementsByClassName('toggle-button')[0]
 var navBarLinks = document.getElementsByClassName('navbar-links')[0]

 function toggleFunction() {
     navBarLinks.classList.toggle('active')
 }

 toggleButton.addEventListener('click', toggleFunction)

Basically, when the navbar-links class is active, I want it to be set as display: flex, and when it's not active I want it to be display: none

The HTML elements defined in Python screen are here:

    html.Nav([

    html.Div('Covid-19 global data Dashboard', className='dashboard-title'),

    html.A([html.Span(className='bar'),
            html.Span(className='bar'),
            html.Span(className='bar')],
           href='#', className='toggle-button'),

    html.Div(
        html.Ul([
            html.Li(html.A('Linked-In', href='#')),
            html.Li(html.A('Source Code', href='#')),
            html.Li(html.A('CSV Data', href='#'))
        ]),
        className='navbar-links'),

], className='navbar')

I didn't expect that there would be issues with accessing elements through JavaScript. After doing some research I found out that JavaScript when executes getElementsByClassName function the returned value is null. That is because the function is run before the page is rendered (as far as I understand). It gives me this error:

enter image description here

This project is getting quite big, so I don't know which parts should I include in this post, but I will share the git repository and the preview of the page. Is there an easy solution to it?



Solution 1:[1]

Dash callback solution (no Javascript):

import dash
import dash_html_components as html
from dash.dependencies import Output, Input, State

navbar_base_class = "navbar-links"

app = dash.Dash(__name__)

app.layout = html.Nav(
    [
        html.Div("Covid-19 global data Dashboard", className="dashboard-title"),
        html.A(
            id="toggle-button",
            children=[
                html.Span(className="bar"),
                html.Span(className="bar"),
                html.Span(className="bar"),
            ],
            href="#",
            className="toggle-button",
        ),
        html.Div(
            id="navbar-links",
            children=html.Ul(
                children=[
                    html.Li(html.A("Linked-In", href="#")),
                    html.Li(html.A("Source Code", href="#")),
                    html.Li(html.A("CSV Data", href="#")),
                ],
            ),
            className=navbar_base_class,
        ),
    ],
    className="navbar",
)


@app.callback(
    Output("navbar-links", "className"),
    Input("toggle-button", "n_clicks"),
    State("navbar-links", "className"),
    prevent_initial_call=True,
)
def callback(n_clicks, current_classes):
    if "active" in current_classes:
        return navbar_base_class
    return navbar_base_class + " active"


if __name__ == "__main__":
    app.run_server(debug=True)

The idea of the code above is to take the toggle-button click as Input and the current value of navbar-links as State. We can use this state to determine if we should add the active class or remove it. The new className value is returned in the callback.


Javascript solution:

window.addEventListener("load", function () {
  var toggleButton = document.getElementsByClassName("toggle-button")[0];
  var navBarLinks = document.getElementsByClassName("navbar-links")[0];

  function toggleFunction() {
    navBarLinks.classList.toggle("active");
  }

  toggleButton.addEventListener("click", toggleFunction);
});

The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets and images. This is in contrast to DOMContentLoaded, which is fired as soon as the page DOM has been loaded, without waiting for resources to finish loading.

https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event

DOMContentLoaded would be preferable to use, but it only works for me with load.

Solution 2:[2]

You can defer the execution of JavaScript code until after React has loaded via the DeferScript component from dash-extensions. Here is a small example,

import dash
import dash_html_components as html
from html import unescape
from dash_extensions import DeferScript


mxgraph = r'{"highlight":"#0000ff","nav":true,"resize":true,"toolbar":"zoom layers lightbox","edit":"_blank","xml":"<mxfile host=\"app.diagrams.net\" modified=\"2021-06-07T06:06:13.695Z\" agent=\"5.0 (Windows)\" etag=\"4lPJKNab0_B4ArwMh0-7\" version=\"14.7.6\"><diagram id=\"YgMnHLNxFGq_Sfquzsd6\" name=\"Page-1\">jZJNT4QwEIZ/DUcToOriVVw1JruJcjDxYho60iaFIaUs4K+3yJSPbDbZSzN95qPTdyZgadm/GF7LAwrQQRyKPmBPQRzvktidIxgmwB4IFEaJCUULyNQvEAyJtkpAswm0iNqqegtzrCrI7YZxY7Dbhv2g3r5a8wLOQJZzfU4/lbByoslduPBXUIX0L0cheUrugwk0kgvsVojtA5YaRDtZZZ+CHrXzukx5zxe8c2MGKntNgknk8bs8fsj3+KtuDhxP+HZDVU5ct/RhatYOXgGDbSVgLBIG7LGTykJW83z0dm7kjklbaneLnEnlwFjoL/YZzb93WwNYgjWDC6EEdkuC0cZEO7p3i/6RF1WutL8nxmnkxVx6UcUZJIy/LgP49622mO3/AA==</diagram></mxfile>"}'
app = dash.Dash(__name__)
app.layout = html.Div([
    html.Div(className='mxgraph', style={"maxWidth": "100%"}, **{'data-mxgraph': unescape(mxgraph)}),
    DeferScript(src='https://viewer.diagrams.net/js/viewer-static.min.js')
])

if __name__ == '__main__':
    app.run_server()

Solution 3:[3]

If you need a pure JS solution you need to use MutationObserver. I've wrote a little helper function we are currently using that did the trick. Another suggestion would be to change the mutation to an element on screen then fire an event to handle the rest

/**
 *
 * @param {string} id
 * @param {*} event
 * @param {(this: HTMLElement, ev: any) => any} callback
 * @param {boolean | AddEventListenerOptions} options
 */
function attachEventToDash(id, event, callback, options) {
    debugger;
    var observer = new MutationObserver(function (_mutations, obs) {
        var ele = document.getElementById(id);
        if (ele) {
            debugger;
            ele.addEventListener(event, callback, options)
            obs.disconnect();
        }
    });
    window.addEventListener('DOMContentLoaded', function () {
        observer.observe(document, {
            childList: true,
            subtree: true
        });
    })
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 emher
Solution 3 Jordan Hall