Repository navigation
Proposal for Offline CSS and JS and Customizable index.html #265
Description
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
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
staticfolder - 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:
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
appobject (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_bundletemplate names. Dash will look for these names and fill them in with the appropriate component CSS and JavaScript on page load. - A
<div/>with anidreact-entry-point. Dash's front-end will render the app within thisdivonce 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/>withMy 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-rendererbuild, they could remove the defaultdash_renderertemplate 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.urlvariable.
Activity
Akronix commentedon Jun 9, 2018
Wow! That sounds very promising and powerful!!
Just a question regarding this:
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 commentedon Jun 17, 2018
@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 commentedon Jun 17, 2018
@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!
ItsBlinkHere commentedon Jun 28, 2018
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.
amarvin commentedon Jun 28, 2018
@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.pyfile to add a fewmetatags:ItsBlinkHere commentedon Jun 28, 2018
@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.
amarvin commentedon Jun 28, 2018
Totally agree. So I'm excited for an upcoming update with such features.
felixvelariusbos commentedon Jun 29, 2018
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?
amarvin commentedon Jun 29, 2018
I include my favicon in the /static/ directory, and then add a Flask route to serve static files (see related topic)
ned2 commentedon Jun 30, 2018
@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 commentedon Jun 30, 2018
@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
staticfolder 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 thenameparam to theDashclass as the name of your package.T4rk1n commentedon Jul 5, 2018
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
staticat 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:The following applies to the static folder includes:
favicon.icois 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:
Available blocks
<div id='react-entry-point'></div>for dash to load.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
Default Project structure
ned2 commentedon Jul 6, 2018
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.
nicolaskruchten commentedon Jul 6, 2018
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.htmland 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:index.htmlindex.htmlthat included the same blocks within a different frameindex.htmlthat overrode the blocks while keeping the default frameThe '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
includedirective 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
staticassets are, and if there happens to be a file there calledindex.htmlwe'll do Jinja processing on it. That way we're not saying "create atemplatesdirectory".T4rk1n commentedon Jul 6, 2018
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:
I also tried the approach of just taking the
index.htmlfrom 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 installedappdirsso I could have a cross-platform way of finding a good directory to store the template. So this method add a dependency toappdirs.T4rk1n commentedon Jul 9, 2018
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.chriddyp commentedon Jul 9, 2018
Thanks to everyone who is providing feedback on this one :)
I think that I'm still in favor of the interpolated string option:
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:
Here are some use cases that I have in mind:
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 CSSThere have been various requests by users to add various elements to the head:
<meta http-equiv="X-UA-Compatible" content="IE=edge">) Improves cross-browser compatibility #275In 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:
I feel like in order to understand that, I'd need knowledge of:
extendsandblock)blocks are available (e.g.header) and where the fit into the underlying HTMLblockheader or whether I need to provide a largerindex.htmlSince 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:
templatesfile and that we should either look in the (configurable)staticfolder or in the root folderstaticconfigurable, 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")staticfolder, users that are already including CSS might have their CSS included twice when they upgrade. We could get around this by:assetsinstead ofstaticand start auto-including CSS by default only in that folder. "assets" becomes the new "static" and we start using that language everywhere.T4rk1n commentedon Jul 9, 2018
I like the
assetsfolder 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_indexfield on dash, can be a string or a function.if its a string:
format the string by keywords
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.
nicolaskruchten commentedon Jul 10, 2018
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, likeinterpolate_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 ;)T4rk1n commentedon Jul 10, 2018
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.nicolaskruchten commentedon Jul 10, 2018
Ah good point about the
{}! We should probably regex-replace some less-common string patterns rather than just using simple interpolation...T4rk1n commentedon Jul 10, 2018
Yes, I did that in the past, would you recommand a particular syntax ? I like
%(key).chriddyp commentedon Jul 27, 2018
Implemented in #286 🎉