Append script when dash components are loaded

Hi there.

I want to append a javascript file which makes some changes in the DOM generated by Dash. I get a not encountered error if I simply use app.scripts.append_script. I’ve read this in SO and this in this forum.

I have finally opted for using a setInterval workaround, but Itd be nice if Dash provided a way to assign callbacks when the DOM is effectively loaded.

Just curious, what are you trying to do here?

I’m trying to make a hide content / show content button. I’m just using raw javascript to assign a toggle hide/show function to the button. The javscript code is here (WIP): https://github.com/Grasia/WikiChron/blob/master/js/app.js

Maybe there is a better way. I didn’t want to break my head in this.

If you just need to hide an element, you could do that without javascript like this:

from dash import Dash
from dash.dependencies import Input, State, Output, Event                                      
import dash_core_components as dcc
import dash_html_components as html

app = Dash()
app.layout = html.Div([
    html.Div("Some content", id='content', style={'display': 'none'}),
    html.Button('Toggle Hidden', id='toggle-hide'),
])


@app.callback(Output('content', 'style'),
              [],
              [State('content', 'style')],
              [Event('toggle-hide', 'click')])                                                                                                                                                 
def callback(style):
    if style is None or 'display' not in style:
        style = {'display': 'none'}
    else:
        style['display'] = 'block' if style['display'] == 'none' else 'none'
    return style


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

@nedned, it’d be probably much verbose and I’d need at least two callbacks since I also want to change the style of the button, but that might work.
Still looking forward for a hook where we can set javascript code that will be triggered when the actual Dash react components are loaded.
Thank you.

I have run into similar issues and have also used a setTimeout/setInterval hack. My dash app is in an iframe and needs to send information back to the parent domain. I have managed to get this to work but it isn’t elegant. The code below is not complete, it only contains the pieces necessary to communicate back to the parent domain. The comments should help you to follow what is going on.

def write_to_data_uri(s):
    """
    Writes to a uri.
    Use this function to embed javascript into the dash app.
    Adapted from the suggestion by user 'mccalluc' found here:
    https://community.plot.ly/t/problem-of-linking-local-javascript-file/6955/2
    """
    uri = (
        ('data:;base64,').encode('utf8') +
        urlsafe_b64encode(s.encode('utf8'))
    ).decode("utf-8", "strict")
    return uri

# Get parentDomain
# Define JS func "sendMessageToParent"
app.scripts.append_script({
    'external_url': write_to_data_uri("""
var parentDomain = '';

window.addEventListener('message',function(event) {
   console.log('message received from parent:  ' + event.data,event);
   parentDomain = event.data;
},false);

function sendMessageToParent(s) {
   console.log("Sending message to parent at ("+parentDomain+") s=" + s);
   parent.postMessage(s,parentDomain);
}
    """)})

# Hidden div to store the message/string which will be sent to the parent domain
# using 'title' to store the message/string because it is easy to get javascript
# funciton to retrieve the value of the title
html.Div(
    id='msg_for_parent',
    title='',
    style={'display': 'none'}
    ),

# Button to execute javascript. Does nothing in the dash world
html.Button(
    'Click Me',
    id='exec_js',
),

# call back to update the title of the hidden div, i.e. update the message for
# the parent domain
# using 'url' as an input but any dash component will do.
@app.callback(
    dash.dependencies.Output('msg_for_parent', 'title'),
    [dash.dependencies.Input('url', 'pathname')] 
    )
def update_msg_for_parent(pathname):
    msg = "hello from %s" % pathname
    return msg

# Javascript to message the parent. Or, in more detail, to send the 'title' of 
# the div 'msg_for_parent' to the parent domain.
# 
# setTimeout is a dirty, dirty hack. Forces the page to wait two seconds before
# appending this script, by which time "msg_for_parent" and "exec_js" are 
# defined. Without this, there is an error to the effect of "cannot set property
# onclick of null"
app.scripts.append_script({
    'external_url': write_to_data_uri("""
setTimeout(function(){
   $(document).ready(function(){
        document.getElementById("exec_js").onclick = function(){
            var msg = document.getElementById("msg_for_parent").title;
            sendMessageToParent(msg);
        };
    });
}, 2000);
    """)})
1 Like

I have coded an elegant solution for this!! :tada: :tada:

Now, you can use a component called that is available in the grasia-dash-components library (pip install grasia-dash-components) and set the src attribute to the url of the external javascript code you want to append. Below I show you a small example where I import the jQuery library:

import dash
import dash_html_components as html 
import grasia-dash-components as gdc

app = dash.Dash()
app.layout  = html.P('Hello world!')
app.layout += gdc.Import(src="https://code.jquery.com/jquery-3.3.1.min.js")

The Import component will load only when all the dash app is loaded and it isn’t affected by the app.scripts.config.serve_locally setting.
Similarly, you can import local .js files as long as you are serving them from a local web server (You might use the Flask server that Dash uses behind the hoods. Ping me if you need more help on this.)

Technical explanation:
The solution consists in using a react component called that which belongs to an external dash library, and, therefore is loaded only when all the React components for the Dash app are loaded.
This component renders as a html tag with the defer attribute set to true.
The source code is available in Github.

Please, try it and let me know any issues you face.

3 Likes

Your library appears to be what I’m looking for! But when I try to install it with pip I get the following:

Complete output from command python setup.py egg_info:
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "C:\Users\Work\AppData\Local\Temp\pip-install-gv_jvz0l\grasia-dash-co                                                                                                                                  mponents\setup.py", line 5, in <module>
    with open('long_description.md') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'long_description.md                                                                                                                                  '

----------------------------------------

Command “python setup.py egg_info” failed with error code 1 in C:\Users\Work\App Data\Local\Temp\pip-install-gv_jvz0l\grasia-dash-components\

My mistake, the distribution package was missing a metadata file.
Fixed in 0.3.4.
Thanks for reporting :+1:

Hi Abeserra,

I tried to implement your above code, but I get the below error message:
TypeError: unsupported operand type(s) for +=: ‘P’ and ‘Import’

I have a callback in my app that produces a table. I suppose I need to load jquery after this callback, because it appears without any style when produced by callback, but gets styled when inserted by hand into the html layout.

I wanted to try the above approach but am already failing at the beginning due to the TypeError.

Thanks in advance.

By what the error message says, looks like you are using += in a context where it isn’t supported. Try to not use that and test it again.

I got this problem

  File "dashjs.py", line 3
import grasia-dash-components as gdc
             ^

SyntaxError: invalid syntax

I did this in the directory as well

pip install grasia-dash-components

ok that was incorrect import statement

it suppose to be like this

import grasia_dash_components

1 Like

Ups, such a stupid mistake from my side!
Yeah, as @cameron54 pointed out, the correct statement is import grasia_dash_components as gdc.

In fact, the app.layout assignment is also wrong. Here there is a working version that I have tested of a similar example:

import dash
import dash_html_components as html
import grasia_dash_components as gdc

app = dash.Dash()
app.layout = html.Div([
    html.P('Hello world!', id='hello-world'),
    gdc.Import(src="https://code.jquery.com/jquery-3.3.1.min.js")
])

app.run_server()

And you can check that the jQuery library has been loaded indeed, running this command in the browser console: console.log(jQuery.fn.jquery)

Thank you @cameron54!

Hey Abeserra. I’ve tried using the library to import a javascript file from a server running on localhost and it doesn’t work. I would love to contact you to maybe see what happened. Thank you!

Hi @itamag. Sorry for the late reply. I’ve been very busy with other stuff within the past weeks.
So, have you made sure that you can load that js outside Dash? In other words, can you go to localhost/path/to/file.js and you can see it?

New in dash 0.22.0

Including custom CSS or JavaScript in your Dash apps is simple. Just create a folder named assets in the root of your app directory and include your CSS and JavaScript files in that folder.

See the official instructions below:

https://dash.plot.ly/external-resources

Yep, that works since Dash 0.22.0.

However, keep in mind that with that you can’t choose whether to load or not some js depending on your code or when to load it (i.e. you want to load some js only when a callback is triggered); all the js code located in assets will get loaded at once along with the Dash app.

In case you want to do load the js code in a more discretionary way you might want to use our auxiliar component: gdc.Import()

1 Like

Since this still shows up as part of the google search for this feature:

I’ve had trouble upgrading to 1.0.0 since my code relied on dash-grasia-components.Import, but it looks like the current version of it is not compatible with Dash 1.0.0 (see related github issue).

I’ve decided to fix this by making a standalone component that pulls out the Import function from dash-grasia-components. I’ve published it to npm as well as pypi so anyone can use pip to install it:

pip install dash_defer_js_import

Alternatively, it can be built locally from source code which is hosted here: https://github.com/zouhairm/dash_defer_js_import

It can then be used as follows:

import dash_defer_js_import as dji

# Dash server init and layout setup
# ...

app.layout = html.Article(dji.Import(src="./path/to/script.js"))

Hope this helps others if they run into this issue.