django-components/docs/concepts/fundamentals/defining_js_css_html_files.md
Juro Oravec 715bf7d447
feat: allow to set main JS and CSS from files + lazy-load component m… (#870)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-30 18:00:46 +01:00

7.5 KiB

title weight
Defining HTML / JS / CSS files 8

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:

Defining file paths relative to component or static dirs

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

# In a file [project root]/components/calendar/calendar.py
from django_components import Component, register

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

In the example above, the files are defined relative to the directory where component.py is.

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

Assuming that COMPONENTS.dirs contains path [project root]/components, we can rewrite the example as:

# In a file [project root]/components/calendar/calendar.py
from django_components import Component, register

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

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

Defining multiple paths

Each component can have only a single template, and single main JS and CSS. However, you can define additional JS or CSS using the nested Media class.

This Media class behaves similarly to Django's Media class, with a few differences:

  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 below)
  2. Individual JS / CSS files can be any of str, bytes, Path, SafeString, or a function.
  3. Our Media class does NOT support Django's extend keyword
class MyComponent(Component):
    class Media:
        js = ["path/to/script1.js", "path/to/script2.js"]
        css = ["path/to/style1.css", "path/to/style2.css"]

Configuring 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",
        }
class MyComponent(Component):
    class Media:
        css = {
            "all": ["path/to/style1.css", "path/to/style2.css"],
            "print": ["path/to/style3.css", "path/to/style4.css"],
        }

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

Supported types for file paths

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__)
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",
        ]

Path as objects

In the example above, you could see that when we used mark_safe to mark a string as a SafeString, we had to define the full <script>/<link> tag.

This is an extension of Django's Paths as objects feature, where "safe" strings are taken as is, and 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.

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

class LazyJsPath:
    def __init__(self, static_path: str) -> None:
        self.static_path = static_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_name = "calendar/template.html"

    def get_context_data(self, date):
        return {
            "date": date,
        }

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

Customize how paths are rendered into HTML tags with media_class

Sometimes you may need to change how all CSS <link> or JS <script> tags are rendered for a given component. You can achieve this by providing your own subclass of Django's Media class to component's media_class attribute.

Normally, the JS and CSS paths are passed to Media class, which decides how the paths are resolved and how the <link> and <script> tags are constructed.

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_name = "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

NOTE: The instance of the Media class (or it's subclass) is available under Component.media after the class creation (__new__).