plotly/dash

Proposal for Offline CSS and JS and Customizable `index.html`

chriddyp opened this issue ยท 23 comments

Opening this issue to propose the officially endorsed and documented method for embedding custom, local CSS and JavaScript in Dash apps.

This development for this issue has been sponsored by an organization. Many thanks!
If your company or organization would like to sponsor development, please reach out.

Background: In Dash, HTML tags and higher-level components are embedded in the App by assigning the app.layout property to a nested hierarchy of Dash components (e.g. the components in the dash-core-components or the dash-html-components library). These components are serialized as JSON and then rendered client-side with React.js. On page load, Dash serves a very minimal HTML string which includes the blank container for rendering the app within, the component library's JavaScript and CSS, and a few meta HTML tags like the page title and the encoding (see

dash/dash/dash.py

Lines 293 to 318 in 6a1809f

def index(self, *args, **kwargs): # pylint: disable=unused-argument
scripts = self._generate_scripts_html()
css = self._generate_css_dist_html()
config = self._generate_config_html()
title = getattr(self, 'title', 'Dash')
return '''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{}</title>
{}
</head>
<body>
<div id="react-entry-point">
<div class="_dash-loading">
Loading...
</div>
</div>
<footer>
{}
{}
</footer>
</body>
</html>
'''.format(title, css, config, scripts)
).

This architecture doesn't work well for embedding custom JavaScript scripts and CSS Stylesheets because these scripts and stylesheets usually need to be included in the HTML that is served on page load.

We will support user-supplied JavaScript and CSS through two enhancements:

Enhancement 1 - Automatic

  • This "automatic" method will template in all stylesheets (CSS) and scripts (JavaScript) that are included in a static folder
  • These links will be included in alphabetical order
  • These links will be included after the component library's stylesheets and scripts
  • The server route (/static/<path:string>) for serving these files will be configured automatically by Dash

This method will be what most Dash users will use. It's very easy to use and easy to document ("To include custom CSS, just place your CSS files in static folder. Dash will take care of the rest"). Since the files will be templated alphabetically, the user can control the order (if necessary) by prefixing ordered numbers to the files (e.g. 1-loading.css, 2-app.css).

With this method, we'll be able to add custom CSS processing middleware like minifying CSS or creating cache-busting URLs completely automatically.

If the user needs more control over the placement of these files, they can use the method in "Enhancement 2 - Manual Override".

Enhancement 2 - Manual Override

Currently on page load, Dash serves this HTML string:

dash/dash/dash.py

Lines 293 to 318 in 6a1809f

def index(self, *args, **kwargs): # pylint: disable=unused-argument
scripts = self._generate_scripts_html()
css = self._generate_css_dist_html()
config = self._generate_config_html()
title = getattr(self, 'title', 'Dash')
return '''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{}</title>
{}
</head>
<body>
<div id="react-entry-point">
<div class="_dash-loading">
Loading...
</div>
</div>
<footer>
{}
{}
</footer>
</body>
</html>
'''.format(title, css, config, scripts)
.

This enhancement will make this overridable:

  • The user will assign a HTML string or a function that returns an HTML string to a property on the app object (e.g. app.index).
  • This HTML string will include several predefined template variable names that will correspond to the stylesheets and scripts that Dash needs to include to render the component libraries.
  • The user can include extra meta tags in their template or even include other HTML around their Dash app container.

Here is an example:

def custom_index():
    return '''
    <!DOCTYPE html>
    <html>

        <head>
            <meta charset="UTF-8">
            <meta description="This is my dash app">

            <title>My Custom Dash App</title>
            <link ref="stylesheet" href="/static/my-custom-css-normalize.css">
            {dash_renderer_css_bundle}
            {dash_component_css_bundles}
            <link ref="stylesheet" href="/static/my-component-css-override.css">

        </head>

        <body>
            <div>
                My Custom Header
            </div>
            <div id="react-entry-point">
                <div class="_dash-loading">
                    Loading...
                </div>
            </div>
        </body>

        <footer>
            <script type="javascript" src="/static/my-custom-javascript-bundle.js">
            {dash_renderer_javascript_bundle}
            {dash_component_javascript_bundles}
            <script type="javascript" src="/static/my-custom-javascript-override.js">
        </footer>

    </html>
    '''

app.index = custom_index

The following items will be required in creating an index string:

  • The dash_component_css_bundles, dash_component_javascript_bundles, dash_renderer_css_bundle, dash_renderer_javascript_bundle template names. Dash will look for these names and fill them in with the appropriate component CSS and JavaScript on page load.
  • A <div/> with an id react-entry-point. Dash's front-end will render the app within this div once the JavaScript has been evaluated.

Note the level of customizability in this solution:

  • The user has full control over the location of the custom JS and CSS files with respect to the auto-templated CSS and JavaScript files
  • The user can include custom meta tags in the <head/>. In the example, see the custom <title/> and custom <meta/> description.
  • The user can include custom HTML around their Dash app container (see the <div/> with My Custom Header)
  • The user can omit the templated tags if they want to supply their own front-end JavaScript bundles. This ties in nicely with the "Custom JavaScript Hooks" requirement: if the user adds their own hooks to a custom dash-renderer build, they could remove the default dash_renderer template variable and include their own version.
  • If the tags depend on the URL of the page, they could program in different values that depend on the flask.request.url variable.

Wow! That sounds very promising and powerful!!

Just a question regarding this:

  • If the tags depend on the URL of the page, they could program in different values that depend on the flask.request.url variable.

We are interested in having some parts of the web application without any Dash components at all (just html and js), using the flask server underneath. How might we manage this? would it be possible to not include "react-entry-point" div or to not load Dash depending on the flask.request.url variable?

ned2 commented

@Akronix, for this scenario, you could simply use pure Flask routes for those non-Dash parts of your application and have them wired up to appropriate endpoints that are not routed to Dash.

ned2 commented

@chriddyp, this proposal sounds like a great way to address both the common use-case of adding CSS and JS files and also contexts that require more customisation. It doesn't get much more simple than dropping the files in a static folder, and the manual override method on the other hand allows full control over arbitrary HTML customisations. Can't think of any downsides to this proposal currently. Looking forward to it!

It would also be nice if we could just pass in a simple styles dictionary and include it in the styles section of the head. Sometimes I end up having to go into the source code and changing the smallest things like this.

def index(self, *args, **kwargs):  # pylint: disable=unused-argument
        scripts = self._generate_scripts_html()
        css = self._generate_css_dist_html()
        config = self._generate_config_html()
        title = getattr(self, 'title', 'Dash')
        return '''
        <!DOCTYPE html>
        <html>
            <head>
                <meta charset="UTF-8">
                <title>{}</title>
                {}
                <!-- Custom styles here -->
                <style>
                    body {
                        background-color : black;
                    }
                </style>
            </head>
            <body>
                <div id="react-entry-point">
                    <div class="_dash-loading">
                        Loading...
                    </div>
                </div>
                <footer>
                    {}
                    {}
                </footer>
            </body>
        </html>
        '''.format(title, css, config, scripts)

@ItsBlinkHere, you can override that function within your Dash app files in a simpler way, without having to modify the Dash library itself. This is the approach that I use within my application.py file to add a few meta tags:

import dash

# Modify Dash template to improve cross-browser compatibility (support intranet apps viewed in IE11)
def index(self, *args, **kwargs):  # pylint: disable=unused-argument
    scripts = self._generate_scripts_html()
    css = self._generate_css_dist_html()
    config = self._generate_config_html()
    title = getattr(self, 'title', 'Dash')
    return '''
    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1">
            <title>{}</title>
            {}
            <link rel="icon" type="image/png" href="/static/favicon-16x16.png" sizes="16x16" />
        </head>
        <body>
            <div id="react-entry-point">
                <div class="_dash-loading">
                    Loading...
                </div>
            </div>
            <footer>
                {}
                {}
            </footer>
        </body>
    </html>
    '''.format(title, css, config, scripts)


dash.Dash.index = index

@amarvin Yes that works but it is still very unnecessary. We need to be able to pass in custom html into the head, weather it be meta tags or styles . It would be very easy to implement.

Totally agree. So I'm excited for an upcoming update with such features.

I'm very excited about this; this is fixing a lot of issues/smoothing over a lot of "gotchyas" that I run into all the time. This is, in my humble opinion, A Big Deal that takes the tool one HUGE step forward.

A few questions of clarification:

If I were to add a local favicon (or any local resource), would it go in this static/ directory?

Also, this seems like it could be generic enough to add all sorts of useful information in the head, like meta tags for SEO or (as aforementioned) favicons. Am I getting too ahead of myself here, or is that what this?

I include my favicon in the /static/ directory, and then add a Flask route to serve static files (see related topic)

ned2 commented

@felixvelariusbos, you just need to put your favicon somewhere that is being exposed publicly and make sure that you get its location right in the corresponding element in the head. static/ is a sensible location for sure. And yep, the second manual method proposed by @chriddyp will allow you to insert whatever custom meta tags etc in the header, that's exactly the idea.

ned2 commented

@amarvin you should't have to explicitly add a Flask route to serve static files any more. Dash now follows the default Flask setup of exposing a static folder at the location /static. So you should be able to simply create a static folder and it will just work. The one caveat to this is that if your Dash app is running as a Python package (as opposed to a module/standalone script), you'll need to provide the name param to the Dash class as the name of your package.

My solution for this is would use Jinja2 for it's index rendering and customization #281.

Here a summary of the api I am working on:

static file management.

Dash will now include javascript and stylesheet files contained in a static folder on the index render.
Just have a folder named static at the root of the project to have them included.

You can also give a static_folder param to dash constructor to change the static folder path if it's not the root static:

import dash

app = dash.Dash(static_folder='./resources/static')

The following applies to the static folder includes:

  • Files are included in alphabetic order.
  • Subdirectories will be included after the root folder.
  • javascript files will be included after the components libraries and dash_renderer will be included last.
  • CSS files will be included after the components libraries stylesheets.
  • If a favicon.ico is found, it will be included in the index.

index.html rendering.

Changed the index route to use Jinja2, added templates/index.html. This template can be overriden by the user by changing the config of dash.

Either:

  • app = dash.Dash(index_template='home.html')
  • app.config.index_template = 'home.html'

You just need to put the template in a folder named templates on your root dir.

The new template has to start with: {% extends 'index.html' %}

You can override parts of the template with blocks:

{% extends 'index.html' %}
{% block header %}
<div>My static header</div>
{% endblock %}

Available blocks

  • meta_tags - add the meta tags to the page
  • css_files - add the css files to the page
  • header - custom before the app render.
  • react_entry_point - must have a <div id='react-entry-point'></div> for dash to load.
  • footer - custom after the app render.

Note: Overriding a block will prevent the default block from rendering.

ie: If you override the css_files, no css files will be included unless you do it.

Context variables

  • lang
  • meta_tags
  • title
  • css_files
  • scripts_files

Default Project structure

project_name/
  templates/        # custom template to override the index.html
  static/           # JS/CSS files (can be in subdirectory)
    favicon.ico     # displayed on index if found.
  app.py            # where the app.run_server is located
ned2 commented

Regarding this proposed solution @T4rk1n, besides from the concerns about coupling with Jinja2 (which I raise in #281 ), I'm also worried that this starts coercing people into a specific project directory structure. I feel like Dash's un-opinionated approach to this side of Dash applications is beneficial. More documentation here, potentially including suggested static assets arrangements would certainly be desirable though.

So @T4rk1n and I spoke about this proposal yesterday and I'm warming up to it (or a variant of it), despite some reservations remaining...

If Dash used Jinja for index.html and it included certain externally-defined blocks like 'css files' and 'dash js files' and if we let users interact with Jinja, the following scenarios would be possible:

  1. Use Dash's index.html
  2. Use a custom index.html that included the same blocks within a different frame
  3. Use a custom index.html that overrode the blocks while keeping the default frame

The 'manual override' proposal above is basically string-based and has some special tokens that need to appear in order for things to work. We can just as easily implement this as a Jinja template: file instead of string, Jinja include directive instead of Python string interpolation.

Re imposition of project directories, we could make things a bit more implicit by saying that you have to tell Dash about where your static assets are, and if there happens to be a file there called index.html we'll do Jinja processing on it. That way we're not saying "create a templates directory".

I can do it without the jinja2 rendering, I just feel it get quite messy with string interpolation the more you had to it. With the template file, you also get syntax highlight without the need to copy/paste from another file.

I also think it gives more power to the user, but as discussed with @nicolaskruchten, we want an easy experience for the common user who probably doesn't know anything about jinja. I feel this is still easier than manually overriding the index with a string format, as you can get already formed blocks and just change what you need. Just need more documentation.

With the reusable blocks a user could do without extending the template:

<div>some html</div>
{% include 'blocks/react_entry_point.html' %}
<div>some more html</div>

I also tried the approach of just taking the index.html from the root project, have to put the file in a folder that is available to Jinja. Cannot make the project root a template folder, so I have to copy the file to some directory where the program has write access, I installed appdirs so I could have a cross-platform way of finding a good directory to store the template. So this method add a dependency to appdirs.

For the static files loading, do we want to do like the other resources and provide a way to load them externally if not config.serve_locally ? It could require the user to set a config for the base path where the static files are hosted.

Thanks to everyone who is providing feedback on this one :)

I think that I'm still in favor of the interpolated string option:

  • I agree with @ned2 's point about requiring another concept for users to learn.
  • Some users in the community wish that Dash was entirely Jinja based (e.g. #44) and so I feel like it's easier if we have clear separation between Dash and Jinja

For me, I think the main reason that I prefer the interpolated string method is that I find it to be more transparent in three areas:

  • Am I overriding existing content or adding new content?
  • What's the order of the tags?
  • What's included by default?

Here are some use cases that I have in mind:

  1. When I want to include custom CSS, sometimes I want that CSS to be included before the component's CSS, sometimes after, and sometimes replacing that CSS. By reading the index string, it's easy for me to inject the different strings where I want it. If I used the templates, I might start by setting {% block css %} but then I wouldn't have control over the placement of that block with respect to the auto-included CSS

  2. There have been various requests by users to add various elements to the head:

In those cases, if I used {% block meta %}, it wouldn't be clear to me if I was adding new tags or overriding the existing tags, and both features are valid.
With the index string method, it's very transparent to me what I would be overriding vs extending and which order the tags would be placed in.

And while I like how simple something like this looks:

{% extends 'index.html' %}
{% block header %}
<div>My Project header</div>
{% endblock %}

I feel like in order to understand that, I'd need knowledge of:

  1. First, basic jinja concepts (extends and block)
  2. Then, which blocks are available (e.g. header) and where the fit into the underlying HTML
  3. Whether what I need to do can be simply included in a block header or whether I need to provide a larger index.html

Since our index string is so simple and readable, I feel like it's just as much work to read the index string and see exactly where the different blocks go.


In response to some other comments:

  • I agree that users should not have to create an templates file and that we should either look in the (configurable) static folder or in the root folder
  • While we can make static configurable, I think we should focus on a standard way to do it (i.e. de-emphasize that it's configurable). This is the ruby on rails way to do things and I think it makes our communication easier (e.g. in the forums, we can universally say "put it in the 'static' folder")
  • In addition to a file, I think that users should be able to just pass in a string without creating a separate file
  • If we automatically include CSS and JS that's in a static folder, users that are already including CSS might have their CSS included twice when they upgrade. We could get around this by:
    • Ignoring those users. There probably aren't many of them and they should be reading the CHANGELOG.md anyway.
    • Making our new preferred folder assets instead of static and start auto-including CSS by default only in that folder. "assets" becomes the new "static" and we start using that language everywhere.

I like the assets folder idea, make a clear distinction on what gonna be auto included.

For the index customization with string interpolations, I thought of something like this:

A custom_index field on dash, can be a string or a function.

if its a string:

format the string by keywords

import dash

app = dash.Dash()

index = '''
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>{title}</title>
        {css}
    </head>
    <body>
        <div>Custom header</div>
        <div id="react-entry-point">
            <div class="_dash-loading">
                Loading...
            </div>
        </div>
        <footer>
            {config}
            {scripts}
        </footer>
        <div>Custom footer</div>
    </body>
</html>
'''
app.custom_index = index

The keys would get replaced by dash, they can be omitted.

Or if it's a function:

We call the function with the variables as kwargs.

import dash

app = dash.Dash()

def custom_index(scripts, css, config, **kwargs):
    return '''
        <!DOCTYPE html>
        <html>
            <head>
                <meta charset="UTF-8">
                <meta http-equiv="X-UA-Compatible" content="IE=edge">
                <title>My app title</title>
                {}
            </head>
            <body>
                <div>Custom header</div>
                <div id="react-entry-point">
                    <div class="_dash-loading">
                        Loading...
                    </div>
                </div>
                <footer>
                    {}
                    {}
                </footer>
                <div>Custom footer</div>
            </body>
        </html>
        '''.format(css, config, scripts)
        
app.custom_index = custom_index

One of the strengths of the "just provide a string" approach is that Dash can be quite strict about requiring developers to include the four required tags, and can have very informative error messages if users forget one of them. If we officially support a function as an escape hatch we will likely field a number of issues where people forget or misspell one of them and can't figure out why things aren't working. We'll also start seeing people doing database queries inside that function etc etc. Perhaps going the string-only approach is the most entry-level developer-friendly thing to do. That said, if index() happened to call another internal method with those parameters, like interpolate_index() then clever developers will end up overriding that method and you basically get the flexibility you want, @T4rk1n, without us having to document or support it ;)

One problem with the string format I proposed, If there is {} somewhere in the string that doesn't get formatted it will throw a KeyError or IndexError. So if someone include a <script> it would probably fail.

I like @nicolaskruchten idea of calling interpolate_index from the index, that way someone can override it and doesn't have to worry about the kwargs.

Ah good point about the {}! We should probably regex-replace some less-common string patterns rather than just using simple interpolation...

Yes, I did that in the past, would you recommand a particular syntax ? I like %(key).

Implemented in #286 ๐ŸŽ‰