Media Path as Objects

Hey there!

As stated #34864 that is a duplicate of #29490 people wanted to be able to add more functionality specially to the js attribute of the Media class on forms/widgets. And i think that this was a great addition on 4.1.

The documentation on this addition was really narrow and didn’t cover a “useful” use case and also contains a invalid attribute to the script tag (rel is not valid for a script tag). So i wonder if i should open a ticket to state this problem?

And another thing is that i still think that we could perhaps have a more simple way for people wanting this behavior. I myself just needed this feature, and had to implement it on my own, just to add a defer attribute to a script tag. If the said ticket is open i would be happy to had a more “useful” example, or even, add this to the django project itself. Below is my initial implementation of a custom Js Path that is “smarter” and more flexible.

# python 3.11
from django.utils.html import html_safe
from django.templatetags.static import static


@html_safe
class JsScriptMedia:
    """An object that can be used in Media classes to define additional attributes to the script tag"""

    def __init__(
        self,
        src: str,
        *,
        async_: bool = False,
        defer: bool = False,
        cross_origin: str | None = None,
        integrity: str | None = None,
        type: str | None = None,
    ):
        if src.startswith(("http://", "https://", "/")):
            self.src = src
        else:
            self.src = static(src)
        self.async_ = async_
        self.defer = defer
        self.cross_origin = cross_origin
        self.integrity = integrity
        self.type = type

        self.attrs = []
        named_attr_values = {
            "src": self.src,
            "crossorigin": self.cross_origin,
            "integrity": self.integrity,
            "type": self.type,
        }
        for attr, value in named_attr_values.items():
            if value is not None:
                self.attrs.append(f'{attr}="{value}"')

        bool_attr_values = {
            "defer": self.defer,
            "async": self.async_,
        }
        for attr, value in bool_attr_values.items():
            if value:
                self.attrs.append(attr)

    def __str__(self):
        return f'<script src="{self.src}" {" ".join(self.attrs)}></script>'

Edit:
Almost forgot, here’s how this class can be used:


from django import forms

class SomeForm(forms.Form):
  class Media:
    js = [JsScriptMedia("https://code.jquery.com/jquery-3.3.1.min.js", defer=True)]

I believe that the above snippet may also need some improvements if added to the Django codebase, like validating the crossorigin possible values.

Again, thanks for adding the feature that allowed this behavior.
Cheers!

Hi @leandrodesouzadev.

If example in the docs is invalid, we should definitely fix that.

Then maybe adding a concise but more useful example would be useful.

The exact example here seems a bit long/specific… :thinking: Maybe just having it take **kwargs that end up as attributes, to keep it a bit more schematic? :thinking: — I guess that would be something to settle in review.

So, opening a ticket to improve the example seems valid. (Exact scope to be decided)

Hey there @carltongibson, thanks for the reply.

I will open up a ticket then.

I agree, it ended up by way to specific. I believe then that using kwargs would lead to a more narrow example. But we still would need to make that a True boolean flag value got rendered differently: only rendering the attribute name (for the async and defer tags). Maybe this is more simple and generic, yet produces the same result:

# python 3.11
from django.utils.html import html_safe
from django.templatetags.static import static


@html_safe
class InlineMediaPath:
    """An object that can be used in Media classes to define additional attributes to css/js"""

    def __init__(self, tag: str, **kwargs):
        self.tag = tag
        self.attrs = []
        for attr, value in kwargs.items():
            if value is True:
                # Only render the attr name, like: `async` and `defer`
                self.attrs.append(attr)
                continue
            if attr in ("src", "href"):
                # Get the absolute URL for this media path
                url = value if value.startswith(("http://", "https://", "/")) else static(src)
                self.attrs.append(f'{attr}="{url}"')
                continue
            self.attrs.append(f'{attr}="{value}"')
            

    def __str__(self):
        return f'<{self.tag} {" ".join(self.attrs)}></{self.tag}>'

And i forgot to mention the usage, output of this class:

class SomeForm(forms.Form):
    class Media:
        css = {"all": [InlineMediaPath(tag="link", href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css", crossorigin="anonymous", integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH")]}
         js = (InlineMediaPath(tag="script", src="https://unpkg.com/htmx.org@1.9.10"), InlineMediaPath(tag="script", src="https://unpkg.com/alpinejs", defer=True), InlineMediaPath(tag="script", src="polls/js/add_poll.js", **{"async": True})

Would output:

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" media="all">

<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://unpkg.com/alpinejs" defer></script>
<script src="/static/polls/js/add_poll.js" defer></script>

Just opened the ticket: #35261

1 Like