javascriptjquerypythondjangodjango-custom-field

Adding Javascript to Custom widgets


This question is related to

Django: Best Way to Add Javascript to Custom Widgets

But is not the same.

The original question asks how to add supporting javascript to a custom django widget, and the answer is to use forms.Media, but that solution does not work for me. My example is this:

The widget, when rendered in a form, creates a line which looks like (toy example) this:

<div id="some-generated-id">Text here</div>

Now, what I also want to add to the output is another line looking like this:

<script>
$('#some-generated-id').datetimepicker(some-generated-options)
</script>

The initial idea is that when the widget is rendered, both the div and script get rendered, but that does not work. The problem is that the structure of the html document looks like:

-body
    - my widget
    - my widget's javascript
-script
    -calls to static files (jQuery, datetimepicker,...)

At the time the widget's javascript code is loaded in the browser, jQuery and datetimepicker js files have not yet been loaded (they load at the end of the document).

I cannot do this using Media, since the options and id I generate are vital to the function. What is the best way to solve this?


Solution

  • From the docs:

    The order in which assets are inserted into the DOM is often important. For example, you may have a script that depends on jQuery. Therefore, combining Media objects attempts to preserve the relative order in which assets are defined in each Media class.

    Consider this example:

    class FooWidget(forms.TextInput):
        class Media:
            js = ('foo.js',)
    
    class BarWidget(forms.TextInput):
        class Media:
            js = ('bar.js',)
    
    class SomeForm(forms.Form):
        field1 = forms.CharField(widget=BarWidget)
        field2 = forms.CharField(widget=FooWidget)
    
        def __init__(self, *args, **kwargs):
            super(SearchForm, self).__init__(*args, **kwargs)
    

    Now when you call form.media, the scripts will render like this:

    <script type="text/javascript" src="/static/bar.js"></script>
    <script type="text/javascript" src="/static/foo.js"></script>
    

    Why does bar.js render before foo.js? Because django renders them based on the order they were called on in the form, not the order that the classes were defined in. If you want to change the order in this example, simply swap the position field1 and field2 in SomeForm.

    How does this help you with jQuery? You can render your jQuery CDN script via your custom widget:

    class FooWidget(forms.TextInput):
        class Media:
            js = ('https://code.jquery.com/jquery-3.4.1.js', 'foo.js',)
    
    class BarWidget(forms.TextInput):
        class Media:
            js = ('https://code.jquery.com/jquery-3.4.1.js', 'bar.js',)
    

    Now your form.media will look like this:

    <script src="https://code.jquery.com/jquery-3.4.1.js"></script>
    <script type="text/javascript" src="/static/bar.js"></script>
    <script type="text/javascript" src="/static/foo.js"></script>
    

    Notice how /static/ wasn't appended to the jQuery CDN? This is because the .media attribute checks whether the given filepaths contain http or https, and only appends your STATIC_URL setting to filepaths that are relative.

    Also note that duplicate file names are automatically removed, so I would say it's good practice to include a https://code.jquery.com/jquery-3.4.1.js at the beginning of every widget that requires it. That way, no matter what order you render them in, the jQuery script will always appear before files that need it.

    On a side note, I would be careful when including numbers in your filenames. As Django 2.2 there appears to be a bug when trying to order the scripts.

    For example:

    class FooWidget(forms.TextInput):
        class Media:
            js = ('foo1.js', 'foo2.js',)
    
    class BarWidget(forms.TextInput):
        class Media:
            js = ('bar1.js', 'bar13.js',)
    
    class SomeForm(forms.Form):
        field1 = forms.CharField(widget=BarWidget)
        field2 = forms.CharField(widget=FooWidget)
    
        def __init__(self, *args, **kwargs):
            super(SearchForm, self).__init__(*args, **kwargs)
    

    Will look like:

    <script type="text/javascript" src="/static/bar1.js"></script>
    <script type="text/javascript" src="/static/foo1.js"></script>
    <script type="text/javascript" src="/static/bar13.js"></script>
    <script type="text/javascript" src="/static/foo2.js"></script>
    

    I've tried various combinations of names containing numbers, and I can't follow the logic, so I assume this is a bug.