Messages after form-submit in iommi

iommi Django

Very often you want to show users some success or/and error messages after they submit a form.

It's quite easy in iommi thanks to Form.extra__on_save (for success) and Form.post_validation (for errors).
In this example I'm going to use bootstrap5.

I believe you already have a iommi.py file, because you followed the docs, so lets start.

messages.iommi

So first we need to edit the iommi.py file

from django.utils.html import escape
from django.utils.text import format_lazy
from django.utils.translation import gettext, gettext_lazy
from django.contrib import messages


class Form(iommi.Form):

    class Meta:

        # messages on save containing model verbose name and str(object)
        # we use extra_evaluated, so they can be changed per Table if needed
        extra_evaluated__message_created = lambda form, **kwargs: format_lazy(
            "{model_verbose_name} <em>{{object_name}}</em> {text}",
            model_verbose_name=form.model._meta.verbose_name.capitalize(),
            text=gettext_lazy("created"),
        )
        extra_evaluated__message_saved = lambda form, **kwargs: format_lazy(
            "{text1} {model_verbose_name} <em>{{object_name}}</em> {text2}",
            model_verbose_name=form.model._meta.verbose_name,
            text1=gettext_lazy("Changes of"),
            text2=gettext_lazy("saved"),
        )

        @staticmethod
        def extra__on_save(request, form, **kwargs):
            if getattr(form.extra_evaluated, "disable_messages", False):
                return
            if form.extra.is_create:
                msg = form.extra_evaluated.message_created
            else:
                msg = form.extra_evaluated.message_saved
            messages.success(request, msg.format(object_name=escape(str(form.instance))))

        @staticmethod
        def post_validation(request, form, **kwargs):
            if getattr(form.extra_evaluated, "disable_messages", False):
                return

            # message on invalid
            if not form.is_valid():
                messages.error(request, gettext("Changes not saved, invalid or missing values"))

Add a template filter to templatetags/bootstrap5.py

from django.contrib import messages
from django import template

register = template.Library()


@register.filter
def bs5_alert_type(message):
    if message.level == messages.DEBUG:
        return "primary"
    elif message.level == messages.ERROR:
        return "danger"
    return messages.DEFAULT_TAGS[message.level]

Then create a static/js/messages.js with

function bs5_alert_type(type) {
    if(type === "debug")
        return "primary";
    else if(type === "error")
        return "danger";
    return type
}


function bs5_alert_icon(type) {
    let icon = null;
    if(type === "debug" || type === "primary")
        icon = "bi-shield";
    else if(type === "info")
        icon = "bi-shield";
    else if(type === "success")
        icon = "bi-shield-check";
    else if(type === "warning")
        icon = "bi-shield-exclamation";
    else if(type === "error" || type === "danger")
        icon = "bi-shield-x";

    if(icon)
        return '<i class="bi ' + icon + ' fs-2hx text-' + bs5_alert_type(type) + ' me-4"></i>';
    return "";
}


// pop-up messages on an event
document.addEventListener("message.show", function(event) {
    let messages_block = document.querySelector("#messages");
    if(!messages_block) {
        messages_block = document.createElement('div');
        messages_block.id = "messages";
        document.body.append(messages_block);
    }
    let type = bs5_alert_type(event.detail.type);
    let message_element = document.createElement('div');
    message_element.classList.add("row");
    message_element.classList.add("one-message");
    let message_element_content = "<div class=\"col col-md-auto\">";
    message_element_content += "    <div class=\"alert alert-" + type + " d-flex align-items-center p-5 mb-5 alert-dismissible fade show\" role=\"alert\">";
    message_element_content += bs5_alert_icon(type);
    message_element_content += "        <div class=\"d-flex flex-column me-8\"><span>" + event.detail.message + "</span></div>";
    message_element_content += "        <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\"></button>";
    message_element_content += "    </div>";
    message_element_content += "</div>";
    message_element.innerHTML = message_element_content;
    messages_block.append(message_element);
    if(type === "success") {
        autoHideSuccessMessage(message_element);
    }
});


function showMessage(message, type, element) {
    if(!element) {
        element = document;
    }
    element.dispatchEvent(
        new CustomEvent("message.show", {
            bubbles: true,
            detail: {type: type, message: message},
        })
    );
}


// message close button
IommiBase.addLiveEventListener(
    "click",
    "#messages .one-message .btn-close",
    function (event) {
        let msg_block = this.closest(".one-message");
        let messages_block = msg_block.closest("#messages");
        msg_block.remove();
        if(!messages_block.querySelectorAll('.one-message').length) {
            messages_block.remove();
        }
    }
);


// success messages auto-close after 5s
function autoHideSuccessMessage(message_element) {
    setTimeout(function() {
        message_element.querySelector(".btn-close").dispatchEvent(new Event('click', { 'bubbles': true }));
    }, 5000);
}

To your main.css add

#messages {
	width: 100%;
	position: fixed;
	bottom: 0;
	left: 0;
	z-index: 900;
	padding: 0 1rem;
}
@media (min-width: 768px) {
	#messages {
		width: auto;
		bottom: 1rem;
		left: 2.25rem;
		margin-right: 2.25rem;
		padding: 0;
	}
}

Then add to your base template before </body>

{# load bootstrap5 at the beggining of the template #}

<script src="{% static 'js/messages.js' %}"></script>
<script>
{% for message in messages %}
    showMessage("{{ message|safe }}", "{{ message|bs5_alert_type }}");
{% endfor %}
</script>
The {{ message|safe }} can be potentially dangerous, it's so I can use html inside messages (e.g.<em>{{object_name}}</em>), but then I also do msg.format(object_name=escape(str(form.instance))).

And now you have nice colored popup messages with icons. Success messages disapear after 5s, errors stay until closed manually.