Michaël Perrin

Sustainable web development.

Notification messages for JSON responses with jQuery and Symfony2

We’re going to setup an easy-to-use notification system using jQuery and the Symfony2 event dispatcher, plus some CSS to get things look nice. The flash messages should be displayed for both regular and AJAX requests.

This article explains how to implement the notification system manually, but I also published a bundle to use it right out of the box: https://github.com/michaelperrin/AretusaFlashBundle

What we want to do:

  • display a success or fail notification message after an AJAX request has been performed.
  • make it totally automatic

How we’re going to do it:

  • Backend side: - Define notification messages in the Symfony application using the Symfony Flash Messages system which is also used for non-AJAX requests
  • Use the Symfony Event Dispatcher to automatically add notification messages data to JSON responses.
  • Non-JSON responses will use the session to display notifications on the rendered page, as usual
  • Frontend side: - Make a jQuery plugin to display notification in a nice way (animated, display notifications in a stack, Growl-like)
  • The same jQuery plugin will listen to all AJAX responses and display related notifications if there are some

Symfony: define notification messages in the controller

We’re going to do things in the most standard way. So let’s use the standard Symfony flash messages system in our action. It uses the session to store flash messages.

<?php  
class CartController extends Controller  
{
    public function addProductAction()
    {
        // ...

        $response = new JsonResponse();

        $dataToReturn = array(
            // ...
        );

        $response->setData($dataToReturn);

        $this->get('session')->getFlashBag()->add(
            'success',
            'Your product has been added to your cart.'
        );

        return $response;
    }
}

Note:JsonResponse is available since Symfony 2.1.

Add a response listener to the service container

We’re going to listen to response events in Symfony and catch JSON responses to embed additional data for our notification system.

For this purpose, add a listener to the Service Container (services.yml) and tell the dispatcher to listen for the response event:

services:  
    acme_test_bundle.flash_messenger:
        class: Acme\TestBundle\Messenger\Flash
        arguments: ["@session"]
        tags:
            - { name: kernel.event_listener, event: kernel.response}

The session object is passed to the listener as this is where we’re going to check if there are any pending notification to display.

Create the response listener

The listener checks if there are some pending notification and add them to the JSON response structure.

<?php  
namespace Acme\TestBundle\Messenger;

use Symfony\Component\HttpKernel\Event\FilterResponseEvent;  
use Symfony\Component\HttpFoundation\Session\Session;  
use Symfony\Component\HttpFoundation\JsonResponse;

class Flash  
{
    protected $session;

    public function __construct(Session $session)
    {
        $this->session = $session;
    }

    public function onKernelResponse(FilterResponseEvent $event)
    {
        $response = $event->getResponse();

        // modify JSON response object
        if ($response instanceof JsonResponse) {
            // Embed flash messages to the JSON response if there are any
            $flashMessages = $this->session->getFlashBag()->all();

            if (!empty($flashMessages)) {
                // Decode the JSON response before encoding it again with additional data
                $data = json_decode($response->getContent(), true);
                $data['messages'] = $flashMessages;
                $response->setData($data);
            }
        }
    }
}

A JSON response which looked like this:

{
    key1: 'value1',
    ...
}

will now look like this:

{
    key1: 'value1',
    ...,
    messages: {
        success: ['Message 1', ...]
        error: [ ... ]
    }
}

Display flash messages

We’re now going to implement a jQuery plugin to listen to all AJAX responses and display notifications. The trick here is to use the pretty unknown jQuery ajaxComplete method.

(function($) {
    var methods = {
        init: function(options) {
            methods.settings = $.extend({}, $.fn.flashNotification.defaults, options);

            setTimeout(
                function() {
                    $('.alert')
                        .show('slow')
                        .delay(methods.settings.hideDelay)
                        .hide('fast')
                    ;
                },
                500
            );

            methods.listenIncomingMessages();
        },

        /**
         * Listen to AJAX responses and display messages if they contain some
         */
        listenIncomingMessages: function() {
            $(document).ajaxComplete(function(event, xhr, settings) {
                var data = $.parseJSON(xhr.responseText);

                if (data.messages) {
                    var messages = data.messages;

                    var i;

                    if (messages.error) {
                        for (i = 0; i < messages.error.length; i++) {
                            methods.addError(messages.error[i]);
                        }
                    }

                    if (messages.success) {
                        for (i = 0; i < messages.success.length; i++) {
                            methods.addSuccess(messages.success[i]);
                        }
                    }

                    if (messages.info) {
                        for (i = 0; i < messages.info.length; i++) {
                            methods.addInfo(messages.info[i]);
                        }
                    }
                }
            });
        },

        addSuccess: function(message) {
            var flashMessageElt = methods.getBasicFlash(message).addClass('alert-success');

            methods.addToList(flashMessageElt);
            methods.display(flashMessageElt);
        },

        addError: function(message) {
            var flashMessageElt = methods.getBasicFlash(message).addClass('alert-error');

            methods.addToList(flashMessageElt);
            methods.display(flashMessageElt);
        },

        addInfo: function(message) {
            var flashMessageElt = methods.getBasicFlash(message).addClass('alert-info');

            methods.addToList(flashMessageElt);
            methods.display(flashMessageElt);
        },

        getBasicFlash: function(message) {
            var flashMessageElt = $('<div></div>')
                .hide()
                .addClass('alert')
                .append(methods.getCloseButton())
                .append($('<div></div>').html(message))
            ;

            return flashMessageElt;
        },

        getCloseButton: function() {
            var closeButtonElt = $('<button></button>')
                .addClass('close')
                .attr('data-dismiss', 'alert')
                .html('&times')
            ;

            return closeButtonElt;
        },

        addToList: function(flashMessageElt) {
            flashMessageElt.appendTo($('#flash-messages'));
        },

        display: function(flashMessageElt) {
            setTimeout(
                function() {
                    flashMessageElt
                        .show('slow')
                        .delay(methods.settings.hideDelay)
                        .hide('fast', function() { $(this).remove(); } )
                    ;
                },
                500
            );
        }
    };

    $.fn.flashNotification = function(method) {
        // Method calling logic
        if (methods[method]) {
            return methods[ method ].apply(this, Array.prototype.slice.call(arguments, 1));
        } else if (typeof method === 'object' || ! method) {
            return methods.init.apply(this, arguments);
        } else {
            $.error('Method ' +  method + ' does not exist on jQuery.flashNotification');
        }
    };

    $.fn.flashNotification.defaults = {
        'hideDelay'         : 4500,
        'autoHide'          : true,
        'animate'           : true
    };
})(jQuery);

With this plugin, if the messages property is defined in a JSON response, the following HTML code will be appended to the document and displayed for a few seconds:

<div class="alert alert-success">  
    <button class="close" data-dismiss="alert">&times;</button>
    <div>The message</div>
</div>  

Make notification messages look nice

With these styles, notifications will look better:

.alert {
    width: 200px;
    background-color: black;
    background-color: rgba(30, 30, 30, 0.9);
    text-shadow: 1px 1px black;
    color: #eee;
    padding-left: 65px;
    box-shadow: 4px 3px 15px rgba(0,0,0,0.9);
    border: 0;
    background-repeat: no-repeat;
    background-position: 15px 50%;
    display: none;
    z-index: 1000;
}

.alert .close {
    color: white;
    color: rgba(255, 255, 255, 0.8);
    text-shadow: 0 1px 0 #000;
    opacity: 1;
}

Display flash messages on HTML pages as well

When a HTML page is rendered (generally a non-AJAX response), flash messages have to be displayed as well on the page. Let’s create a Twig template for this purpose:

<div id="flash-messages">  
    {% for flashMessage in app.session.flashbag.get('success') %}
        <div class="alert alert-success">
            <button class="close" data-dismiss="alert">&times;</button>
            {{ flashMessage|trans|raw|nl2br }}
        </div>
    {% endfor %}

    {% for flashMessage in app.session.flashbag.get('error') %}
        <div class="alert alert-error">
            <button class="close" data-dismiss="alert">&times;</button>
            {{ flashMessage|trans|raw|nl2br }}
        </div>
    {% endfor %}

    {% for flashMessage in app.session.flashbag.get('info') %}
        <div class="alert alert-info">
            <button class="close" data-dismiss="alert">&times;</button>
            {{ flashMessage|trans|raw|nl2br }}
        </div>
    {% endfor %}
</div>  

This template will probably be included in your layout file (layout.html.twig) like this:

<!DOCTYPE html>  
<html lang="en">  
    <body>
        {# ... #}

        <div id="content">
            {# ... #}

            {{ include('AcmeTestBundle:Default:flashMessages.html.twig') }}
        </div>

        <script>
        $('#flash-messages').flashNotification('init');
        </script>
    </body>
</html>  

Conclusion

We’ve created a system to display notification messages either for AJAX or non-AJAX responses, non-AJAX ones being handled the usual way apart from that they look nice.

I’m going to implement a Symfony bundle embedding all this so that you can easily integrate it into your Symfony project without copying and pasting that much.

I’ll enhance the jQuery plugin as well and make it much better.

Stay tuned!

Michaël Perrin

Read more posts by this author.

comments powered by Disqus