1

I have this code: (template)

// Send the changed input data to the server
(function() {
    window.addEventListener('DOMContentLoaded', () => {
        document.querySelectorAll('input').forEach(elem => {
            elem.addEventListener('change', function() {
                if(this.name.substring(0,3) != "add")
                {
                    const formData = new FormData();
                    formData.append(this.name, this.value);
                    fetch('/settings', {
                        method: 'post',
                        body: formData
                    });
                }
            });
        });
    });
})();

Here needs showing message in template (app.py)

if len(value) > 30:
   flash("Error! Header title max length is 30 characters", "error")
   return redirect("/settings")

layout.html has:

<div class="alert alert-primary mb-0 text-center" role="alert">
   {{ get_flashed_messages() | join(" ") }}
   </div>

As redirect() not working in case of form submission with AJAX, How can show appropriate message about submitted data?

Or how can send back the conditional result from app.py to template with AJAX and show message in a <div>?

parmer_110
  • 325
  • 2
  • 11
  • What do you think of limiting the length of the input by the attribute `maxlength="30"` for the input field? This way you avoid input errors and don't have to generate an error message with javascript. – Detlef Jan 14 '23 at 12:44
  • May user send long input by browser inspect. I want have server-side validation. So I have wrote maxlength too. – parmer_110 Jan 14 '23 at 13:28
  • Server-side validation is important, but you might be able to skip the `flash` hint and just use [`abort(400)`](https://flask.palletsprojects.com/en/2.2.x/api/#flask.abort). – Detlef Jan 14 '23 at 13:57

2 Answers2

1

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>
Detlef
  • 6,137
  • 2
  • 6
  • 24
-1

Flash message will appear only when you reload the page as they are rendered on the server. Redrawing the body with the response should solve the problem. try the following:

window.addEventListener('DOMContentLoaded', () => {
    document.querySelectorAll('input').forEach(elem => {
        elem.addEventListener('change', function() {
            if(this.name.substring(0,3) != "add")
            {
                const formData = new FormData();
                formData.append(this.name, this.value);
                
                var xhr = new XMLHttpRequest();
                xhr.open('POST', '/settings');
                xhr.onload = function() {
                    if (xhr.status === 200) {
                        body.innerHTML = xhr.responseText;
                    } else {
                       console.log('An error occurred');
                    }
               };
              xhr.send(fData);
            }
        });
    });
});

Probably you need to redraw the whole page with the response.

Elie Saad
  • 516
  • 4
  • 8