Michael Chaplin
Michael Chaplin

Reputation: 21

Creating Custom Flask WTForms Widgets

I have a custom Flask WTForm where I want to have a portion of that form that includes a list of button type inputs that are created based on the number of entries in a table but have been having difficulties having them show up the way I want and passing the form validation. My goal for the look of this field is to have it show up as an Inline Button Group with a Checkbox type input. Below is an example of my route method.

@bp.route('/new_channel', methods=['GET', 'POST'])
def new_channel():

    # Pre-populate the NewChannelForm 
    newChannelForm = NewChannelForm()
    newChannelForm.required_test_equipment.choices =  [(equip.id, equip.name) for equip in TestEquipmentType.query.order_by('name')]
    test_equipment_types = TestEquipmentType.query.all()

return render_template('new_channel.html', title='Add New Channel', form=newChannelForm,
                            test_equipment_types=test_equipment_types)

I have tried using a FieldList with a FormField containing a custom form with a BooleanField and managed to get the styling right but the form validation didn't work. From looking into it further, BooleanField isn't compatible with a FieldList.

My next step is to use Flask WTForm example of a MultiSelectField with a custom widget for the Field and a custom widget for the option. The default is shown below:

class MultiCheckboxField(SelectMultipleField):
    """
    A multiple-select, except displays a list of checkboxes.

    Iterating the field will produce subfields, allowing custom rendering of
    the enclosed checkbox fields.
    """
    widget = widgets.ListWidget(prefix_label=False)
    option_widget = widgets.CheckboxInput()

My goal is to modify this to make a custom widget called InLineButtonGroupWidget which will use the styling for a list of in-line buttons like my picture included before. Additionally, I am looking to create a custom option_widget called CheckboxButtonInput to get the styling of each individual button where I can pass info to the field. This is what I have as the goal for the both:

InLineButtonGroupWidget:

<div class="btn-group-toggle" role="group" data-toggle="buttons"></div>

CheckboxButtonInput:

<label class="btn btn-outline-info" for="check-1">Calibrator
     <input type="checkbox" id="check-1">
</label> 

The documentation for how to create custom widgets is a bit over my head and doesn't explain it the best so I'm looking for some

Edit: Used Andrew Clark's suggestions and here is my final implementation:

routes.py

@bp.route('/new_channel', methods=['GET', 'POST'])
def new_channel():

    class NewChannelForm(FlaskForm):
        pass
    
    test_equipment_types = TestEquipmentType.query.all()
    for test_equipment_type in test_equipment_types:
        # Create field(s) for each query result
        setattr(NewChannelForm, f'checkbox_{test_equipment_type.name}', BooleanField(label=test_equipment_type.name, id=f'checkbox-{test_equipment_type.id}'))

    newChannelForm = NewChannelForm()

    if newChannelForm.validate_on_submit():
        print('Form has been validated')

        for test_equipment_type in test_equipment_types:
            if newChannelForm.data[f'checkbox_{test_equipment_type.name}']:
                channel.add_test_equipment_type(test_equipment_type)
        return redirect(url_for('main.index'))    

    print(newChannelForm.errors.items())

    return render_template('new_channel.html', title='Add New Channel', form=newChannelForm, units_dict=ENG_UNITS,
                            test_equipment_types=test_equipment_types)

new_channel.html

    <!-- Test Equipment Selection -->
        <div class="row">  
            <legend>Test Equipment Selection:</legend>           
            <div class="col-md-12">
                <div class="btn-group-toggle mb-3" role="group" data-toggle="buttons">
                    {% for test_equipment_type in test_equipment_types %}
                    <label class="btn btn-outline-info" for="checkbox-{{ test_equipment_type.id }}">
                        {{ test_equipment_type.name }}
                        {{ form['checkbox_{}'.format(test_equipment_type.name)] }}
                    </label>                    
                    {% endfor %}
                </div>
            </div>
        </div>

Upvotes: 2

Views: 1477

Answers (1)

Andrew Clark
Andrew Clark

Reputation: 880

I usually tackle form building doing something like this:

def buildNewChannelForm():
    class NewChannelForm(FlaskForm):
        # put any non dynamic fields here
        pass

    test_equipment_types = TestEquipmentType.query.all()
    for test_equipment_object in test_equipment_types:
        # create field(s) for each query result
        setattr(NewChannelForm, f'field_name_{test_equipment_object.id}', SelectField(label='label name', choices=[(equip.id, equip.name) for equip in TestEquipmentType.query.order_by('name')]))

    return NewChannelForm()

Edit 1:

I'm not sure if there are better ways to do it, but I usually do something like this to handle data submission

def buildNewChannelForm():
    new_channel_form_variable_list = []
    class NewChannelForm(FlaskForm):
        # put any non dynamic fields here
        pass

    test_equipment_types = TestEquipmentType.query.all()
    for test_equipment_object in test_equipment_types:
        # create field(s) for each query result
        setattr(NewChannelForm, f'field_name_{test_equipment_object.id}', SelectField(label='label name', choices=[(equip.id, equip.name) for equip in TestEquipmentType.query.order_by('name')]))

        # append variable name
        new_channel_form_variable_list.append(f'field_name_{test_equipment_object.id}')

    return NewChannelForm(), new_channel_form_variable_list

Then you can render your form using your variable list, just include in your render_template statement

{% for variable_name in new_channel_form_variable_list %}
    {{ form[variable_name] }}
{% endfor %}

Then on submission of form in route, it's just a dictionary. So you can do something like this

result_dictionary = form.data

# either loop through your variable list or handle each one individually
for variable_name in new_channel_form_variable_list:
    print(f'variable name: {variable_name}, value: {result_dictionary[variable_name]}')

Upvotes: 1

Related Questions