0

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?

  • 1
    I think you need to create a function that initializes on document or window load time. The JS has probably already run before element `toggle-button` has been loaded, hence 'undefined error'... – Rene van der Lende Jul 11 '21 at 16:29
  • Does this answer your question? [How to make JavaScript execute after page load?](https://stackoverflow.com/questions/807878/how-to-make-javascript-execute-after-page-load) – 5eb Jul 11 '21 at 16:54
  • I think it does, but I don't know how to implement that into my JS script due to my JS knowledge limitations – Sebastian Meckovski Jul 11 '21 at 17:19
  • You can add an event listener for the `load` event. For an example see the second example of the second answer of the linked post ([link](https://stackoverflow.com/a/36096571/9098350)). You can put the code in your question inside the handler function for the load event and that will probably work. – 5eb Jul 11 '21 at 17:24
  • I don't think you need javascript for this btw. You could also probably do it with a callback in Dash. – 5eb Jul 11 '21 at 17:27
  • Yeah I was researching how to do it with dash, but couldn't find it. Similar event is described [here](https://community.plotly.com/t/append-script-when-dash-components-are-loaded/8023/3) but dash.dependencies Event module is no longer available on Dash – Sebastian Meckovski Jul 11 '21 at 17:32
  • I changed my JavaScript to this but it still doesn't work. I'm probably doing something wrong `var toggleButton = document.getElementsByClassName('toggle-button')[0] var navBarLinks = document.getElementsByClassName('navbar-links')[0] window.onload = function toggleFunction() { navBarLinks.classList.toggle('active') } toggleButton.addEventListener('click', toggleFunction)` – Sebastian Meckovski Jul 11 '21 at 17:33

3 Answers3

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()
emher
  • 5,634
  • 1
  • 21
  • 32
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.

5eb
  • 14,798
  • 5
  • 21
  • 65
  • I just implemented python solution, which is great help, thank you so much! I tried JS solution first, but It didn't work. That still solves my problem so you don't worry about it – Sebastian Meckovski Jul 11 '21 at 18:36
0

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
        });
    })
}
Jordan Hall
  • 192
  • 1
  • 11