jquerypythonflaskjinja2flask-wtforms

How to create chained selectfield in flask without refreshing the page?


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?


Solution

  • 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()