4

I am experimenting with flask and just creating some basic functionality to add data to a form displayed in a modal. While I have managed to get the form to display in the modal and save it from the modal, I am struggling to understand what needs to be done to ensure that field validation errors are shown on the modal itself. Currently if there are errors user is redirected to a whole page with edit form.

They say picture is better than words - So here is a gif showing what is happening:

Modal Form Error Goes to the page

The entire app code is on github and the current state on heroku can be accessed here... username: admin@admin.com and password: adminpassword. It's all sandbox anyway.

Relevant code is as below:

routes.py


@expenses.route("/expense")
@login_required
def expense():
    page = request.args.get('page', 1, type=int)
    expenses = Expense.query.order_by(Expense.expense_date.desc()).paginate(page=page, per_page=5)
    form = ExpenseForm()
    return render_template('expense/expense.html', expenses=expenses, form=form)


@expenses.route("/expense/new", methods=['GET', 'POST'])
@login_required
def new_expense():
    form = ExpenseForm()
    if form.validate_on_submit():
        expense = Expense(description=form.description.data, expense_date=form.expense_date.data,
                        amount=form.amount.data,vat_amount=form.vat_amount.data,Transferrable=form.Transferrable.data, author=current_user)
        db.session.add(expense)
        db.session.commit()
        flash('Your expense has been created!', 'success')
        return redirect(url_for('expenses.expense'))
    return render_template('expense/create_expense.html', title='New Expense',
                           form=form, legend='New Expense')

Now the expenses.html is a big one but on it the modal is called using the following:

<button type="button" class="btn btn-primary btn-sm m-1" data-toggle="modal" data-target="#AddNewModal">Add New Expense</button>
{% include "expense/partials/addModal.html" %}

and the addModal.html is as shown below:

<!-- Add New Modal -->
{% from "util/macros.html" import form_field with context %}
<div class="modal fade" id="AddNewModal" tabindex="-1" role="dialog" aria-labelledby="AddNewModalLabel" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="AddNewModalLabel">Add New Expense</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
        <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <div class="amount-section">
          <form method="POST" action="/expense/new">
          {{ form.hidden_tag() }}
          <fieldset class="form-group">
            <legend class="border-bottom mb-4">{{ legend }}</legend>
            <div class="form-group">
              {{ form.description.label(class="form-control-label") }}
              {% if form.description.errors %}
                {{ form.description(class="form-control form-control-lg is-invalid") }}
                <div class="invalid-feedback">
                  {% for error in form.description.errors %}
                  <span>{{ error }}</span>
                  {% endfor %}
                </div>
              {% else %}
                {{ form.description(class="form-control form-control-lg") }}
              {% endif %}
            </div>
            <div class="form-group">
              {{ form.amount.label(class="form-control-label") }}
              {% if form.amount.errors %}
                {{ form.amount(class="form-control form-control-lg is-invalid") }}
                <div class="invalid-feedback">
                {% for error in form.amount.errors %}
                  <span>{{ error }}</span>
                {% endfor %}
                </div>
              {% else %}
                {{ form.amount(class="form-control form-control-lg") }}
              {% endif %}
            </div>
            <div class="form-group">
              {{ form.expense_date.label(class="form-control-label") }}
              {% if form.expense_date.errors %}
                {{ form.expense_date(class="form-control form-control-lg is-invalid") }}
                <div class="invalid-feedback">
                  {% for error in form.expense_date.errors %}
                    <span>{{ error }}</span>
                  {% endfor %}
                </div>
              {% else %}
                {{ form.expense_date(class="form-control form-control-lg", type="date") }}
              {% endif %}
            </div>
            <div class="form-group">
              {{ form.vat_amount.label(class="form-control-label") }}
              {% if form.vat_amount.errors %}
                {{ form.vat_amount(class="form-control form-control-lg is-invalid") }}
                <div class="invalid-feedback">
                  {% for error in form.vat_amount.errors %}
                    <span>{{ error }}</span>
                  {% endfor %}
                </div>
              {% else %}
                {{ form.vat_amount(class="form-control form-control-lg") }}
              {% endif %}
            </div>
            <!-- {{ form_field(form.vat_amount,with_label=True) }} -->
            <div class="form-group">
            {% if form.Transferrable.errors %}
              {{ form.Transferrable(class="form-control form-control-lg is-invalid") }}
              <div class="invalid-feedback">
                {% for error in form.Transferrable.errors %}
                  <span>{{ error }}</span>
                {% endfor %}
              </div>
            {% else %}
              {{ form.Transferrable(type="checkbox") }}
            {% endif %}
              {{ form.Transferrable.label(class="form-control-label") }}
            </div>
            <!-- {{ form_field(form.Transferrable) }} -->
            <p><button type="submit" class="btn btn-primary">Add</button></p>
            </fieldset>
          </form>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
        </div>
      </div>
    </div>
  </div>
</div>

techbolt
  • 103
  • 8

1 Answers1

2

It is because if form has some errors you render create_expense.html that's why form is presented not in modal.

I would merge 2 views: /expense/new and /expense so that it can handle both GET and POST and conditionally show modal if form has some errors.

Merged views:

@expenses.route("/expense", methods=['GET', 'POST'])
@login_required
def expense():
    page = request.args.get('page', 1, type=int)
    expenses = Expense.query.order_by(Expense.expense_date.desc()).paginate(page=page, per_page=5)
    form = ExpenseForm()
    if form.validate_on_submit():
        expense = Expense(description=form.description.data, expense_date=form.expense_date.data,
                        amount=form.amount.data,vat_amount=form.vat_amount.data,Transferrable=form.Transferrable.data, author=current_user)
        db.session.add(expense)
        db.session.commit()
        flash('Your expense has been created!', 'success')
    return render_template('expense/expense.html', expenses=expenses, form=form)

Conditional modal showing at the bottom of addModal.html:

{% if form.errors %}

<script>
$('#AddNewModal').modal('show');
</script>

{% endif %}

And action for the form in addModal.html must be changed as well to:

<form method="POST" action="/expense">

However after these changes views /expense/new and /expense would have some code in common so refactoring might be needed. Now you at least know why errors are not shown in the modal.

stasiekz
  • 1,775
  • 5
  • 22
  • Thanks.. It helped me understand what is going on but I am now confused on one behaviour of the form validation - See the DataRequired validation triggers without a refresh but the integrity of datatype (integer vs string) is triggered only after a round trip to database. Why is that? - and am I not correct to think that it will be bad practice to get errors from backend rather than deal with them on front end. If so how will I achieve that here? – techbolt Sep 20 '19 at 08:59
  • @techbolt Validation is performed on backend cause the page quickly reloads. You can see that your browser makes POST request on modal form submission in Developer tools -> Network. To get additional layer of validation you can modify your form fields so that bootstrap handles the errors before actual call to backend is made. – stasiekz Sep 20 '19 at 10:29
  • Thanks so very much for taking time out to help me.... this makes so much more sense. I changed the type for the field to number and now it wont allow me to enter string so that is surely a better way to do it. Basically I change the field on addModal to ```{{ form.amount(class="form-control form-control-lg", value=expense.amount, type="number", required="required", step="0.01") }}``` – techbolt Sep 20 '19 at 10:58