JinjaX

Adding CSS and JS

Your components might need custom styles or custom JavaScript for many reasons. Instead of using global stylesheet or scripts files, writing assets per individual component has several advantages:

  • Portability: You can copy a component from one project to another kmowing it will keep working as expected.
  • Performance: On each page, only load the css and js that you need. Also, the browser will already have cached the assets of the components for other pages that use them.
  • Simple testing: You can test the JS of a component indepently from others.

Declaring assets

The css and/or the js of a component must be declared in the metatada header with {#css ... #} and {#js ... #}

{#css lorem.css, ipsum.css #}
{#js foo.js, bar.js #}
  • The filepaths must be relative to the root of your components catalog (e.g.: components/).
  • Multiple assets must be separated by commas.
  • Only one and one tag is allowed per component at most, but both are optional.

Global assets

Best practice is to store both CSS and JS files of the component within the same folder. Doing that has several advantages, including easier component reuse in other projects, improved code readability, and simplified debugging.

However, there are instances when you may need to rely on global CSS or JS files, such as third-party libraries. In such cases, you can specify these dependencies in the component's metadata using URLs that start with either "/", "http://," or "https://."

When you do this, JinjaX will render them as is; instead of prepending them with the component's prefix like it normally does.

For example, this code:

{#css foo.css, bar.css, /static/bootstrap.min.css #}
{#js http://example.com/cdn/moment.js, bar.js  #}

{{ catalog.render_assets() }}

will be render as this HTML output:

<link rel="stylesheet" href="/static/components/foo.css">
<link rel="stylesheet" href="/static/components/bar.css">
<link rel="stylesheet" href="/static/bootstrap.min.css">
<script type="module" src="http://example.com/cdn/moment.js"></script>
<script type="module" src="/static/components/bar.js"></script>

Including assets in your pages

The catalog will collect all css and js file paths from the components used on a "page" render on the catalog.collected_css and catalog.collected_js lists.

For example, after rendering this component:

components/MyPage.jinja
{#css mypage.css #}
{#js mypage.js #}

<Layout title="My page">
  <Card>
    <CardBody>
      <h1>Lizard</h1>
      <p>The Iguana is a type of lizard</p>
    </CardBody>
    <CardActions>
      <Button size="small">Share</Button>
    </CardActions>
  </Card>
</Layout>

Asuming the Card, and Button components declare css assests, this will the state of the collected_css list:

catalog.collected_css
['mypage.css', 'card.css', 'button.css']

You can add the <link> and <script> tags in your page automatically by calling catalog.render_assets() like this:

components/Layout.jinja
{#def title #}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>{{ title }}</title>
  {{ catalog.render_assets() }}
</head>
<body>
  {{ content }}
</body>
</html>

The variable will be rendered as:

<link rel="stylesheet" href="/static/components/mypage.css">
<link rel="stylesheet" href="/static/components/card.css">
<link rel="stylesheet" href="/static/components/button.css">
<script type="module" src="/static/components/mypage.js"></script>
<script type="module" src="/static/components/card.js"></script>
<script type="module" src="/static/components/button.js"></script>

Middleware

The tags above will not work at all if your application can't return the content of those files, and right now it can't.

For that reason, JinjaX include a WSGI middleware that will process those URLs if you add it to your application.

from flask import Flask
from jinjax import Catalog

app = Flask(__name__)

# Here we add the flask Jinja globals, filters, etc. like `url_for()`
catalog = jinjax.Catalog(jinja_env=app.jinja_env)

catalog.add_folder("myapp/components")

app.wsgi_app = catalog.get_middleware(
    app.wsgi_app,
    autorefresh=app.debug,
)

The middleware uses the battle-tested Whitenoise library and it will only respond to the .css and .js files inside the component(s) folder(s). You can configure it to also return files with other extensions. For example:

catalog.get_middleware(app, allowed_ext=[".css", ".js", ".svg", ".png"])

Be aware that, if you use this option, get_middleware() must be called after all folders are added.

Good practices

CSS Scoping

The styles of your components will not be auto-scoped. This means the styles of a component can affect other components, and, likewise, it will be affected by global styles or the styles of other components.

To protect yourself against that, always add a custom class to the root element(s) of your component and use it to scope the rest of the component styles. Example:

components/Card.jinja
{#css card.css #}

<div {{ attrs.render(class="Card") }}>
  <h1>My Card</h1>
  ...
</div>
components/card.css
/* 🚫 DO NOT do this */
h1 { font-size: 2em; }
h2 { font-size: 1.5em; }
a { color: blue; }

/* 👍 DO THIS instead */
.Card h1 { font-size: 2em; }
.Card h2 { font-size: 1.5em; }
.Card a { color: blue; }

Always use a class instead of an id, or the component will not be usable more than once per page.

JS events

Your components might be inserted in the page on-the fly, after the JavaScript files has been loaded and executed. So, attaching events to the elements on the page on load will not be enough:

components/card.js
// This will fail for any Card component inserted after page load
document.querySelectorAll('.Card button.share')
  .forEach( (node) => {
    node.addEventListener("click", handleClick)
  })

/* ... etc ... */

A solution can be using event delegation:

components/card.js
// This will work for any Card component inserted after page load
document.addEventListener("click", (event) => {
  if (event.target.matches(".Card button.share")) {
    handleClick(event)
  }
})