populate WTForms select field using value selected from previous field
Asked Answered
H

2

27

New to this, trying to build an app following a well known Flask tutorial, using Flask-bootstrap, Flask-wtforms, Jinja etc

I have a form with 2 select fields and a button.

class Form(FlaskForm): 
    school_year = SelectField('School year', choices=some_tuples_list)
    category = SelectField('Category', choices=[]) 
    submit = SubmitField('submit')

I want only the first field to be pre-populated, and the other to get populated (on the client side?) based on the previous field's selected value.

In the template I try something like

{{ form.school_year(**{"onchange":"getCategories()"}) }}

which works ok (provided that I return the tuples list to populate the next field, using the proper javascript and route) but I want something like the following

{{ wtf.form_field(form.school_year(**{"onchange":"getCategories()"})) }}

which doesn't work (Error: wtforms.widgets.core.HTMLString object' has no attribute 'flags')

So, I guess my question really is: how do I implement an onChange event on this wtf form field? (And is this what I have to do, or is there a way from the view function?)

Thanks in advance.

Hexapod answered 19/12, 2016 at 22:36 Comment(3)
I manage this with Javascript. Interested to see if someone has a different solution. Keep in mind if you plan to use form.validate_on_submit() in your view function the select choices list must be loaded with every possible value or WTForms validation will fail.Sorayasorb
@PJ Santoro Yes, but how do I call the Javascript in the wtf field in the template?Hexapod
I'll share an example below of how I implement this.Sorayasorb
S
49

Here is an example implementation of this logic to work with WTForms native functionality. The trick here, is if you want to use WTForms validation, you need to instantiate the form with every possible value, then modify the available options in Javascript to show the filtered values based on the other select.

For this example I'm going to use the concept of States and Counties (I work with a lot of geo data so this is a common implementation I build).

Here's my form, I've assigned unique IDs to the important elements to access them from Javascript:

class PickCounty(Form):
    form_name = HiddenField('Form Name')
    state = SelectField('State:', validators=[DataRequired()], id='select_state')
    county = SelectField('County:', validators=[DataRequired()], id='select_county')
    submit = SubmitField('Select County!')

Now, the Flask view to instantiate and process the form:

@app.route('/pick_county/', methods=['GET', 'POST'])
def pick_county():
    form = PickCounty(form_name='PickCounty')
    form.state.choices = [(row.ID, row.Name) for row in State.query.all()]
    form.county.choices = [(row.ID, row.Name) for row in County.query.all()]
    if request.method == 'GET':
        return render_template('pick_county.html', form=form)
    if form.validate_on_submit() and request.form['form_name'] == 'PickCounty':
        # code to process form
        flash('state: %s, county: %s' % (form.state.data, form.county.data))
    return redirect(url_for('pick_county'))

A Flask view to respond to XHR requests for Counties:

@app.route('/_get_counties/')
def _get_counties():
    state = request.args.get('state', '01', type=str)
    counties = [(row.ID, row.Name) for row in County.query.filter_by(state=state).all()]
    return jsonify(counties)

And, finally, the Javascript to place at the bottom of your Jinja template. I'm assuming because you mentioned Bootstrap, that you are using jQuery. I'm also assuming this is in line javascript so I'm using Jinja to return the correct URL for the endpoint.

<script charset="utf-8" type="text/javascript">

$(function() {

    // jQuery selection for the 2 select boxes
    var dropdown = {
        state: $('#select_state'),
        county: $('#select_county')
    };

    // call to update on load
    updateCounties();

    // function to call XHR and update county dropdown
    function updateCounties() {
        var send = {
            state: dropdown.state.val()
        };
        dropdown.county.attr('disabled', 'disabled');
        dropdown.county.empty();
        $.getJSON("{{ url_for('_get_counties') }}", send, function(data) {
            data.forEach(function(item) {
                dropdown.county.append(
                    $('<option>', {
                        value: item[0],
                        text: item[1]
                    })
                );
            });
            dropdown.county.removeAttr('disabled');
        });
    }

    // event listener to state dropdown change
    dropdown.state.on('change', function() {
        updateCounties();
    });

});

</script>
Sorayasorb answered 20/12, 2016 at 16:3 Comment(7)
Seems to do the job -and also provides a very hands on introduction to jQuery for me. Thank you very much.Hexapod
+1 for instantiating the selection with all possible values. I just spent ages trying to figure out why the form would not validate after updating the options with Javascript. Thanks!Shocker
I'm using this example and i'm getting the error NameError: name 'State' is not definedOzoniferous
@bms9nmh State and County are representations of SQLAlchemy models defined elsewhere, but you can replace them with whatever hierarchical data source you are using.Sorayasorb
Using the generator expression I'm generating a list of tuples from sqlalchemy.. [('01', 'Alabama'), ('02', 'Alaska')...] which, in the rendered view will populate the wtforms select options for those fields. In the XHR endpoing it will jsonify into a nested list in javascript e.g: [['01', 'Alabama'], ['02', 'Alaska']...]Sorayasorb
I have 3 select fields and options in the 2nd change depending on 1st and options in 3rd depend on the choice in 2nd. However, I keep getting a validation error using the exactly similar code. I instantiated all 3 select fields with default choices as suggested in the comments, But keeps failing on the validation for the last(3rd) select field. Can anyone help?Kath
For anyone wondering, If you use a chain of SelectFields each triggering the rest, then you need to instantiate ALL SelectFields with all possible values. Otherwise, the validation will fail.Kath
N
1

PJ Santoro's answer is great. The update on load was called but the event listener didn't work for me at first. Turned out that I hadn't swapped out 'state' for my own field ID as I thought it was a keyword referring to the state of the field! D'oh! So in looking for other options, I found this also worked, that might be useful for somebody out there:

    // event listener to state dropdown change
$('#state').change(function() {
    updateCounties();
});
Nicotine answered 4/9, 2017 at 22:7 Comment(1)
Yes, this is the JQuery method to bind to change event (up above is the 'pure' JS method).Gemini

© 2022 - 2024 — McMap. All rights reserved.