POST method: Ajax VS vanilla HTML (with Flask)

TL;DR:

  1. Using an html POST is easier, but will cause the page to be reloaded; using Ajax there’s some coding overhead, but we have a fine-grained control over what changes on the page.
  2. Using html POST will structure the website with an unique URL for every action; with Ajax, I could, in theory, have the whole web app within /index

Let’s look at some examples based on Flask.

@app.route('/register', methods=['GET', 'POST'])
def register():
    rform = RegistrationForm()

    # POST request
    if request.method == 'POST' and rform.validate() and rform.check_duplicates():
        new_user = User(rform.username.data,
                        rform.password.data,
                        rform.email.data)
        db.session.add(new_user)
        db.session.commit()
        flash('User successfully registered')
        return redirect(url_for('login'))

    # GET request
    return render_template('register.html', rform = rform)

-> on a GET/unvalid request you re-render the template, passing the rform object so that errors can be displayed;
-> on a POST request you validate and possibly add a new user and load a new login page.

The frontend code would be

<form action="" method="post" class="pure-form pure-form-aligned">
  {{ rform.hidden_tag() }}
  <fieldset>

    <div class="pure-control-group">
      <label for="username">Username</label>

      {{ rform.username(size=30) }}
      {% for error in rform.errors.username %}
      {{ error }}
      {% endfor %}
    </div>

    (...)

    <button type="submit" class="pure-button pure-button-primary">Register</button>
  </fieldset>
</form>

Note that action is void, since the post is executed against the same page you’re submitting the form from (in this case, register). Obviously, if you have (say) the login form on the index page you’ll probably want to call something like action=”/register” or whatnot.
The rform.hidden_tag() will make a CSRF token to be passed along the POST request when submit is clicked to protect us from spammy fuckers.

On the other hand, if you don’t want the page to be reloaded, you have to mess up with Ajax. Example:

@app.route('/_add_story', methods = ['POST'])
@login_required
def add_story():

    story = StoryForm()

    if not story.validate():
        return jsonify(errors = story.errors)

    # do crazy things here and there, then:
    return jsonify(newStory = story.content.data)

We want the user to submit a story and then either immediately see the story submitted, or see the errors messages. In both cases we send back to the page a Json array that will be handled with jQuery.

<form class="pure-form pure-form-stacked">
  <fieldset>
    <label for="phrase">Phrase: </label>  <span id="storyPhraseError" class="error"></span>
      {{story.content(size=35)}}

      (...)
    <a class="pure-button pure-button-primary" href="#" id="sendStory">send story!</a>
  </fieldset>
</form>

The submit button doesn’t actually submit anything through the html POST, but does so since it is binded with the following jQuery script. As an advantage for the increased complexity, the page is not reloaded. The CSRF thing is not needed anymore since this the user must already be logged in to see the page.

$(function() {
    $('a#sendStory').bind('click', function() {
        $.post('/_add_story',
               {
		   story : $('input[name="story"]').val(),
               }, function(data) {

		   // clean the error field
		   $("#storyError").text("");

		   // display errors if the form is not validated
		   if (!(jQuery.isEmptyObject(data.errors))) {
		       $("#storyPhraseError").text(data.errors.content);
		   }
		   // add a new entry
		   else {
                       /* some brand-new html here */
		   }
               });
        return false;
    });
});

The jQuery snippet is self-commented.