Skip to content

Secondary JS / CSS files

Overview¤

Each component can define extra or "secondary" CSS / JS files using the nested Component.Media class, by setting Component.Media.js and Component.Media.css.

The main HTML / JS / CSS files are limited to 1 per component. This is not the case for the secondary files, where components can have many of them.

There is also no special behavior or post-processing for these secondary files, they are loaded as is.

You can use these for third-party libraries, or for shared CSS / JS files.

These must be set as paths, URLs, or custom objects.

@register("calendar")
class Calendar(Component):
    class Media:
        js = [
            "https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js",
            "calendar/script.js",
        ]
        css = [
            "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css",
            "calendar/style.css",
        ]

Note

django-component's management of files is inspired by Django's Media class.

To be familiar with how Django handles static files, we recommend reading also:

Media class¤

Use the Media class to define secondary JS / CSS files for a component.

This Media class behaves similarly to Django's Media class:

  • Static paths - Paths are handled as static file paths, and are resolved to URLs with Django's {% static %} template tag.
  • URLs - A path that starts with http, https, or / is considered a URL. URLs are NOT resolved with {% static %}.
  • HTML tags - Both static paths and URLs are rendered to <script> and <link> HTML tags with media_class.render_js() and media_class.render_css().
  • Bypass formatting - A SafeString, or a function (with __html__ method) is considered an already-formatted HTML tag, skipping both static file resolution and rendering with media_class.render_js() or media_class.render_css().
  • Inheritance - You can set extend to configure whether to inherit JS / CSS from parent components. See Media inheritance.

However, there's a few differences from Django's Media class:

  1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list, or (CSS-only) a dictonary (See ComponentMediaInput).
  2. Individual JS / CSS files can be any of str, bytes, Path, SafeString, or a function (See ComponentMediaInputPath).
  3. Individual JS / CSS files can be glob patterns, e.g. *.js or styles/**/*.css.
  4. If you set Media.extend to a list, it should be a list of Component classes.
from components.layout import LayoutComp

class MyTable(Component):
    class Media:
        js = [
            "path/to/script.js",
            "path/to/*.js",  # Or as a glob
            "https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js",  # AlpineJS
        ]
        css = {
            "all": [
                "path/to/style.css",
                "path/to/*.css",  # Or as a glob
                "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css",  # TailwindCSS
            ],
            "print": ["path/to/style2.css"],
        }

        # Reuse JS / CSS from LayoutComp
        extend = [
            LayoutComp,
        ]

CSS media types¤

You can define which stylesheets will be associated with which CSS media types. You do so by defining CSS files as a dictionary.

See the corresponding Django Documentation.

Again, you can set either a single file or a list of files per media type:

class MyComponent(Component):
    class Media:
        css = {
            "all": "path/to/style1.css",
            "print": ["path/to/style2.css", "path/to/style3.css"],
        }

Which will render the following HTML:

<link href="/static/path/to/style1.css" media="all" rel="stylesheet">
<link href="/static/path/to/style2.css" media="print" rel="stylesheet">
<link href="/static/path/to/style3.css" media="print" rel="stylesheet">

Note

When you define CSS as a string or a list, the all media type is implied.

So these two examples are the same:

class MyComponent(Component):
    class Media:
        css = "path/to/style1.css"
class MyComponent(Component):
    class Media:
        css = {
            "all": ["path/to/style1.css"],
        }

Media inheritance¤

By default, the media files are inherited from the parent component.

class ParentComponent(Component):
    class Media:
        js = ["parent.js"]

class MyComponent(ParentComponent):
    class Media:
        js = ["script.js"]

print(MyComponent.media._js)  # ["parent.js", "script.js"]

You can set the component NOT to inherit from the parent component by setting the extend attribute to False:

class ParentComponent(Component):
    class Media:
        js = ["parent.js"]

class MyComponent(ParentComponent):
    class Media:
        extend = False  # Don't inherit parent media
        js = ["script.js"]

print(MyComponent.media._js)  # ["script.js"]

Alternatively, you can specify which components to inherit from. In such case, the media files are inherited ONLY from the specified components, and NOT from the original parent components:

class ParentComponent(Component):
    class Media:
        js = ["parent.js"]

class MyComponent(ParentComponent):
    class Media:
        # Only inherit from these, ignoring the files from the parent
        extend = [OtherComponent1, OtherComponent2]

        js = ["script.js"]

print(MyComponent.media._js)  # ["script.js", "other1.js", "other2.js"]

Info

The extend behaves consistently with Django's Media class, with one exception:

  • When you set extend to a list, the list is expected to contain Component classes (or other classes that have a nested Media class).

Accessing Media files¤

To access the files that you defined under Component.Media, use Component.media (lowercase).

This is consistent behavior with Django's Media class.

class MyComponent(Component):
    class Media:
        js = "path/to/script.js"
        css = "path/to/style.css"

print(MyComponent.media)
# Output:
# <script src="/static/path/to/script.js"></script>
# <link href="/static/path/to/style.css" media="all" rel="stylesheet">

When working with component media files, it is important to understand the difference:

  • Component.Media

    • Is the "raw" media definition, or the input, which holds only the component's own media definition
    • This class is NOT instantiated, it merely holds the JS / CSS files.
  • Component.media

    • Returns all resolved media files, including those inherited from parent components
    • Is an instance of Component.media_class
class ParentComponent(Component):
    class Media:
        js = ["parent.js"]

class ChildComponent(ParentComponent):
    class Media:
        js = ["child.js"]

# Access only this component's media
print(ChildComponent.Media.js)  # ["child.js"]

# Access all inherited media
print(ChildComponent.media._js)  # ["parent.js", "child.js"]

Note

You should not manually modify Component.media or Component.Media after the component has been resolved, as this may lead to unexpected behavior.

If you want to modify the class that is instantiated for Component.media, you can configure Component.media_class (See example).

File paths¤

Unlike the main HTML / JS / CSS files, the path definition for the secondary files are quite ergonomic.

Relative to component¤

As seen in the getting started example, to associate HTML / JS / CSS files with a component, you can set them as Component.template_file, Component.js_file and Component.css_file respectively:

[project root]/components/calendar/calendar.py
from django_components import Component, register

@register("calendar")
class Calendar(Component):
    template_file = "template.html"
    css_file = "style.css"
    js_file = "script.js"

In the example above, we defined the files relative to the directory where the component file is defined.

Alternatively, you can specify the file paths relative to the directories set in COMPONENTS.dirs or COMPONENTS.app_dirs.

If you specify the paths relative to component's directory, django-componenents does the conversion automatically for you.

Thus, assuming that COMPONENTS.dirs contains path [project root]/components, the example above is the same as writing:

[project root]/components/calendar/calendar.py
from django_components import Component, register

@register("calendar")
class Calendar(Component):
    template_file = "calendar/template.html"
    css_file = "calendar/style.css"
    js_file = "calendar/script.js"

Important

File path resolution in-depth

At component class creation, django-components checks all file paths defined on the component (e.g. Component.template_file).

For each file path, it checks if the file path is relative to the component's directory. And such file exists, the component's file path is re-written to be defined relative to a first matching directory in COMPONENTS.dirs or COMPONENTS.app_dirs.

Example:

[root]/components/mytable/mytable.py
class MyTable(Component):
    template_file = "mytable.html"
  1. Component MyTable is defined in file [root]/components/mytable/mytable.py.
  2. The component's directory is thus [root]/components/mytable/.
  3. Because MyTable.template_file is mytable.html, django-components tries to resolve it as [root]/components/mytable/mytable.html.
  4. django-components checks the filesystem. If there's no such file, nothing happens.
  5. If there IS such file, django-components tries to rewrite the path.
  6. django-components searches COMPONENTS.dirs and COMPONENTS.app_dirs for a first directory that contains [root]/components/mytable/mytable.html.
  7. It comes across [root]/components/, which DOES contain the path to mytable.html.
  8. Thus, it rewrites template_file from mytable.html to mytable/mytable.html.

NOTE: In case of ambiguity, the preference goes to resolving the files relative to the component's directory.

Globs¤

Components can have many secondary files. To simplify their declaration, you can use globs.

Globs MUST be relative to the component's directory.

[project root]/components/calendar/calendar.py
from django_components import Component, register

@register("calendar")
class Calendar(Component):
    class Media:
        js = [
            "path/to/*.js",
            "another/path/*.js",
        ]
        css = "*.css"

How this works is that django-components will detect that the path is a glob, and will try to resolve all files matching the glob pattern relative to the component's directory.

After that, the file paths are handled the same way as if you defined them explicitly.

Supported types¤

File paths can be any of:

  • str
  • bytes
  • PathLike (__fspath__ method)
  • SafeData (__html__ method)
  • Callable that returns any of the above, evaluated at class creation (__new__)

To help with typing the union, use ComponentMediaInputPath.

from pathlib import Path

from django.utils.safestring import mark_safe

class SimpleComponent(Component):
    class Media:
        css = [
            mark_safe('<link href="/static/calendar/style1.css" rel="stylesheet" />'),
            Path("calendar/style1.css"),
            "calendar/style2.css",
            b"calendar/style3.css",
            lambda: "calendar/style4.css",
        ]
        js = [
            mark_safe('<script src="/static/calendar/script1.js"></script>'),
            Path("calendar/script1.js"),
            "calendar/script2.js",
            b"calendar/script3.js",
            lambda: "calendar/script4.js",
        ]

Paths as objects¤

In the example above, you can see that when we used Django's mark_safe() to mark a string as a SafeString, we had to define the URL / path as an HTML <script>/<link> elements.

This is an extension of Django's Paths as objects feature, where "safe" strings are taken as is, and are accessed only at render time.

Because of that, the paths defined as "safe" strings are NEVER resolved, neither relative to component's directory, nor relative to COMPONENTS.dirs. It is assumed that you will define the full <script>/<link> tag with the correct URL / path.

"Safe" strings can be used to lazily resolve a path, or to customize the <script> or <link> tag for individual paths:

In the example below, we make use of "safe" strings to add type="module" to the script tag that will fetch calendar/script2.js. In this case, we implemented a "safe" string by defining a __html__ method.

# Path object
class ModuleJsPath:
    def __init__(self, static_path: str) -> None:
        self.static_path = static_path

    # Lazily resolve the path
    def __html__(self):
        full_path = static(self.static_path)
        return format_html(
            f'<script type="module" src="{full_path}"></script>'
        )

@register("calendar")
class Calendar(Component):
    template_file = "calendar/template.html"

    class Media:
        css = "calendar/style1.css"
        js = [
            # <script> tag constructed by Media class
            "calendar/script1.js",
            # Custom <script> tag
            ModuleJsPath("calendar/script2.js"),
        ]

Rendering paths¤

As part of the rendering process, the secondary JS / CSS files are resolved and rendered into <link> and <script> HTML tags, so they can be inserted into the render.

In the Paths as objects section, we saw that we can use that to selectively change how the HTML tags are constructed.

However, if you need to change how ALL CSS and JS files are rendered for a given component, you can provide your own subclass of Django's Media class to the Component.media_class attribute.

To change how the tags are constructed, you can override the Media.render_js() and Media.render_css() methods:

from django.forms.widgets import Media
from django_components import Component, register

class MyMedia(Media):
    # Same as original Media.render_js, except
    # the `<script>` tag has also `type="module"`
    def render_js(self):
        tags = []
        for path in self._js:
            if hasattr(path, "__html__"):
                tag = path.__html__()
            else:
                tag = format_html(
                    '<script type="module" src="{}"></script>',
                    self.absolute_path(path)
                )
        return tags

@register("calendar")
class Calendar(Component):
    template_file = "calendar/template.html"
    css_file = "calendar/style.css"
    js_file = "calendar/script.js"

    class Media:
        css = "calendar/style1.css"
        js = "calendar/script2.js"

    # Override the behavior of Media class
    media_class = MyMedia