You can validate the entries made on the server. If the validation fails, the error messages can be returned as JSON and you can assign them to the respective input field.
A possible implementation of this strategy is to use webargs if it suits your application.
from flask import (
Flask,
jsonify,
render_template,
request
)
from webargs import fields, validate
from webargs.flaskparser import use_args
app = Flask(__name__)
@app.route('/settings', methods=['GET', 'POST'])
@use_args(
{
'title': fields.Str(
validate=[
validate.Length(min=3, max=30),
validate.Regexp(r'^(?! ).*(?<! )$',
error='No leading or trailing whitespace allowed.'
),
]
),
'other': fields.Str(
validate=[
validate.Length(min=3, max=30),
validate.Regexp(r'^(?! ).*(?<! )$',
error='No leading or trailing whitespace allowed.'
),
]
)
},
location='form'
)
def settings(args):
if request.method == 'POST':
print(args)
return render_template('settings.html')
@app.errorhandler(422)
@app.errorhandler(400)
def handle_error(err):
headers = err.data.get("headers", None)
messages = err.data.get("messages", ["Invalid request."])
if headers:
return jsonify({"errors": messages}), err.code, headers
else:
return jsonify({"errors": messages}), err.code
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Config</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous">
</head>
<body>
<main class="container mt-3">
<div id="alert-placeholder">
{% with messages = get_flashed_messages(with_categories=true) -%}
{% if messages -%}
{% for category, message in messages -%}
<div class="alert alert-{{category}} alert-dismissible fade show" role="alert">
<div>{{ message }}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor -%}
{% endif -%}
{% endwith -%}
</div>
<form method="post">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" name="title" id="title" class="form-control" minlength="3" maxlength="30" required />
<div id="title-error" class="invalid-feedback my-2"></div>
</div>
<div class="mb-3">
<label for="other" class="form-label">Other</label>
<input type="text" name="other" id="other" class="form-control" minlength="3" maxlength="30" required />
<div id="other-error" class="invalid-feedback my-2"></div>
</div>
</form>
</main>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<script type="text/javascript">
(function() {
window.addEventListener('DOMContentLoaded', () => {
const alertPlaceholder = document.getElementById('alert-placeholder');
// Show flash messages with JavaScript.
function flash(message, type) {
const wrapper = document.createElement('div');
wrapper.innerHTML = [
`<div class="alert alert-${type} alert-dismissible fade show" role="alert">`,
` <div>${message}</div>`,
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
'</div>'
].join('');
alertPlaceholder.append(wrapper);
const alert = wrapper.querySelector('.alert');
new bootstrap.Alert(alert);
setTimeout(() => {
bootstrap.Alert.getInstance(alert).close();
}, 3000);
}
// Query all input fields whose name does not start with add.
document.querySelectorAll('input:not([name^="add"])').forEach(elem => {
// Add a change listener to each.
elem.addEventListener('change', async function() {
// Submit form data.
const formData = new FormData();
formData.append(this.name, this.value);
const resp = await fetch('/settings', {
method: 'post',
body: formData
});
// Clear error messages for this input field.
this.classList.remove('is-invalid');
document.querySelector(`[id="${this.name}-error"]`).innerText = '';
// If the status code is not 200, parse the json data.
if (!resp.ok) {
const data = await resp.json();
if (data.errors && data.errors.form) {
// Show the error messages.
this.classList.add('is-invalid');
const errors = data.errors.form;
Object.keys(errors).forEach(key => {
document
.querySelector(`#${key}-error`)
.innerHTML = '<ul>'
+ errors[key].map(err => `<li>${err}</li>`).join('')
+ '</ul>';
});
}
} else {
// If everything is ok, show success message.
flash(`Successfully updated ${this.name}.`, 'info');
}
});
});
});
})();
</script>
</body>
</html>