Creating Dynamic Forms with Django

What is a dynamic form and why would you want one?

Usually, you know what a form is going to look like when you build it. You know how many fields it has, what types they are, and how they’re going to be laid out on the page. Most forms you create in a web app are fixed and static, except for the data within the fields.

A dynamic form doesn’t always have a fixed number of fields and you don’t know them when you build the form. The user might be adding multiple lines to a form, or even multiple complex parts like a series of dates for an event. These are forms that need to change the number of fields they have at runtime, and they’re harder to build. But the process of making them can be pretty straightforward if you use Django’s form system properly.

Django does have a formsets feature to handle multiple forms combined on one page, but that isn’t always a great match and they can be difficult to use at times. We’re going to look at a more straightforward approach here.

Creating a dynamic form

For our examples, we’re going to let the user create a profile including a number of interests listed. They can add any number of interests, and we’ll make sure they don’t repeat themselves by verifying there are no duplicates. They’ll be able to add new ones, remove old ones, and rename the interests they’ve already added to tell other users of the site about themselves.

Start with the basic static profile form.

  
class Profile(models.Model):
    first_name = models.CharField()
    last_name = models.CharField()
    interest = models.CharField()

class ProfileForm(forms.ModelForm):
    first_name = forms.CharField(required=True)
    last_name = forms.CharField(required=True)
    interest = forms.CharField(required=True)

class Meta:
    model = Profile

Create a fixed number of interest fields for the user to enter.

  
class Profile(models.Model):
    first_name = forms.CharField()
    last_name = forms.CharField()

Class ProfileInterest(models.Model):
    profile = models.ForeignKey(Profile)
    interest = models.CharField()

Class ProfileForm(forms.ModelForm):
    first_name = forms.CharField(required=True)
    last_name = forms.CharField(required=True)
    interest_0 = forms.CharField(required=True)
    interest_1 = forms.CharField(required=True)
    interest_2 = forms.CharField(required=True)

    def save(self):
        Profile = self.instance
        Profile.first_name = self.cleaned_data[“first_name”]
        Profile.last_name = self.cleaned_data[“last_name”]

        profile.interest_set.all().delete()
        For i in range(3):
           interest = self.cleaned_data[“interest_{}”.format(i]
           ProfileInterest.objects.create(
               profile=profile, interest=interest)

But since our model can handle any number of interests, we want our form to do so as well.

  
Class ProfileForm(forms.ModelForm):
    first_name = forms.CharField(required=True)
    last_name = forms.CharField(required=True)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        interests = ProfileInterest.objects.filter(
            profile=self.instance
        )
        for i in range(len(interests) + 1):
            field_name = 'interest_%s' % (i,)
            self.fields[field_name] = forms.CharField(required=False)
            try:
                self.initial[field_name] = interests[i].interest
            except IndexError:
                self.initial[field_name] = “”
        # create an extra blank field
        field_name = 'interest_%s' % (i + 1,)
        self.fields[field_name] = forms.CharField(required=False)
        self.fields[field_name] = “”

    def clean(self):
        interests = set()
        i = 0
        field_name = 'interest_%s' % (i,)
        while self.cleaned_data.get(field_name):
           interest = self.cleaned_data[field_name]
           if interest in interests:
               self.add_error(field_name, 'Duplicate')
           else:
               interests.add(interest)
           i += 1
           field_name = 'interest_%s' % (i,)
       self.cleaned_data[“interests”] = interests

    def save(self):
        profile = self.instance
        profile.first_name = self.cleaned_data[“first_name”]
        profile.last_name = self.cleaned_data[“last_name”]

        profile.interest_set.all().delete()
        for interest in self.cleaned_data[“interests”]:
           ProfileInterest.objects.create(
               profile=profile,
               interest=interest,
           )

Rendering the dynamic fields together

You won’t know how many fields you have when rendering your template now. So how do you render a dynamic form?

  
def get_interest_fields(self):
    for field_name in self.fields:
        if field_name.startswith(‘interest_’):
            yield self[field_name]

The last line is the most important. Looking up the field by name on the form object itself (using bracket syntax) will give you bound form fields, which you need to render the fields associated with the form and any current data.

  
{% for interest_field in form.get_interest_fields %}
    {{ interest_field }}
{% endfor %}

Reducing round trips to the server

It’s great that the user can add any number of interests to their profile now, but kind of tedious that we make them save the form for every one they add. We can improve the form in a final step by making it as dynamic on the client-side as our server-side.

We can also let the user enter many more entries at one time. We can remove the inputs from entries they’re deleting, too. Both changes make this form much easier to use on top of the existing functionality.

Adding fields on the fly

To add fields spontaneously, clone the current field when it gets used, appending a new one to the end of your list of inputs.

  
$('.interest-list-new').on('input', function() {
    let $this = $(this)
    let $clone = $this.clone()

You’ll need to increment the numbering in the name, so the new field has the next correct number in the list of inputs.

  
    let name = $clone.attr('name')
    let n = parseInt(name.split('_')[1]) + 1
    name = 'interest_' + n

The cloned field needs to be cleared and renamed, and the event listeners for this whole behavior rewired to the clone instead of the original last field in the list.

  
    $clone.val('')
    $clone.attr('name', name)
    $clone.appendTo($this.parent())
    $this.removeClass('interest-list-new')
    $this.off('input', arguments.callee)
    $clone.on('input', arguments.callee)
})

Removing fields on the fly

Simply hide empty fields when the user leaves them, so they still submit but don’t show to the user. On submit, handle them the same but only use those which were initially filled.

  
$form.find(“input[name^=interest_]:not(.interest-list-new)”)
    .on(“blur”, function() {
        var value = $(this).val();
        if (value === “”) {
            $(this).hide();
        }
    })

Why dynamic forms matter

An unsatisfying user experience that takes up valuable time may convince users to leave your site and go somewhere else. Using dynamic forms can be a great way to improve user experiences through response time to keep your users engaged.

Download Shipping Faster: Django Team Improvements
blog comments powered by Disqus
Times
Check

Success!

Times

You're already subscribed

Times