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(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.

Note: This example was written using django 1.9. The signature of build_attrs has since been updated. It now expects both the base attributes and the additional attributes to be passed in. Now you should call it like this: final_attrs = self.build_attrs(self.attrs, attrs).

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:

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 calling the base class to get its normal rendered code. Be sure to pass it the name, value, and final_attrs attributes! Next 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 let’s add a little bit of javascript. Add this to your render method:

js = """
<script type="text/javascript">
    var countableField = new CountableField("%(id)s")
</script>
""" % {'id': final_attrs.get('id')}

Now we need to create the JavaScript code that’s being called here. Go into the countable-field.js file that we created earlier and add this code:

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

        Countable.live(textarea, updateFieldWordCount);
    }

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

In this code we’re passing the form field to Countable using its live() 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!

A demonstration of the django countable field

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.

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