Flask Ajax Forms
Introduction
When building web applications with Flask, forms are a fundamental component for user interaction. Traditional form submissions require a full page reload, which can disrupt the user experience. This is where Ajax (Asynchronous JavaScript and XML) comes into play.
Ajax allows you to submit form data to a Flask backend and receive responses asynchronously without refreshing the entire page. This creates a smoother, more interactive user experience that feels more like a desktop application.
In this tutorial, we'll learn how to:
- Create Flask routes that handle Ajax requests
- Build forms that submit data asynchronously
- Process form data on the server and return JSON responses
- Update the page dynamically with the server's response
Prerequisites
Before diving in, make sure you have:
- Basic understanding of Flask routing and templates
- Familiarity with HTML forms
- Basic knowledge of JavaScript and jQuery
- Flask and its dependencies installed in your environment
Setting Up Your Flask Application
Let's start by setting up a basic Flask application structure:
from flask import Flask, render_template, request, jsonify
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
@app.route('/')
def index():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
Create a templates folder with an index.html
file that will contain our form:
<!DOCTYPE html>
<html>
<head>
<title>Flask Ajax Form Example</title>
<!-- Include jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
form { margin-bottom: 20px; }
input, button { margin: 5px 0; padding: 8px; }
#result { padding: 10px; background-color: #f0f0f0; display: none; }
</style>
</head>
<body>
<h1>Flask Ajax Form Example</h1>
<form id="ajaxForm">
<div>
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<button type="submit">Submit</button>
</form>
<div id="result"></div>
<script>
// We'll add our Ajax code here
</script>
</body>
</html>
Adding the Ajax Form Submission
Now, let's add the JavaScript code that will handle the Ajax form submission:
<script>
$(document).ready(function() {
$("#ajaxForm").on('submit', function(event) {
// Prevent the default form submission
event.preventDefault();
// Get form data
var formData = {
'name': $('#name').val(),
'email': $('#email').val()
};
// Send Ajax POST request
$.ajax({
type: 'POST',
url: '/process',
data: formData,
dataType: 'json',
encode: true
})
.done(function(data) {
// Handle the response
$('#result').html(data.message).show();
if (data.status === 'success') {
$('#ajaxForm')[0].reset();
}
})
.fail(function(data) {
// Handle errors
$('#result').html('An error occurred').show();
console.log('Error:', data);
});
});
});
</script>
Creating the Flask Route to Process the Form
Now, let's add the Flask route that will handle the form submission:
@app.route('/process', methods=['POST'])
def process():
if request.method == 'POST':
name = request.form.get('name')
email = request.form.get('email')
# Validate data (basic example)
if not name or not email:
return jsonify({'status': 'error', 'message': 'Please provide both name and email'})
# Process the data (in a real app, you might save to a database, etc.)
# For this example, we'll just echo the data back
response = {
'status': 'success',
'message': f'Thank you, {name}! Your form was submitted successfully. We will contact you at {email}.'
}
return jsonify(response)
This route receives the form data via the POST request, processes it, and returns a JSON response that will be handled by our JavaScript.
Complete Example
Let's put everything together. Your app.py
should look like this:
from flask import Flask, render_template, request, jsonify
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
@app.route('/')
def index():
return render_template('index.html')
@app.route('/process', methods=['POST'])
def process():
if request.method == 'POST':
name = request.form.get('name')
email = request.form.get('email')
# Validate data
if not name or not email:
return jsonify({'status': 'error', 'message': 'Please provide both name and email'})
# Process the data
response = {
'status': 'success',
'message': f'Thank you, {name}! Your form was submitted successfully. We will contact you at {email}.'
}
return jsonify(response)
if __name__ == '__main__':
app.run(debug=True)
Adding More Functionality: Form Validation
Let's enhance our example with client-side form validation before submitting:
<script>
$(document).ready(function() {
$("#ajaxForm").on('submit', function(event) {
event.preventDefault();
// Reset previous error messages
$('.error').remove();
// Get form data
var name = $('#name').val();
var email = $('#email').val();
// Validate form data
var isValid = true;
if (!name) {
$('#name').after('<span class="error" style="color: red;">Please enter your name</span>');
isValid = false;
}
if (!email) {
$('#email').after('<span class="error" style="color: red;">Please enter your email</span>');
isValid = false;
} else if (!isValidEmail(email)) {
$('#email').after('<span class="error" style="color: red;">Please enter a valid email</span>');
isValid = false;
}
if (!isValid) return;
// Send Ajax POST request
$.ajax({
type: 'POST',
url: '/process',
data: {
'name': name,
'email': email
},
dataType: 'json',
encode: true
})
.done(function(data) {
// Handle the response
$('#result').html(data.message).show();
if (data.status === 'success') {
$('#ajaxForm')[0].reset();
}
})
.fail(function(data) {
// Handle errors
$('#result').html('An error occurred').show();
console.log('Error:', data);
});
});
// Helper function to validate email format
function isValidEmail(email) {
var pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return pattern.test(email);
}
});
</script>
Showing a Loading Indicator
To provide better user feedback during the Ajax request, let's add a loading indicator:
<style>
/* Add this to your CSS */
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
display: inline-block;
margin-left: 10px;
vertical-align: middle;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<script>
$(document).ready(function() {
$("#ajaxForm").on('submit', function(event) {
event.preventDefault();
// ...validation code here...
// Show loading indicator
$('#submit-button').append('<span class="loader"></span>');
$('#submit-button').prop('disabled', true);
$.ajax({
// ...Ajax settings here...
})
.done(function(data) {
// Remove loading indicator
$('.loader').remove();
$('#submit-button').prop('disabled', false);
// ...response handling here...
})
.fail(function(data) {
// Remove loading indicator
$('.loader').remove();
$('#submit-button').prop('disabled', false);
// ...error handling here...
});
});
});
</script>
Don't forget to update your button with an ID:
<button type="submit" id="submit-button">Submit</button>
Real-World Example: Contact Form with File Upload
Let's create a more advanced example: a contact form that includes file uploading via Ajax:
First, update our Flask app:
import os
from werkzeug.utils import secure_filename
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'pdf', 'doc', 'docx', 'txt'}
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/contact', methods=['GET', 'POST'])
def contact():
if request.method == 'GET':
return render_template('contact.html')
if request.method == 'POST':
name = request.form.get('name')
email = request.form.get('email')
message = request.form.get('message')
# Check if the post request has the file part
if 'file' not in request.files:
return jsonify({'status': 'error', 'message': 'No file part'})
file = request.files['file']
# If user does not select file, browser also submits an empty part without filename
if file.filename == '':
return jsonify({'status': 'error', 'message': 'No selected file'})
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
# Create upload folder if it doesn't exist
if not os.path.exists(app.config['UPLOAD_FOLDER']):
os.makedirs(app.config['UPLOAD_FOLDER'])
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)
# In a real app, you might send an email or save to database here
return jsonify({
'status': 'success',
'message': f'Thank you, {name}! Your message has been sent and your file has been uploaded successfully.'
})
else:
return jsonify({
'status': 'error',
'message': 'Invalid file type. Allowed types: pdf, doc, docx, txt'
})
Now, let's create the contact.html template:
<!DOCTYPE html>
<html>
<head>
<title>Contact Form with File Upload</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 20px; max-width: 600px; margin: 0 auto; }
h1 { text-align: center; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input, textarea { width: 100%; padding: 8px; box-sizing: border-box; }
textarea { height: 150px; }
button { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; cursor: pointer; }
button:hover { background-color: #45a049; }
#result { margin-top: 20px; padding: 10px; display: none; }
.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<h1>Contact Form</h1>
<form id="contactForm" enctype="multipart/form-data">
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="message">Message:</label>
<textarea id="message" name="message" required></textarea>
</div>
<div class="form-group">
<label for="file">Attachment (PDF, DOC, DOCX, TXT):</label>
<input type="file" id="file" name="file">
</div>
<button type="submit" id="submit-button">Send Message</button>
</form>
<div id="result"></div>
<script>
$(document).ready(function() {
$("#contactForm").on('submit', function(event) {
event.preventDefault();
// Show loading indicator
$('#submit-button').append('<span class="loader"></span>');
$('#submit-button').prop('disabled', true);
// Create FormData object for file uploads
var formData = new FormData(this);
$.ajax({
type: 'POST',
url: '/contact',
data: formData,
cache: false,
contentType: false,
processData: false,
success: function(response) {
// Remove loading indicator
$('.loader').remove();
$('#submit-button').prop('disabled', false);
// Display response message
if (response.status === 'success') {
$('#result').removeClass('error').addClass('success').html(response.message).show();
$('#contactForm')[0].reset();
} else {
$('#result').removeClass('success').addClass('error').html(response.message).show();
}
},
error: function(xhr, status, error) {
// Remove loading indicator
$('.loader').remove();
$('#submit-button').prop('disabled', false);
// Display error message
$('#result').removeClass('success').addClass('error').html('An error occurred. Please try again later.').show();
console.log('Error:', xhr.responseText);
}
});
});
});
</script>
</body>
</html>
Don't forget to add a route to access the contact form:
@app.route('/contact', methods=['GET'])
def contact_page():
return render_template('contact.html')
Key Considerations When Using Ajax Forms in Flask
1. Cross-Site Request Forgery (CSRF) Protection
When working with Flask-WTF forms, you need to include CSRF protection in your Ajax requests:
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
csrf = CSRFProtect(app)
In your template:
<form id="ajaxForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<!-- rest of your form -->
</form>
<script>
// Include the token in Ajax requests
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token() }}");
}
}
});
</script>
2. Error Handling
Always handle possible errors on both the client and server sides:
@app.route('/process', methods=['POST'])
def process():
try:
# Process form data
# ...
return jsonify({'status': 'success', 'message': 'Data processed successfully'})
except Exception as e:
app.logger.error(f"Error processing form: {str(e)}")
return jsonify({'status': 'error', 'message': 'Server error occurred'}), 500
3. Handling File Uploads
As shown in our example, handling file uploads requires using FormData
and special Ajax settings:
var formData = new FormData(document.getElementById('uploadForm'));
$.ajax({
url: '/upload',
type: 'POST',
data: formData,
contentType: false, // Don't set content type
processData: false, // Don't process data
// other settings...
});
Summary
In this tutorial, you've learned how to:
- Create Flask routes that handle Ajax form submissions
- Use jQuery to send form data asynchronously without page reloads
- Process form data on the server and return JSON responses
- Update the page dynamically based on the server's response
- Add client-side form validation
- Display loading indicators for better user experience
- Handle file uploads using Ajax
- Implement CSRF protection with Ajax requests
Ajax forms significantly improve user experience by eliminating page refreshes and providing immediate feedback. They're essential for building modern, responsive web applications with Flask.
Additional Resources and Exercises
Resources
Exercises
-
Basic Exercise: Modify the contact form to include a subject field and validate it on both client and server sides.
-
Intermediate Exercise: Create an Ajax-powered search form that displays results without page reload as the user types (implement debouncing for better performance).
-
Advanced Exercise: Build a multi-step form wizard that saves data at each step using Ajax, allowing users to navigate between steps without losing their progress.
-
Challenge Exercise: Implement a dynamic form that changes its fields based on previous selections, all handled via Ajax requests to the Flask backend.
By mastering Ajax forms in Flask, you're taking a significant step toward building modern, responsive web applications that provide a smooth user experience.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)