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

Jinja index rendering #281

Closed
T4rk1n opened this issue Jul 5, 2018 · 8 comments
Closed

Jinja index rendering #281

T4rk1n opened this issue Jul 5, 2018 · 8 comments
Assignees

Comments

@T4rk1n
Copy link
Contributor

T4rk1n commented Jul 5, 2018

Right now the rendering of the dash index is done with a simple string formatting.
This is not ideal, flask is shipped with jinja2 and we should use it to render the
dash index page instead.

Pros:

  • Easier to change.
  • Easier to read.
  • Easily expandable.
  • Safer formatting.
  • Extendable by the user.
  • Context variables.
  • Blocks

Cons:

  • Slower ?

Here's the template I've come up with on branch jinja-index-render:

<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
    <meta charset="UTF-8">
    {% block meta_tags %}
    {% for meta in metas %}
        <meta name="{{ meta.name }}" content="{{ meta.content }}" >
    {% endfor %}
    {% endblock %}
    <title>{{ title }}</title>
    {% block css_files %}
    {% if favicon %}
    <link rel="shortcut icon" href="{{ favicon }}" type="image/x-icon">
    {% endif %}
    {% for c in css_files %}
        <link rel="stylesheet" href="{{ c | safe }}">
    {% endfor %}
    {% endblock %}
</head>
<body>
{% block header %}
{% endblock %}
{% block react_entry_point %}
    <div id="react-entry-point">
        <div class="_dash-loading">
            Loading...
        </div>
    </div>
{% endblock %}
<footer>
    <script id="_dash-config" type="application/json">{{ configs | safe }}</script>
    {% for src in scripts_files %}
        <script src="{{ src | safe }}"></script>
    {% endfor %}
    {% block footer %}
    {% endblock %}
</footer>
</body>
</html>

This template can be overridden by the user by changing the config of dash.

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

The user can then modify blocks as he wishes:

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

I'd like feedback on this, this is a step I want to take before adding new features such as #265 and #66.

@nicolaskruchten
Copy link
Contributor

nicolaskruchten commented Jul 5, 2018

I'm having a bit of trouble understanding the "pros" here... Is it mostly that it would be easier for people who already use Jinja elsewhere to reuse bits of those apps? Because this is just rendered one time, without any conditional variables being injected etc (other than JS and CSS lists)..

@ned2
Copy link
Contributor

ned2 commented Jul 6, 2018

I don't really see the pros here also. index.html is just a skeleton html that is needed to render the initial page the React app is injected to. I don't see it as being a common site of customisation, which is why the manual override method proposed in #265 is an advanced feature, intended for use by people with unusual requirements. So it doesn't seem like a use-case that we should be optimising for developer ergonomics (which seems to be the main benefit of using Jinja2 templates).

The big downside that is not mentioned is that this introduces a functional dependency on Jinja2. Even though Jinja2 is bundled with Flask, it's not used by default. In addition to introducing this dependency, we'd now also require people to learn yet another markup language, as opposed to vanilla HTML.

Currently, there's nothing stopping someone subclassing the Dash class and using Jinja2 to compile the HTML if they wanted the convenience. This could be called out as a mode of use that the manual override method from #265 should support?

@valentijnnieman
Copy link
Contributor

I would also argue that, if this is our proposed solution in the docs, could create confusion about the use of templates (templates implying more than one template), while one of the great things about Dash is that you can write single-page applications in Python. Not that you have to use templates of course, but I would stay away from using templates in official documentation, just to avoid confusion!

@nicolaskruchten
Copy link
Contributor

(perhaps this isn't really a distinct issue from #265... I'm having trouble picking apart which bits of my feedback go in which of the two issues :)

I also don't like the templates directory because it's plural: it induces people to ask "what other templates could/should I have in there?". I would favour an implicit default path personally (as mentioned in #265)

The biggest problem that I see here is that this proposal seems to reduce the difficulty of very-advanced use cases at the expense of raising the complexity of very-basic use cases, by forcing people to learn Jinja as opposed to the string interpolation suggested in #265. This isn't a deal breaker for me but it's something to think about.

@nicolaskruchten
Copy link
Contributor

(sorry, I definitely didn't mean to close the issue, I mis-clicked!)

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Jul 6, 2018

The templates with plural is just the way it is with default flask. I also think as since it's just single file we want to override it would be better to just look for it in the project root.

@chriddyp
Copy link
Member

chriddyp commented 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:

  • 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. HTML5 templates? #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 I want to replace the component's 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 use cases 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 design decisions:

  • I agree that users should not have to create an templates folder 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. (I don't necessarily agree with this.)
    • 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.

@T4rk1n T4rk1n closed this as completed Jul 25, 2018
HammadTheOne pushed a commit to HammadTheOne/dash that referenced this issue May 28, 2021
@mccarthysean
Copy link

For what it's worth, here's what I did to build a Dash template from an existing Flask, Jinja template.

In case you're wondering, I have a hybrid web app that uses both Flask and Dash.

First extend your "base.html" template into a Dash-specific template (e.g. "base_dash.html"), using HTML comments for the things Dash needs to replace (e.g. {%metas%} or {%app_entry%}:

{% extends "base.html" %}

{% block head %}
  <!-- metas -->
  <title>
    <!-- title -->
  </title>
  <!-- favicon -->
  <!-- css -->
{% endblock %}

{% block body %}
  <!-- app_entry -->

<footer>
  <!-- config -->
  <!-- scripts -->
  <!-- renderer -->
</footer>
{% endblock %}

Back in Python, when setting up the Dash app:

    dashapp = dash.Dash()

    # FYI, you need both an app context and a request context to use url_for() in the Jinja2 templates
    with app.app_context(), app.test_request_context():
        layout_dash = pathlib.Path(get_root_path(__name__)).joinpath("templates").joinpath("base_dash.html")

        with open(layout_dash, "r") as f:
            html_body = render_template_string(f.read())

        comments_to_replace = ("metas", "title", "favicon", "css", "app_entry", "config", "scripts", "renderer")
        for comment in comments_to_replace:
            html_body = html_body.replace(f"<!-- {comment} -->", "{%" + comment + "%}")

        dashapp.index_string = html_body

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

No branches or pull requests

6 participants