Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a build a form with repeated elements well

The title really doesn't say it, as i'm having trouble summarizing the issue. So here goes the long explanation:

Let's say I'm adding multiple contacts' information, and I have these fields:

  • Name of the contact
  • Method of Contact (email, phone number, instant message)
    • If email: Show an email field (let's say this field exists)
    • If phone number: Show a phone number field
    • If instant message: Show a text field

So right off the bat, I'm going to be needing JavaScript to complete this on the page itself (to add add or deletion contact fields), which I'm ok with. However, since I can add multiple contacts (and as the software developer, I don't know how many contact the user wants to add, it could be 1, 10, or 100)

So my biggest problem is how am I going to structure the things like the names for each of the field. Should I throw everything into things like names[], contactmethods[] and access things in order, or if there's a better solution.

In addition, if the server starts to verify these information, and finds some malformed info, I would like to be able to send the data that the client sent to the server back to the client, so they don't lose everything they've entered. How would I easily accomplish that?

Some background info: Technologies currently in use (that's relevant):

  • Flask
  • jQuery
  • WTForms
like image 682
Pwnna Avatar asked Jul 09 '12 20:07

Pwnna


1 Answers

No need to build anything (at least on the server side) - WTForms already supports what you need - it calls them "field enclosures". The behavior you are looking for is found in wtforms.fields.FormField and wtforms.fields.FieldList

class ContactForm(Form):
    name = TextField("Name", validators=[Required()])
    contact_type = SelectField("Contact Type",
                                validators=[Required()],
                                choices=[
                                    ("email", "Email"),
                                    ("phone", "Phone Number"),
                                    ("im", "Instant Message")
                                ])
    # `If` is a custom validator - see below
    email_address = TextField("Email",
                                  validators=[If("contact_type",
                                                     "email",
                                                     [Required(), Email()])
                                  ])
    phone_number = TextField("Phone #",
                                  validators=[If("contact_type",
                                                           "phone", [Required()])
                                  ])
    im_handle = TextField("IM Handle",
                                  validators=[If("contact_type",
                                                           "im", [Required()])
                                  ])


class SignUpForm(Form):
    # Other fields go here
    contacts = FieldList(FormField(ContactForm))

You'll also need a custom validator to validate the appropriate field, given the user's choice:

# CAUTION: Untested code ahead
class If(object):
    def __init__(self,
                      parent,
                      run_validation=None,
                      extra_validators=None,
                      msg=None):
        self.parent = parent
        self.msg = msg if msg is not None else u"Invalid"
        if callable(run_validation):
            self.run_validation = run_validation
        else:
            _run_validation = lambda self, parent, form: parent.data == run_validation
            self.run_validation = _run_validation
        self.extra_validators = extra_validators if extra_validators is not None \
                                                     else []

    def __call__(self, field, form):
        parent = getattr(form, self.parent)
        if self.run_validation(parent, form):
            return field.validate(form, extra_validators=self.extra_validators)

When you call form.validate() on the server side the fields will be automatically checked against the requirements and the errors will be populated appropriately so you can render them back on the client side.

Creating new fields on the client side is simple and WTForms will pick them up on the back end as long as you name then using the same naming convention it uses - namely field.short_name + '-' + index.

like image 159
Sean Vieira Avatar answered Sep 29 '22 01:09

Sean Vieira