Django example: creating a custom form field widget

I’m back with another Django mini tutorial! This is something that probably a lot of people already know how to do, but since there’s no official documentation for creating custom Django form field widgets, I thought I’d write a post about the information I pieced together in my research. Most of what I learned about creating these widgets came from inspecting GitHub repositories from others who have done this before – so I’m no expert and I’m making a lot of assumptions. Nevertheless, something here might be useful to someone else!

For the project I’m working on I wanted to have a form field to write a short essay for an application. The essay could have a minimum word count, a maximum word count, both or neither. In the help text for the form field I displayed the essay length requirements, but I also wanted to display a word count and indicate if the current length was within the length limits. The resulting control I created looked something like this:

An example screenshot of the django countable field widget

In this mini tutorial I’m going to walk through the steps that I used to create this custom widget.

Let’s start by creating the widget app. I started by creating a directory in my project root named countable_field containing only 2 files: __init__.py and widgets.py.  To make sure things are working, let’s start off simple, by creating a widget that just uses the basic functionality of the django textarea with no additional functionality. In widgets.py, create a new class called CountableWidget that inherits from widgets.Textarea:

from django.forms import widgets

class CountableWidget(widgets.Textarea):
    pass

Now let’s test it out by using it in a form field. First, don’t forget to add the app to INSTALLED_APPS in your settings file. Next in your form file, import the widget you just created:

from countable_field.widgets import CountableWidget

Now, on the form field you want to use, set the widget type to the new countable field:

self.fields['essay_response'].label = requirements.essay_prompt
self.fields['essay_response'].widget = CountableWidget()
# Make the help text state the length requirements
if requirements.essay_min_length and requirements.essay_max_length:
    self.fields['essay_response'].help_text = 
        "Must be between {} and {} words long.".format(requirements.essay_min_length, requirements.essay_max_length)
elif requirements.essay_min_length:
    self.fields['essay_response'].help_text = 
        "Must be at least {} words long.".format(requirements.essay_min_length)
elif requirements.essay_max_length:
    self.fields['essay_response'].help_text = 
        "Must be no more than {} words long.".format(requirements.essay_max_length)

With this code you should see a standard textarea field similar to the one in the screenshot above but without the word count field. Great! Now let’s do the fun part and add the word count.

Instead of writing my own javascript to count words, I took advantage of the open source Countable.js javascript library. This excellent little script can count paragraphs, words or characters in a field. Download the Countable.js file from their GitHub and add it to your countable_field directory under the sub directories static/countable_field/js. We’re also going to need another JavaScript file a little later, so go ahead and create a file called countable-field.js in the same directory. Next, define the JavaScript files as a required asset using the Media class. Your CountableWidget class should look like this:

class CountableWidget(widgets.Textarea):
    class Media:
        js = (
            'countable_field/js/countable.js',
            'countable_field/js/countable-field.js'
        )

Now we need to add the word count placeholder underneath the field. Start by declaring the render method in your CountableField widget:

class CountableWidget(widgets.Textarea):
    # ...
    def render(self, name, value, attrs=None):
        final_attrs = self.build_attrs(self.attrs, attrs)

Let’s take a second to look at what this means. name and value are required parameters that are used by the widgets base class for rendering. We won’t be manipulating them in this widget, but we will need to pass them on to the base class.

attrs is a dictionary of additional optional attributes that we will be using to pass in the minimum and maximum essay length. It could be used to communicate any parameters you need to pass from the form to your custom widget. In order to get a final dictionary of all of the attributes, including base attributes, call the build_attrs method, passing in the attrs that were included in the call to your custom widget.

Now we’ll build the template for the word count text to display under the form field. In your render method, add this piece of code:

final_attrs['data-count'] = 'words'
output = super(CountableWidget, self).render(name, value, final_attrs)
output += """<span class="text-count" id="%(id)s_counter">Word count: <span class="text-count-current">0</span></span>""" 
          % {'id': final_attrs.get('id'),
             'min_count': final_attrs.get('text_count_min' or 'false'),
             'max_count': final_attrs.get('text_count_max' or 'false')}

In this piece of code we’re first adding the data-count attribute to the input, which will be used by our JavaScript to identify inputs where we’re counting words. Next we call the base class to get its normal rendered code. Be sure to pass it the name, value, and final_attrs attributes! Then we add the HTML to create the word count, including an important ID attribute of [field id]_counter, which we’ll use in our JavaScript to make sure we’re setting the word count on the right field.

Now we need to create the JavaScript code to update our word count. Go into the countable-field.js file that we created earlier and add this code:

(function() {
    function CountableField(textarea) {
        var countDisplay = document.getElementById(textarea.id + "_counter");
        var countDown = false;
        var minCount, maxCount;
        if (textarea != null && countDisplay != null) {
            minCount = textarea.getAttribute("data-min-count");
            maxCount = textarea.getAttribute("data-max-count");

            Countable.on(textarea, updateFieldWordCount);
        }

        function updateFieldWordCount(counter) {
            var count = counter["words"];
            countDisplay.getElementsByClassName("text-count-current")[0].innerHTML = count;
            if (minCount && count  maxCount)
                countDisplay.className = "text-count text-is-over-max";
            else
                countDisplay.className = "text-count";
        }
    }

    document.addEventListener('DOMContentLoaded', function(e) {
        ;[].forEach.call(document.querySelectorAll('[data-count]'), CountableField)
    })
})()

In this code we’re passing the form field to Countable using its on() function so that it will keep track of the words entered in the form field. Using the callback function we’re placing the current word count in the word count area, and we’re also assigning a CSS to class to indicate when the user’s essay is out of the length requirement bounds.

Now that we have the right render code, we need to make sure we pass the min and max length variables from our form to our widget. Go back to your forms.py file and edit your call to the widget to pass in those variables.

self.fields['essay_response'].widget = 
    CountableWidget(attrs={'data-min-count': requirements.essay_min_length,
                           'data-max-count': requirements.essay_max_length})

Note that these attributes will be added to the textarea, so it’s a good idea to name them data-[attribute name] to comply with w3c standards.

Last we just need to add a little bit of CSS. This snippet will make sure the word count displays to the right of the field and that the text will be red when when the word count is outside the bounds of the requirements. Add this to your project’s CSS file:

.text-count {
  float: right;
  font-size: 80%;
  color: #818a91; 
}
.text-count.text-is-under-min, 
.text-count.text-is-over-max {
  color: #d9534f; 
}

You should have a beautiful new text field, including a word count and styles to help you see when you’re in or out of the required length limits!

Note: if you use Crispy forms the CSS and JavaScript from your custom widget will automatically be included in the form for you; otherwise you will need to include them yourself using {{ form.media }} in your template.

Demo of the countable field functionality

I hope this tutorial was helpful to someone! If you have any questions or suggestions please drop them in the comments. All the code from this example is available in my django-countable-field GitHub repo.

To learn more about custom form field widgets the first place I suggest looking at the django source code. Take a look at the way they create their base widgets and copy as much as you can! Also check out some other open source custom widgets out there. A couple that I stole borrowed from are smart selects, a widget for cascading dropdowns, and s3direct, a widget for uploading and downloading files from AWS.

This post was updated on July 21, 2018 to reflect changes in Django and the Countable library.

8 thoughts on “Django example: creating a custom form field widget

  1. Really helpful, thanks. Btw, “Countable.live” seems to have been changed to “Countable.on” in Countable.js latest version, you might want to update it.

    Like

    1. Thanks for reading and for letting me know! I’ve updated the post with that change, and a few other changes as well.

      Like

  2. Hello. This was very helpful. Thank you. But I can’t seem to make anything work unless I put the tag {{ form.media }} in my template. I found {{ form.media }} mentioned on sites like StackOverflow but I can’t find in anywhere in the Django project docs. Is there some configuration setting that keeps you from having to put that in?

    Like

    1. Thanks for pointing this out, Benjamin. I use Crispy forms, which handle the media assets for you. I didn’t realize that you would need to include {{ form.media }} if you’re not using crispy forms – I’ll add a note about that, thank you!

      Like

Leave a Reply to Andrea Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s