Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow optional header and footer #171

Closed
wants to merge 2 commits into from
Closed

allow optional header and footer #171

wants to merge 2 commits into from

Conversation

chriddyp
Copy link
Member

@chriddyp chriddyp commented Nov 30, 2017

This PR depends on html components having a HTML serializer, see
plotly/dash-html-components#29

This PR simplifies adding custom JavaScript and CSS to Dash apps - a more flexible and declarative alternative to app.css.append_css and app.scripts.append_script

Example usage:

app.head = [
    html.Link(
        href='https://codepen.io/chriddyp/pen/bWLwgP.css',
        rel='stylesheet'
    ),
    ('''
    <style type="text/css">
    html {
        font-size: 50px;
    }
    </style>
    '''),
    html.Title(path)
]

app.footer = [
    html.Script(type='text/javascript', children='alert("hello world")')
]

app.head and app.footer can also be functions, which enables setting variables dynamically based off of the URL (i.e. unique page titles)

def head():
    path = request.path
    return html.Title(path)

app.head = head

Unlike app.layout, app.head and app.footer are templated directly into the HTML (as HTML!) rather than generated in Dash’s front-end. This allows the components to be rendered immediately on page load rather than after page load (required for things like page title and meta descriptions and for preventing a “Flash of Unstyled Content” https://en.wikipedia.org/wiki/Flash_of_unstyled_content).

This makes the components in app.head and app.footer different than the components in app.layout. In particular:

  • Callbacks can’t be applied to components rendered in app.head or app.footer
  • Only dash_html_components can be applied to the app.head and app.footer as these are the only valid HTML tags (a `dash_core_components.Graph is not a valid HTML tag, it’s a rich component generated by React.js with dynamic javascript and CSS)

Fixes ##170 and several issues brought up in the dash community forum (https://community.plot.ly/c/dash)

this depends on html components having a HTML serializer, see
plotly/dash-html-components#29
@chriddyp
Copy link
Member Author

Relevant community forum threads that this PR would solve:

Many thanks to the dash community for these feature requests and discussions ❤️

@ned2
Copy link
Contributor

ned2 commented Dec 1, 2017

I really like this; it opens up quite a lot of flexibility.

The main thought I have is that it's likely that a common use case will be adding one or more CSS files as tags and nothing else, in which case there's a bit of overhead in creating a list of potentially multiple html.Link() objects with the needed params. Extending the existing app.css attr to support href values with the key user_url or similar (which could then be all inserted between {component_css} and {custom_head_html}) would possibly be nice here.

@radumas
Copy link

radumas commented Dec 1, 2017

Question: If the resources linked by html.Link are local to the app, is the following still necessary for serving those resources?

@app.server.route('/static/<path:path>')
def static_file(path):
    static_folder = os.path.join(os.getcwd(), 'static')
    return send_from_directory(static_folder, path)

If yes, could that be more obvious in the documentation?
If no, great!

@chriddyp
Copy link
Member Author

chriddyp commented Dec 1, 2017

Question: If the resources linked by html.Link are local to the app, is the following still necessary for serving those resources?

It is still necessary and yes, we should make it more obvious :)

@radumas
Copy link

radumas commented Dec 1, 2017

Thanks @chriddyp !
Follow up question, if one were using a mix of local and external scripts, would there be a conflict somehow?

Do you need to use the below options if using local scripts? (taken from this example)

app.css.config.serve_locally = True
app.scripts.config.serve_locally = True

@chriddyp
Copy link
Member Author

chriddyp commented Dec 1, 2017

Follow up question, if one were using a mix of local and external scripts, would there be a conflict somehow?

Do you need to use the below options if using local scripts? (taken from this example)

This would no longer conflict. serve_locally would only be used to specify whether the component libraries (dash-renderer, dash-core-components, dash-html-componets) should be loaded from the CDN (False) or from the python package directory (True)

Whatever you specify in head or footer would load, regardless of the serve_locally setting. That is, you could load both locally and remotely if you wanted to:

app.head = [
    html.Link(
        href='https://codepen.io/chriddyp/pen/bWLwgP.css',
        rel='stylesheet'
    ),
    html.Link(
        href='/static/my-local-stylesheet.css',
        rel='stylesheet'
    ),
]

With this addition, we would remove the append_css and append_script from the documentation and ecourage app.head and app.footer as the official way to load assets. Behind the scenes, append_css and append_script would still be used by the component libraries (e.g. here: https://github.com/plotly/dash-core-components/blob/e41ae87fe8dd85fdc5f0a573da5e196bfcbb851e/dash_core_components/__init__.py#L15-L49)

@gustavengstrom
Copy link

gustavengstrom commented Dec 5, 2017

This commit seems very useful. However, I recieved the error below when trying this out. My test implied pasting:

app.head = [
html.Link(
href='https://codepen.io/chriddyp/pen/bWLwgP.css',
rel='stylesheet'
)
]

into the first app example in:
https://plot.ly/dash/getting-started

This gave me the error below. Am I missing something. Any ideas?

127.0.0.1 - - [05/Dec/2017 09:20:39] "GET / HTTP/1.1" 500 -
Traceback (most recent call last):
File "/Users/gus/anaconda/lib/python2.7/site-packages/flask/app.py", line 1997, in call
return self.wsgi_app(environ, start_response)
File "/Users/gus/anaconda/lib/python2.7/site-packages/flask/app.py", line 1985, in wsgi_app
response = self.handle_exception(e)
File "/Users/gus/anaconda/lib/python2.7/site-packages/flask/app.py", line 1540, in handle_exception
reraise(exc_type, exc_value, tb)
File "/Users/gus/anaconda/lib/python2.7/site-packages/flask/app.py", line 1982, in wsgi_app
response = self.full_dispatch_request()
File "/Users/gus/anaconda/lib/python2.7/site-packages/flask/app.py", line 1614, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/Users/gus/anaconda/lib/python2.7/site-packages/flask/app.py", line 1517, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/Users/gus/anaconda/lib/python2.7/site-packages/flask/app.py", line 1612, in full_dispatch_request
rv = self.dispatch_request()
File "/Users/gus/anaconda/lib/python2.7/site-packages/flask/app.py", line 1598, in dispatch_request
return self.view_functionsrule.endpoint
File "/Users/gus/anaconda/lib/python2.7/site-packages/dash/dash.py", line 288, in index
custom_head_html = self._generate_head()
File "/Users/gus/anaconda/lib/python2.7/site-packages/dash/dash.py", line 366, in _generate_head
head_html = self._generate_footer_or_header_html('head')
File "/Users/gus/anaconda/lib/python2.7/site-packages/dash/dash.py", line 356, in _generate_footer_or_header_html
[convert_to_html(element) for element in section_instance])
File "/Users/gus/anaconda/lib/python2.7/site-packages/dash/dash.py", line 347, in convert_to_html
element_html = element.to_html5()
AttributeError: 'Link' object has no attribute 'to_html5'

@Volodymyrk
Copy link

We did something similar - we subclassed from Dash to override index() to use our own "template.html" file. Wouldn't ability to pass a custom html template string be more generic, compared to .head() and .footer() methods? I'd like to stay within Python as much as any other pythonista, but ability to just copy-paste some html/js/css is tempting, given how easy it is. Or may be we can have both..?

@yunake
Copy link

yunake commented Dec 11, 2017

yeah we're also overriding the default index(). that said, i think it's unnecessary for most users and those advanced enough to require it can easily subclass, no?

@ned2
Copy link
Contributor

ned2 commented Dec 11, 2017 via email

@jtpio
Copy link

jtpio commented Dec 11, 2017

Overriding the index method has the drawback of having to know the internals of that method and replicating the logic in the derived class.

It works for now but I would also be up for a proper API directly exposed by Dash.

@ppwfx
Copy link

ppwfx commented Jan 8, 2018

will it be merged at some point?

@allanelder
Copy link

@21stio it looks like the CI build failed when it was merged in

@chriddyp can you take a look? I was looking for exactly this feature; it looks like its failing on a Python 3 compatibility with basestring.

def convert_to_html(element):
if isinstance(element, Component):
element_html = element.to_html5()
elif isinstance(element, basestring):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

failing in python3. need:
from past.builtins import basestring # pip install future

@ploncker
Copy link

I love Dash but as a data scientist with no CSS experience this whole CSS thing is very frustrating particularly when there are no clear examples of how use local CSS files. In R Shiny stuff just works and don have to worry about style sheets. But I prefer Python and was hoping Dash could be my salvation.

I want to recreate this bootstrap demo (https://getbootstrap.com/docs/4.0/examples/dashboard/) in Dash but cant get past the first hurdle of using the appropriate CSS's. This or a simple demo showing how to use different CSS locally would be great.

@chriddyp
Copy link
Member Author

chriddyp commented Feb 16, 2018

@ploncker - Could you please open this issue in the community forum? I'd like to reserve comments in this PR about the actual PR. For example, see https://community.plot.ly/t/how-do-i-use-dash-to-add-local-css/4914/22 or https://community.plot.ly/t/serve-locally-option-with-additional-scripts-and-style-sheets/6974/6?u=chriddyp.

@ngnpope
Copy link
Contributor

ngnpope commented Mar 23, 2018

@chriddyp - I've just come across this and it would be great for a number of reasons:

  1. In development I want to use less.js so I need to be able to set type="text/less".
  2. In loading from a CDN, e.g. unpkg.com, I want to use subresource integrity hashes.

I have currently worked around this with the following monkey patch:

import dash_renderer, six, types  # noqa


def _generate_css_dist_html(self):
    links = self._collect_and_register_resources(self.css.get_all_css())
    return '\n'.join('<link rel="stylesheet" {}>'.format(' '.join('{}="{}"'.format(k, v) for k, v in (
        [('href', link)] if isinstance(link, six.string_types) else sorted(link.items())
    ))) for link in links)


def _generate_scripts_html(self):
    srcs = self._collect_and_register_resources(
        self.scripts._resources._filter_resources(dash_renderer._js_dist_dependencies) +
        self.scripts.get_all_scripts() +
        self.scripts._resources._filter_resources(dash_renderer._js_dist),
    )
    return '\n'.join('<script {}></script>'.format(' '.join('{}="{}"'.format(k, v) for k, v in (
        [('src', src)] if isinstance(src, six.string_types) else sorted(src.items())
    ))) for src in srcs)


# XXX: Temporary hack to allow attributes on <link> and <script>.
#      See https://github.com/plotly/dash/pull/171 for alternate solution in development.
app._generate_css_dist_html = types.MethodType(_generate_css_dist_html, app)
app._generate_scripts_html = types.MethodType(_generate_scripts_html, app)

This allows me to do something like the following:

app.css.append_css({'external_url': (
    {'href': '/static/fds/less/default.less', 'type': 'text/less'},
)})
app.scripts.append_script({'external_url': (
    {
        'src': 'https://unpkg.com/less@2.7.2/dist/less.min.js',
        'integrity': 'sha384-tNVWZNCzgnTSr8HLSfGS6L7pO03KOgRXJsprbx6bitch1BMri1brpoPxtyY4pDqn',
        'crossorigin': 'anonymous',
    },
)})

While these changes may not seem necessary because of the proposed changes in this PR, they are still worth considering as it would also benefit adding subresource integrity hashes to the dependencies added by dash_renderer.

What are your thoughts?

@chriddyp
Copy link
Member Author

chriddyp commented Jun 8, 2018

@Volodymyrk

We did something similar - we subclassed from Dash to override index() to use our own "template.html" file. Wouldn't ability to pass a custom html template string be more generic, compared to .head() and .footer() methods? I'd like to stay within Python as much as any other pythonista, but ability to just copy-paste some html/js/css is tempting, given how easy it is. Or may be we can have both..?

@ned2

The main thought I have is that it's likely that a common use case will be adding one or more CSS files as tags and nothing else, in which case there's a bit of overhead in creating a list of potentially multiple html.Link() objects with the needed params.

This is really good feedback. In some separate discussions with @nicolaskruchten , we discussed how this method obscures things about how Dash's components work: suddenly only some components could work in app.head while other components (everything except dash_html_components) don't work in app.head. And suddenly some components (all those in app.head) don't work with callbacks. Too obscure and subtle!

Taking this feedback, I propose 2 solutions for adding local CSS and JS to apps in #265. Let's continue the discussion there!

@shengqiangzhang
Copy link

it seems no any change when i try to change the favicon.

app.head = [
    html.Link(
        href='https://www.sportsingapore.gov.sg/SSC.png',
        rel='icon'
    ),
]

@GaelVaroquaux
Copy link

I ran into this PR trying to add social media tags to a dash app (https://css-tricks.com/essential-meta-tags-social-media/ for instance).

It would have nicely solved my need.

@nicolaskruchten
Copy link
Contributor

@GaelVaroquaux this can be done using the solution linked above #265 which was implemented in #286 ... The docs are here https://dash.plot.ly/external-resources under "Customizing Dash's HTML Index Template"

@GaelVaroquaux
Copy link

@nicolaskruchten : that's exactly what I was looking for. I somehow missed it in the thread above. Thanks a lot!!

@nicolaskruchten
Copy link
Contributor

You're welcome! We should try to get more into the habit of coming back to old issues and posting a "canonical response" like this, to ensure no one is left behind :) Thanks for pinging us on this one!

@GaelVaroquaux
Copy link

Actually, the challenge is that the beautiful solution that I was looking for was somewhat at the bottom of the page, so I didn't see it at first look on that page.

I've been asking for a templating engine for dash for a few days, I finally found it :).

HammadTheOne pushed a commit to HammadTheOne/dash that referenced this pull request May 28, 2021
use dash loosen-testing-reqs branch and fix linting
HammadTheOne pushed a commit that referenced this pull request Jul 23, 2021
use dash loosen-testing-reqs branch and fix linting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.