Notification messages for JSON responses with jQuery and Symfony2
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('×')
;
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">×</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">×</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">×</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">×</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!