django-components/docs/concepts/advanced/rendering_js_css.md
Juro Oravec bf7a204e92
feat: add "simple", "prepend", and "append" render types (#1156)
* feat: add "simple", "prepend", and "append" render types

* refactor: explicitly set strategy for "document" in tests
2025-05-02 15:07:16 +02:00

15 KiB

JS and CSS output locations

If:

  1. Your components use JS and CSS via any of:
  2. And you use the ComponentDependencyMiddleware middleware

Then, by default, the components' JS and CSS will be automatically inserted into the HTML:

  • CSS styles will be inserted at the end of the <head>
  • JS scripts will be inserted at the end of the <body>

If you want to place the dependencies elsewhere in the HTML, you can override the locations by inserting following Django template tags:

So if you have a component with JS and CSS:

from django_components import Component, types

class MyButton(Component):
    template: types.django_html = """
        <button class="my-button">
            Click me!
        </button>
    """
    js: types.js = """
        for (const btnEl of document.querySelectorAll(".my-button")) {
            btnEl.addEventListener("click", () => {
                console.log("BUTTON CLICKED!");
            });
        }
    """
    css: types.css """
        .my-button {
            background: green;
        }
    """

    class Media:
        js = ["/extra/script.js"]
        css = ["/extra/style.css"]

Then the JS from MyButton.js and MyButton.Media.js will be rendered at the default place, or in {% component_js_dependencies %}.

And the CSS from MyButton.css and MyButton.Media.css will be rendered at the default place, or in {% component_css_dependencies %}.

And if you don't specify {% component_dependencies %} tags, it is the equivalent of:

<!doctype html>
<html>
  <head>
    <title>MyPage</title>
    ...
    {% component_css_dependencies %}
  </head>
  <body>
    <main>
      ...
    </main>
    {% component_js_dependencies %}
  </body>
</html>

Setting up the middleware

ComponentDependencyMiddleware is a Django middleware designed to manage and inject CSS / JS dependencies of rendered components dynamically. It ensures that only the necessary stylesheets and scripts are loaded in your HTML responses, based on the components used in your Django templates.

To set it up, add the middleware to your MIDDLEWARE in settings.py:

MIDDLEWARE = [
    # ... other middleware classes ...
    'django_components.middleware.ComponentDependencyMiddleware'
    # ... other middleware classes ...
]

render_dependencies and rendering JS / CSS without the middleware

For most scenarios, using the ComponentDependencyMiddleware middleware will be just fine.

However, this section is for you if you want to:

  • Render HTML that will NOT be sent as a server response
  • Insert pre-rendered HTML into another component
  • Render HTML fragments (partials)

Every time there is an HTML string that has parts which were rendered using components, and any of those components has JS / CSS, then this HTML string MUST be processed with render_dependencies().

It is actually render_dependencies() that finds all used components in the HTML string, and inserts the component's JS and CSS into {% component_dependencies %} tags, or at the default locations.

Render JS / CSS without the middleware

The truth is that the ComponentDependencyMiddleware middleware just calls render_dependencies(), passing in the HTML content. So if you render a template that contained {% component %} tags, you MUST pass the result through render_dependencies(). And the middleware is just one of the options.

Here is how you can achieve the same, without the middleware, using render_dependencies():

from django.template.base import Template
from django.template.context import Context
from django_component import render_dependencies

template = Template("""
    {% load component_tags %}
    <!doctype html>
    <html>
    <head>
        <title>MyPage</title>
    </head>
    <body>
        <main>
            {% component "my_button" %}
                Click me!
            {% endcomponent %}
        </main>
    </body>
    </html>
""")

rendered = template.render(Context())
rendered = render_dependencies(rendered)

Same applies if you render a template using Django's django.shortcuts.render:

from django.shortcuts import render

def my_view(request):
    rendered = render(request, "pages/home.html")
    rendered = render_dependencies(rendered)
    return rendered

Alternatively, when you render HTML with Component.render() or Component.render_to_response(), these, by default, call render_dependencies() for you, so you don't have to:

from django_components import Component

class MyButton(Component):
    ...

# No need to call `render_dependencies()`
rendered = MyButton.render()

Inserting pre-rendered HTML into another component

In previous section we've shown that render_dependencies() does NOT need to be called when you render a component via Component.render().

API of django_components makes it possible to compose components in a "React-like" way, where we pre-render a piece of HTML and then insert it into a larger structure.

To do this, you must add render_dependencies=False to the nested components:

card_actions = CardActions.render(
    kwargs={"editable": editable},
    render_dependencies=False,
)

card = Card.render(
    slots={"actions": card_actions},
    render_dependencies=False,
)

page = MyPage.render(
    slots={"card": card},
)

Why is render_dependencies=False required?

This is a technical limitation of the current implementation.

As mentioned earlier, each time we call Component.render(), we also call render_dependencies().

However, there is a problem here - When we call render_dependencies() inside CardActions.render(), we extract and REMOVE the info on components' JS and CSS from the HTML. But the template of CardActions contains no {% component_depedencies %} tags, and nor <head> nor <body> HTML tags. So the component's JS and CSS will NOT be inserted, and will be lost.

To work around this, you must set render_dependencies=False when rendering pieces of HTML with Component.render() and inserting them into larger structures.

Summary

  1. Every time you render HTML that contained components, you have to call render_dependencies() on the rendered output.
  2. There are several ways to call render_dependencies():
  3. If you pre-render one component to pass it into another, the pre-rendered component must be rendered with render_dependencies=False.

Dependencies strategies

The rendered HTML may be used in different contexts (browser, email, etc). If your components use JS and CSS scripts, you may need to handle them differently.

render() and render_to_response() accept a deps_strategy parameter, which controls where and how the JS / CSS are inserted into the HTML.

The deps_strategy parameter is set at the root of a component render tree, which is why it is not available for the {% component %} tag.

!!! info

The `deps_strategy` parameter is ultimately passed to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).

There are five dependencies strategies:

  • document (default)
    • Smartly inserts JS / CSS into placeholders or into <head> and <body> tags.
    • Inserts extra script to allow fragment strategy to work.
    • Assumes the HTML will be rendered in a JS-enabled browser.
  • fragment
    • A lightweight HTML fragment to be inserted into a document with AJAX.
    • No JS / CSS included.
  • simple
    • Smartly insert JS / CSS into placeholders or into <head> and <body> tags.
    • No extra script loaded.
  • prepend
    • Insert JS / CSS before the rendered HTML.
    • No extra script loaded.
  • append
    • Insert JS / CSS after the rendered HTML.
    • No extra script loaded.

document

deps_strategy="document" is the default. Use this if you are rendering a whole page, or if no other option suits better.

html = Button.render(deps_strategy="document")

When you render a component tree with the "document" strategy, it is expected that:

  • The HTML will be rendered at page load.
  • The HTML will be inserted into a page / browser where JS can be executed.

Location:

JS and CSS is inserted:

  • Preferentially into JS / CSS placeholders like {% component_js_dependencies %}
  • Otherwise, JS into <body> element, and CSS into <head> element
  • If neither found, JS / CSS are NOT inserted

Included scripts:

For the "document" strategy, the JS and CSS is set up to avoid any delays when the end user loads the page in the browser:

  • Components' primary JS and CSS scripts (Component.js and Component.css) - fully inlined:

    <script>
        console.log("Hello from Button!");
    </script>
    <style>
        .button {
        background-color: blue;
        }
    </style>
    
  • Components' secondary JS and CSS scripts (Component.Media) - inserted as links:

    <link rel="stylesheet" href="https://example.com/styles.css" />
    <script src="https://example.com/script.js"></script>
    
  • A JS script is injected to manage component dependencies, enabling lazy loading of JS and CSS for HTML fragments.

!!! info

This strategy is required for fragments to work properly, as it sets up the dependency manager that fragments rely on.

!!! note "How the dependency manager works"

The dependency manager is a JS script that keeps track of all the JS and CSS dependencies that have already been loaded.

When a fragment is inserted into the page, it will also insert a JSON `<script>` tag with fragment metadata.

The dependency manager will pick up on that, and check which scripts the fragment needs.

It will then fetch only the scripts that haven't been loaded yet.

fragment

deps_strategy="fragment" is used when rendering a piece of HTML that will be inserted into a page that has already been rendered with the "document" strategy:

fragment = MyComponent.render(deps_strategy="fragment")

The HTML of fragments is very lightweight because it doesn't include the JS and CSS scripts of the rendered components.

With fragments, even if a component has JS and CSS, you can insert the same component into a page hundreds of times, and the JS and CSS will only ever be loaded once.

This is intended for dynamic content that's loaded with AJAX after the initial page load, such as with jQuery, HTMX, AlpineJS or similar libraries.

Location:

None. The fragment's JS and CSS files will be loaded dynamically into the page.

Included scripts:

  • A special JSON <script> tag that tells the dependency manager what JS and CSS to load.

simple

deps_strategy="simple" is used either for non-browser use cases, or when you don't want to use the dependency manager.

Practically, this is the same as the "document" strategy, except that the dependency manager is not used.

html = MyComponent.render(deps_strategy="simple")

Location:

JS and CSS is inserted:

  • Preferentially into JS / CSS placeholders like {% component_js_dependencies %}
  • Otherwise, JS into <body> element, and CSS into <head> element
  • If neither found, JS / CSS are NOT inserted

Included scripts:

  • Components' primary JS and CSS scripts (Component.js and Component.css) - fully inlined:

    <script>
        console.log("Hello from Button!");
    </script>
    <style>
        .button {
            background-color: blue;
        }
    </style>
    
  • Components' secondary JS and CSS scripts (Component.Media) - inserted as links:

    <link rel="stylesheet" href="https://example.com/styles.css" />
    <script src="https://example.com/script.js"></script>
    
  • No extra scripts are inserted.

prepend

This is the same as "simple", but placeholders like {% component_js_dependencies %} and HTML tags <head> and <body> are all ignored. The JS and CSS are always inserted before the rendered content.

html = MyComponent.render(deps_strategy="prepend")

Location:

JS and CSS is always inserted before the rendered content.

Included scripts:

Same as for the "simple" strategy.

append

This is the same as "simple", but placeholders like {% component_js_dependencies %} and HTML tags <head> and <body> are all ignored. The JS and CSS are always inserted after the rendered content.

html = MyComponent.render(deps_strategy="append")

Location:

JS and CSS is always inserted after the rendered content.

Included scripts:

Same as for the "simple" strategy.