Flask basics: dynamically adding form elements

Sometimes you want to dynamically add elements to forms if certain information pre-exists. In this instance I wanted to add an option change the information displayed but I didn't want the form fields to be there all the time and it was neater to simply not render them than remove them using JavaScript. Also, the number of fields I need is variable so rather than statically generating lots of fields it's better to create the correct number of fields each time.

First, I was checking to see if there was more than one "narrative" in the database, and if there is I want to give the user the option of jumping between them, if there isn't then they don't need the extra fields and having them could cause confusion and result in the user attempting to input data which will cause errors. A bit of searching revealed that dynamic form field generation is not only really simple to do, it's a common enough request that its made it into the docs. However I'm putting it here because neither the docs, nor the in depth mailing list post included a fully worked example, and frankly writing it down means I stand a better chance of remembering what I did.

This form is actually a hybrid static/dynamic form.

The static elements are defined in my forms file as UploadForm so I need to pass forms.UploadForm to my instantiated form.

from wtforms import StringField, RadioField

def admin():
    class OptionalFields(forms.UploadForm): pass

Since I only want to render my new form fields if a certain criteria is met I wrap them in an if statement. Inside that statement I create a static field, and and unknown quantity of dynamically created ones. The static field is there because the existence of multiple "narratives" doesn't necessarily mean that the user will want to jump between them at this point.

if len(check_for_things) > 1:
    OptionalFields.pick_narrative = RadioField(label="Swap?", choices=[("True", "Yes"), ("False", "No")])

    for thing in check_for_things:
        setattr(OptionalFields, thing, StringField(thing.title()))

Once I've done that I need to change my form instance to the one I've just created. As I've been using form=forms.FormImUsing it's just a case of changing forms.FormImUsing to OptionalFields. So, that displays the data, now I need to get at it. Because these fields are dynamically generated attempting to access them via form.fieldname.data results in an error and it's equally not possible to pass a variable to fieldname. Fortunately however it is possible to iterate over form.data.items() and pull out just the items you're looking for.

for fieldname, data in form.data.items():
    if fieldname == 'pick_narrative':
        # do whatever you'd normally do with the data
    if fieldname in check_for_things:
        # do whatever you'd normally do with the data

Putting it back: how to output the variable data to the form so the user can see it

Sometimes, you want to output previously input data to a form so that the user can edit it. It's pretty easy to do this with wtforms, you just pass the form it's data as an obj:

edit_this = models.Narrative.get(models.narrative.unique_data == unique_data)
form = OptionalFields(obj=edit_this)

It's even easy enough to pass a bit of data that uses a different name to your form so say you use site in your database and website in your form you just do this:

form = OptionalFields(obj=edit_this, website = edit_this.site)

However, it suddenly starts looking a lot more complicated when you don't know the field names, this is where dictionaries and kwargs come in handy. While you're looping over the things you're checking you might as well create a dictionary of the 'thing' against its 'value'. Then you just unpack the dictionary as part of the function call:

if len(check_for_things) > 1:
    OptionalFields.pick_narrative = RadioField(label="Swap?", choices=[("True", "Yes"), ("False", "No")])

    thread_dict = {}

    for thing in check_for_things:
        setattr(OptionalFields, thing, StringField(thing.title()))

    if thing_to_be_output == check_against_this:
        thread_dict[thing] = value
        form = OptionalFields(obj=edit_this, **thread_dict)

And now, magically you have the current values visible in your form, even though you didn't know what the form fields would be called in advance.

Comments powered by Talkyard.