Modal dialogs are quite common in GUI programming. They help to focus the users attention on the task at hand but still outline the context of the given or needed information by keeping the parent object visible in the background. As I have been doing a lot of classic GUI programming with PyQt I like to use modal dialogs and it feels natural for me to use them as well in Flask Web Applications. Bootstrap provides a great and simple way to create a modal dialog which is just what we want. However things get a bit more complicated when it comes to dealing with forms in modals. It took me a while to figure out a solution I am comfy with and I want to describe my solution in the following.

Form definition with WTForms

The great thing about Flask is that you can use WTForms to create the form fields and define validators. We don't want to modal forms without it. My form is a form for a brew controlling software defining rests in the mashing process. It allows us to define a mash step name, the temperature setpoint, the duration and a comment.

forms.py

class StepForm(Form):

    name = StringField(
        'Name:', validators=[DataRequired(), Length(0, Step.name.type.length)])

    setpoint = DecimalField(
        'Temperature:', places=0, validators=[DataRequired(), NumberRange(0, 100)])

    duration = IntegerField(
        'Duration:', validators=[DataRequired(), NumberRange(1, 500)])

    comment = TextAreaField('Comment:')

    submit = SubmitField('OK')

The Templates

We want to use a template for creating new steps and a template for editing steps wich contains an additional delete button. Both templates will extend a basic template called step.html.

templates/steps/step.html

{% from 'bootstrap/wtf.html' import form_field %}
<form id="stepForm" name="stepForm" class="form" method="post">
  <div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-label="close">
      <span aria-hidden="true">&times;</span>
    </button>
    <h4 class="modal-title">
    {% block title %}
    {% endblock %}
    </h4>
  </div>
  <div class="modal-body">
    {{ form.hidden_tag() }}
    {{ form_field(form.name) }}
    {{ form_field(form.setpoint) }}
    {{ form_field(form.duration) }}
    {{ form_field(form.comment) }}
  </div>
  <div class="modal-footer">
    {% block footer %}
      {{ form_field(form.submit) }} oder <a href="" data-dismiss="modal">Abbrechen</a>
    {% endblock %}
  </div>
</form>

templates/steps/add.html

 {% extends 'steps/step.html' %}
{% block title %}
Add a new step
{% endblock %}

{% block footer %}
{{ super() }}
{% endblock %}

templates/steps/edit.html

{% extends 'steps/step.html' %}
{% block title %}
Programmschritt bearbeiten
{% endblock %}

{% block footer %}
{{ super() }}
<button id="deleteStep" type="button" class="btn btn-danger" style="float: left;">Entfernen</button>
{% endblock %}

The parent document

We have a parent html document dealing for editing the receipe containing our mash steps. Here we place the frame for our modal dialog. This should be done at the beginning of the document.

templates/receipes/receipe.html

...
{% block main %}
<div id="stepDialog" class="modal fade" tabindex=-1 role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
    </div>
  </div>
</div>
...
{% endblock %}

We will use Ajax to load the forms in our modal dialog. jQuery provides the convenient jQuery.get() function for loading data via HTML GET request. We put the retrieved data in our modal dialog sceleton and pop the dialog open. If the user presses the submit button we interfere with event.preventDefault(). That is because normally submit would send a POST request to the url of our parent document which is the route main.edit_receipe. But we want to send the POST request to the url of our main.create_step route. We also do this via Ajax by using the jQuery.post() function.

templates/receipes/receipe.html

{% block script %}
{{ super() }}
<script>
  $(document).ready(function() {

    // add step button opens modal containing step form loaded via ajax
    $('#addStepButton').click(function() {
      var url = "{{ url_for('main.create_step', receipe_id=receipe.id) }}";
      $.get(url, function(data) {
        $('#stepDialog .modal-content').html(data);
        $('#stepDialog').modal();

        $('#submit').click(function(event) {
          event.preventDefault();
          $.post(url, data=$('#stepForm').serialize(), function(data) {
            if (data.status == 'ok') {
              $('#stepDialog').modal('hide');
              location.reload();
            }
            else {
              $('#stepDialog .modal-content').html(data);
            }
          });
        })
      });
    });
...
</script>
{% endblock %}

The view function

The view function looks like a normal route. We do our form validation here. If the validation succeeds we send a status "ok" which will lead our javascript code to reload the receipe document and show the changes we made.

@main.route('/receipes/<receipe_id>/steps/create', methods=['GET', 'POST'])
def create_step(receipe_id):
    receipe = Receipe.query.filter(Receipe.id == receipe_id).first_or_404()
    form = StepForm()
    if form.validate_on_submit():
        step = Step()
        step.receipe = receipe
        step.name = form.name.data
        step.setpoint = int(form.setpoint.data)
        step.duration = int(form.duration.data)
        step.comment = form.comment.data
        db.session.add(step)
        db.session.commit()
        return jsonify(status='ok')
    return render_template('steps/add.html', form=form)

Conclusion

This solution uses jQuerys Ajax functions to dynamically load templates with form data in a modal dialog. It allows the use of a standard composition of view-function, wtforms and html template. The main disadvantage from my point of view is that the complete html code of the modal dialog gets loaded each time the add button is clicked. This could lead to low response times. An alternative could be to place the html code of the modal dialog in the main html document and only load the data dynamically. However this leads to less clear code and also needs a request being made only with less data so the speed advantage should not be too hilarious.