Rejuvenating vs deprecating Form.Media

Apologies, this is going to be long. There is an accepted ticket from 9 years ago, about renaming or removing Form.Media.

I think it’s worth revisiting this issue, and ideally pushing it forward: Media feels like a rather sad and neglected part of Django at the moment. It’s not part of all the recent template-based form improvements, as far as I can tell. It’s fairly complex for what it tries to do, especially with the magic inheritance by way of metaprogramming: if you inherit from a class that has a class Media, and your new inherited class also has a class Media, the two get magically merged (which is the only reason all Widget subclasses have a metaclass, incidentally).

The feature has also not kept up with changes in front-end development. There is no way to define that a provided script should be loaded with async/defer, which is a very common use case. We don’t support nonces or integrity attributes for security. On the docs side, we list deprecated media types like tty, tv aural etc, some of which have been deprecated for a while, which also highlights how odd it is that this part of Django should care about the W3C’s media query updates. (Side note: this is really only a docs issue, as Django does not validate those keys.)

I wish we could get rid of it entirely, but I suspect we really should keep it or something like it around for the admin (which means keeping around all the merge logic in metaclasses etc, which isn’t ideal, but at least a compromise?). The whole thing feels just very out of place in Django – I can see that it’s a janky-but-useful way of extending the admin, but for regular form.

To play devil’s advocate: The feature is not entirely unused – GitHub finds 8.7k results, 4.7k of which appear in some sort of admin. It’s also in django-CSM, though I didn’t look into it in detail to see how recommended or used it is there.

As far as I can see, sensible options are:

  • Deprecate and remove Media entirely and have some replacement in admin to allow people to add scripts and styles
  • Deprecated and remove Media from Form, but keep it in admin sites (not much maintenance advantages here)
  • Add {{ form.media }} to default form templates – and maybe change it so that we support things like defer and async in the js attribute.

I’m posting this here in the hopes of hearing about general use of Form.Media and maybe finding a consensus on how to move forward.

2 Likes

The Wagtail maintainers will probably want to chime in here since Wagtail seems to be a heavy user of forms.Media. I don’t use Wagtail myself but I like using forms.Media to add CSS and JavaScript to the frontend. In the past I have used it (a lot) for websites, not just for the admin, but these days it’s mostly related to the admin.

django-js-asset is linked in the Trac ticket as well. Adding additional attributes such as defer, async or even arbitrary attributes is relatively easy with it. It is being used in several third party packages such as django-mptt, django-ckeditor etc. to ship configuration to the JavaScript code without having to override or add any HTML templates; this made it much easier to support multiple versions of Django in the past. Maybe a widget template would be the way to go these days, but I’m not sure if that would be so much better.

I don’t have a better idea to contribute right now. I’m quite happy with the status quo and the js-asset hack, but I’ll certainly keep an eye on this thread to see what a better future might look like.

1 Like

Thank you for brining that up – I was aware only of django-cms, but if wagtail use it too, then I think that’s a further good argument against deprecation or limiting it to the admin entirely.

What I’d mostly like to fix is how Form.Media feels neglected and out of step with current Django and current web features in general. I do think including {{ form.media }} in form templates by default and supporting added attibutes for scripts would be enough to bring it back into cohesion (as I think using a third-party package just to get defer and async in is really really not great if we’re committed to having Media stay!). Changed post title to reflect this!

Does anybody else have opinions on default-including {{ form.media }}, and allowing scripts to be an iterable of dicts rather than just strings, in order to provide more rendering arguments?

Hi all :wave:

I posted a rather long and rambling post on the mailing list a while back about this ticket. This dates back when I first started contributing. More import than my email was that it got a few thoughts from more knowledgeable folk.

https://groups.google.com/g/django-developers/c/Hjci3SyDx9k/m/Dd5qTXSTAwAJ

Although that was a fair amount of time ago now and I’m not sure it clearly addresses the issues brought up here.

2 Likes

For what it’s worth we are trying to push ahead with CSP compliance and also easier client-side code customisations (in the admin) for those building with Wagtail.

The form / widget media is a really nice approach for providing scripts and their associated data attributes in the same area of code. An example is a Django field widget that adds Stimulus data attributes and the relevant script needed. This approach is convenient, builds on Wagtail code primatives and does not require any template driven dynamic inline scripts.

However, providing an ergonomic way to go further with nonce attributes and even module scripts would be really appreciated.

This means we can provide documentation for an easy way to add custom JavaScript widgets, integrate with existing ones and also make the code easily CSP compliant plus performant.

See some of the related Wagtail references.
CSP compatibility issues · Issue #1288 · wagtail/wagtail · GitHub - CSP
rfcs/078-adopt-stimulus-js.md at main · wagtail/rfcs · GitHub (RFC for Stimulus usage, and CSP / customisation goals).

I’m on the core team with Wagtail but maybe some other core team members would have some additional context to add.

1 Like

Btw, feincms uses it too but that’s a given since django-js-asset was extracted from it.

The implementation of forms.Media has been cleaned up a bit by Claude Paroz in Fixed #29490 -- Added support for object-based Media CSS and JS paths. · django/django@4c76ffc · GitHub , if the JS object has a __html__ method it’s being called. So, a dict based approach would be strictly worse IMO than what we have right now.

I came here to link to this :blush:

I’m pretty sure @claudep has thoughts in this area too (though my searching for them this morning has failed me :thinking:)

@mathiask mentioned the fix for #29490. You can see in the test part of that commit the idea behind using object-based assets, so you should be able to add async/defer stuff rather easily with this approach. Pushing something like that in the core forms code was my hope at some point, but it’s hard to convince the dev community (it was argued that this might be better left to 3rd-parties).

Just adding my two cents as another contributor to Wagtail – I’d agree it feels like an API that’s not kept up with front-end development. Both in terms of standards (async scripts, CSPs, ES modules, web components) but also practices (dependency management, transpiling, module bundling).

The API works well when loading standalone widgets (for example Wagtail’s Handsontable integration), doesn’t work well when a suite of widgets is implemented with shared code.

For CSS in Wagtail, we’ve only kept Media where widgets have very specific styles. Almost all widgets are built on a shared set of UI components (some form widgets, some more broadly reusable), so the forms-only Media paradigm isn’t a good fit.

For JS – we’re using Media more, but I’d guess mostly due to inertia. We’d ideally break up our JS code into more granular files, but it’s a lot of work to refactor this for modern practices. The best results for this with our current tooling would be code splitting and dynamic imports, which we could combine with Media to some extent but the value isn’t super clear to me – we could just as well do this on a much more granular level without using the Media API at all.


Back to what to do with it – my main recommendation would be to rejuvenate it with modern standards and practices in mind, at least documenting how to use it in a way that makes sense with:

  • script type="module" and dynamic module imports (import() in JS)
  • Import maps (perhaps doing some kind of imports map merging?)
  • async and defer attributes
  • nonce and integrity attributes

So again with Wagtail’s Handsontable integration in mind, currently the Media API helps us produce the equivalent of:

<!-- {{ form.media.css }} output -->
<link href="/static/table_block/css/vendor/handsontable.css" rel="stylesheet">
<!-- {{ form.media.js }} output -->
<script src="/static/table_block/js/vendor/handsontable.js"></script>
<script src="/static/table_block/js/table.js"></script>


[…]
<!-- {{ form.my_table_field_1 }} output -->
<input type="hidden" id="id_my_table_field_1" value="[…]">
[…]
<!-- {{ form.my_table_field_36 }} output -->
<input type="hidden" id="id_my_table_field_36" value="[…]">

Instead we’d want to use ES modules with the relevant attributes and import maps, so our table.js script can do its own loading of Handsontable (and any other dependenceis) only as needed with import():

<!-- {{ form.media.css }} output -->
<link href="/static/table_block/css/vendor/handsontable.css" rel="stylesheet">
<!-- {{ form.media.importmaps }} output -->
<script type="importmap">
{
  "imports": {
    "handsontable": "/static/vendor/handsontable.js"
  }
}
</script>
<!-- {{ form.media.js }} output -->
<script async type="module" nonce="nonce-[…]" src="/static/table.js"></script>

[…]
<!-- {{ form.my_table_field_1 }} output -->
<input type="hidden" id="id_my_table_field_1" value="[…]">
[…]
<!-- {{ form.my_table_field_36 }} output -->
<input type="hidden" id="id_my_table_field_36" value="[…]">

And as a bonus to go further, add Web Components to the mix to guarantee styles are encapsulated:

<!-- {{ form.media.templates }} output -->
<template id="handsontable_widget">
  <link href="/static/vendor/handsontable.css" rel="stylesheet">
  <input type="hidden" value="[…]">
</template>
<!-- {{ form.media.importmaps }} output -->
<script type="importmap">
{
  "imports": {
    "handsontable": "/static/vendor/handsontable.js"
  }
}
</script>
<!-- {{ form.media.js }} output -->
<script async type="module" nonce="nonce-R4nd0m" src="/static/table.js"></script>

[…]
<!-- {{ form.my_table_field_1 }} output -->
<handsontable-input id="id_my_table_field_1" value="[…]"></handsontable-input>
[…]
<!-- {{ form.my_table_field_36 }} output -->
<handsontable-input id="id_my_table_field_36" value="[…]"></handsontable-input>
2 Likes