3
votes

I am currently working on an address form using wtf, which contains Country, State, City..etc. The database is all set with FK.

class Country(db.Model):
    __tablename__ = 'countries'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    users = db.relationship('User', backref='countries', lazy='dynamic')
class City(db.Model):
    __tablename__ = 'cities'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    countries_id = db.Column(db.Integer, db.ForeignKey('countries.id')) 

Now I am trying to achieve a chained selectfield sort effect to optimize user experience. The desired effect is that without leaving or refreshing the page to have the selectfield pull data depending on the previous selectfield.

For example, User selects Australia in Country, then the second selectfield of State should contain only the states in Australia.

I've done some research on this topic, and couldn't come up with an satisfying solution. Here is the two possible yet unsatifying solution that I find.

1.Use jQuery-plugin e.g. Chained. However, this plugin requires line-by-line coding. If I adopt this solution, there would be at least another 400+ lines, and that's not very pythonic. For example:

<select id="series" name="series">
  <option value="">--</option>
  <option value="series-3" class="bmw">3 series</option>
  <option value="series-5" class="bmw">5 series</option>
  <option value="series-6" class="bmw">6 series</option>
  <option value="a3" class="audi">A3</option>
  <option value="a4" class="audi">A4</option>
  <option value="a5" class="audi">A5</option>
</select>

2.Use Wtf's "Select fields with dynamic choice values", which is also undesirable as it only pulls the data once depending on the default value of the previous selectfield. For example: If the default selectfield for country is Australia, then the state selectfield would only contain states within Australia. When you change the country selectfield say to America, the state selectfield would still only contains states within Australia. Below, is the tutorial for this method listed in wtf documentation:

class UserDetails(Form):
    group_id = SelectField(u'Group', coerce=int)

def edit_user(request, id):
    user = User.query.get(id)
    form = UserDetails(request.POST, obj=user)
    form.group_id.choices = [(g.id, g.name) for g in Group.query.order_by('name')]

From the above research, I assume the satisfying solution should be somewhere between the two and it definitely should involve some Javascript to monitor the client side activities without sending request to the server. Has anyone got a satisying solution for the flask framework?

2

2 Answers

14
votes

If you want something dynamic on the client, there's no way around writing some JavaScript. Luckily it's not the 400+ lines you estimate. This example doesn't use WTForms, but that's not really important. The key part is sending the available choices as JSON, and dynamically changing the available options. Here's a single file runnable Flask app that demonstrates the basic idea.

from flask import Flask, render_template_string

app = Flask(__name__)

@app.route('/')
def index():
    systems = {
        'PlayStation': ['Spyro', 'Crash', 'Ico'],
        'N64': ['Mario', 'Superman']
    }

    return render_template_string(template, systems=systems)

template = """
<!doctype html>
<form>
    <select id="system">
        <option></option>
    </select>
    <select id="game"></select>
    <button type="submit">Play</button>
</form>
<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
<script>
    "use strict";

    var systems = {{ systems|tojson }};

    var form = $('form');
    var system = $('select#system');
    var game = $('select#game');

    for (var key in systems) {
        system.append($('<option/>', {'value': key, 'text': key}));
    }

    system.change(function(ev) {
        game.empty();
        game.append($('<option/>'));

        var games = systems[system.val()];

        for (var i in games) {
            game.append($('<option/>', {'value': games[i], 'text': games[i]}));
        }
    });

    form.submit(function(ev) {
        ev.preventDefault();
        alert("playing " + game.val() + " on " + system.val());
    });
</script>
"""

app.run()
5
votes

I had a similar problem recently and solved it using the idea from this answer.

A minimal example could look like this:

enter image description here

So, just two dropdowns, whereby the second depends on the selected value of the first one; all other elements on the page are not affected by this selection (here I only use one check box). Once the selection is made, the button below the dropdowns will trigger an action and the result can then again be displayed on the page (here, I only update the sentence below the button but of course you can also do more reasonable stuff), e.g. to: enter image description here

The HTML file looks like this (located in templates/index.html):

<!DOCTYPE html>
<html lang="en">
  <head>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
  </head>
  <body>
    <div class="container">
      <div class="header">
        <h3 class="text-center text-muted">Dynamic dropdowns</h3>
      </div>

      <div>
        Check me and I don't change status when you select something below
      </div><br>
      <div>
        <input class="form-check-input" type="checkbox" value="" id="use_me">Select me
      </div><br><br>

      <div class="row">
        <div class="form-group col-xs-6">
          <label for="all_classes">Select a class</label>
          <select class="form-control" id="all_classes">
            {% for o in all_classes %}
                    <option value="{{ o }}">{{ o }}</option>
            {% endfor %}
          </select>
        </div>
        <div class="form-group col-xs-6">
          <label for="all_entries">Select an entry</label>
          <select class="form-control" id="all_entries">
            {% for o in all_entries %}
                    <option value="{{ o }}">{{ o }}</option>
            {% endfor %}
          </select>
        </div>
      </div>

      <div>
        <button type="button" id="process_input">Process selection!</button>
      </div><br><br>
      <div id="processed_results">
        Here we display some output based on the selection
      </div>
    </div>
    <script src="https://code.jquery.com/jquery-1.12.4.js" type="text/javascript"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <script type="text/javascript">
      $(document).ready(function() {

        $('#all_classes').change(function(){

          $.getJSON('/_update_dropdown', {
            selected_class: $('#all_classes').val()

          }).success(function(data) {
                $('#all_entries').html(data.html_string_selected);
           })
        });
        $('#process_input').bind('click', function() {

            $.getJSON('/_process_data', {
                selected_class: $('#all_classes').val(),
                selected_entry: $('#all_entries').val(),


            }).success(function(data) {
                $('#processed_results').text(data.random_text);
            })
          return false;

        });
      });
    </script>
  </body>
</html>

And the corresponding python script looks as follows:

from flask import Flask, render_template, request, jsonify
import json

# Initialize the Flask application
app = Flask(__name__)


def get_dropdown_values():

    """
    dummy function, replace with e.g. database call. If data not change, this function is not needed but dictionary
    could be defined globally
    """

    class_entry_relations = {'class1': ['val1', 'val2'],
                             'class2': ['foo', 'bar', 'xyz']}

    return class_entry_relations


@app.route('/_update_dropdown')
def update_dropdown():

    # the value of the first dropdown (selected by the user)
    selected_class = request.args.get('selected_class', type=str)

    # get values for the second dropdown
    updated_values = get_dropdown_values()[selected_class]

    # create the value sin the dropdown as a html string
    html_string_selected = ''
    for entry in updated_values:
        html_string_selected += '<option value="{}">{}</option>'.format(entry, entry)

    return jsonify(html_string_selected=html_string_selected)


@app.route('/_process_data')
def process_data():
    selected_class = request.args.get('selected_class', type=str)
    selected_entry = request.args.get('selected_entry', type=str)

    # process the two selected values here and return the response; here we just create a dummy string

    return jsonify(random_text="you selected {} and {}".format(selected_class, selected_entry))


@app.route('/')
def index():

    """
    Initialize the dropdown menues
    """

    class_entry_relations = get_dropdown_values()

    default_classes = sorted(class_entry_relations.keys())
    default_values = class_entry_relations[default_classes[0]]

    return render_template('index.html',
                           all_classes=default_classes,
                           all_entries=default_values)


if __name__ == '__main__':

    app.run(debug=True)