Flash messages
Flash messages are temporary messages you want to show to your users. They should be displayed once, and only once: on a subsequent page load, they don’t appear anymore.
They should specially work across route redirects. So, they are typically created in the web session.
Handling them involves those steps:
- create a message in the session
- have a quick and easy function to do this
- give them as arguments to the template when rendering it
- have some HTML to display them in the templates
- remove the flash messages from the session.
Getting started
If you didn’t follow the tutorial, quickload those libraries:
(ql:quickload '("hunchentoot" "djula" "easy-routes"))
We also introduce a local nickname, to shorten the use of hunchentoot
to ht
:
(uiop:add-package-local-nickname :ht :hunchentoot)
Add this in your .lisp file if you didn’t already, they are typical for our web demos:
(defparameter *port* 9876)
(defvar *server* nil "Our Hunchentoot acceptor")
(defun start (&key (port *port*))
(format t "~&Starting the web server on port ~a~&" port)
(force-output)
(setf *server* (make-instance 'easy-routes:easy-routes-acceptor :port port))
(ht:start *server*))
(defun stop ()
(ht:stop *server*))
Create flash messages in the session
This is our core function to quickly pile up a flash message to the web session.
The important bits are:
- we ensure to create a web session with
ht:start-session
. - the
:flash
session object stores a list of flash messages. - we decided that a flash messages holds those properties:
- its type (string)
- its message (string)
(defun flash (type message)
"Add a flash message in the session.
TYPE: can be anything as you do what you want with it in the template.
Here, it is a string that represents the Bulma CSS class for notifications: is-primary, is-warning etc.
MESSAGE: string"
(let* ((session (ht:start-session)) ;; <---- ensure we started a web session
(flash (ht:session-value :flash session)))
(setf (ht:session-value :flash session)
;; With a cons, REST returns 1 element
;; (when with a list, REST returns a list)
(cons (cons type message) flash))))
Now, inside any route, we can call this function to add a flash message to the session:
(flash "warning" "You are liking Lisp")
It’s easy, it’s handy, mission solved. Next.
Delete flash messages when they are rendered
For this, we use Hunchentoot’s life cycle and CLOS-orientation:
;; delete flash after it is used.
;; thanks to https://github.com/rudolfochrist/booker/blob/main/app/controllers.lisp for the tip.
(defmethod ht:handle-request :after (acceptor request)
(ht:delete-session-value :flash))
which means: after we have handled a request, delete the
:flash
object from the session.
If your application sends API requests in JavaScript, they can delete flash messages without you noticing. Read more below.
An external API request (from the command line for example) is not concerned, as it doesn’t carry Hunchentoot session cookies.
Render flash messages in templates
Set up Djula templates
Create a new flash-template.html
file.
(djula:add-template-directory "./")
(defparameter *flash-template* (djula:compile-template* "flash-template.html"))
You might need to change the current working
directory of your Lisp REPL to the directory of your .lisp file, so
that djula:compile-template*
can find your template. Use the short
command ,cd
or (swank:set-default-directory "/home/you/path/to/app/")
.
See also asdf:system-relative-pathname system directory
.
HTML template
This is our template. We use Bulma CSS to pimp it up and to use its notification blocks.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WALK - flash messages</title>
<!-- Bulma Version 1-->
<link rel="stylesheet" href="https://unpkg.com/bulma@1.0.2/css/bulma.min.css" />
</head>
<body>
<!-- START NAV -->
<nav class="navbar is-white">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item brand-text" href="#">
Bulma Admin
</a>
<div class="navbar-burger burger" data-target="navMenu">
<span></span>
</div>
</div>
<div id="navMenu" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="#">
Home
</a>
<a class="navbar-item" href="#">
Orders
</a>
</div>
</div>
</div>
</nav>
<!-- END NAV -->
<div class="container">
<div class="columns">
<div class="column is-6">
<h3 class="title is-4"> Flash messages. </h3>
<div> Click <a href="/tryflash/">/tryflash/</a> to access an URL that creates a flash message and redirects you here.</div>
{% for flash in flashes %}
<div class="notification {{ flash.first }}">
<button class="delete"></button>
{{ flash.rest }}
</div>
{% endfor %}
</div>
</div>
</div>
</body>
<script>
// JS snippet to click the delete button of the notifications.
// see https://bulma.io/documentation/elements/notification/
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
const $notification = $delete.parentNode;
$delete.addEventListener('click', () => {
$notification.parentNode.removeChild($notification);
});
});
});
</script>
</html>
Look at
{% for flash in flashes %}
where we render our flash messages.
Djula allows us to write {{ flash.first }}
and {{ flash.rest }}
to
call the Lisp functions on those objects.
We must now create a route that renders our template.
Routes
The /flash/
URL is the demo endpoint:
(easy-routes:defroute flash-route ("/flash/" :method :get) ()
(djula:render-template* *flash-template* nil
:flashes (or (ht:session-value :flash)
(list (cons "is-primary" "No more flash messages were found in the session. This is a default notification.")))))
It is here that we pass the flash messages as a parameter to the template.
In your application, you must add this parameter in all the existing routes. To make this easier, you can:
- use Djula’s default template variables, but our parameters are to be found dynamically in the current request’s session, so we can instead
- create a “render” function of ours that calls
djula:render-template*
and always adds the:flash
parameter. Useapply
:
(defun render (template &rest args)
(apply
#'djula:render-template* template nil
;; All arguments must be in a list.
(list*
:flashes (or (ht:session-value :flash)
(list (cons "is-primary" "No more flash messages were found in the session. This is a default notification.")))
args)))
Finally, this is the route that creates a flash message:
(easy-routes:defroute flash-redirect-route ("/tryflash/") ()
(flash "is-warning" "This is a warning message held in the session. It should appear only once: reload this page and you won't see the flash message again.")
(ht:redirect "/flash/"))
Demo
Start the app with (start)
if you didn’t start Hunchentoot already,
otherwise it was enough to compile the new routes.
You should see a default notification. Click the “/tryflash/” URL and you’ll see a flash message, that is deleted after use.
Refresh the page, and you won’t see the flash message again.
Discussing: Flash messages and API calls
Our :after
method on the Hunchentoot request lifecycle will delete
flash messages for any request that carries the session cookies. If
your application makes API calls, you can use the Fetch method with
the {credentials: "omit"}
parameter:
fetch("http://localhost:9876/api/", {
credentials: "omit"
})
Otherwise, don’t use this :after
method and delete flash messages
explicitely in your non-API routes.
We could use a macro shortcut for this:
(defmacro with-flash-messages ((messages) &body body)
`(let ((,messages (ht:session-value :flash)))
(prog1
(progn
,@body)
(ht:delete-session-value :flash))))
Use it like this:
(easy-routes:defroute flash-route ("/flash/" :method :get) ()
(with-flash-messages (messages)
(djula:render-template* *flash-template* nil
:flashes (or messages
(list (cons "is-primary" "No more flash messages were found in the session. This is a default notification."))))))
We want our macro to return the result of djula:render-template*
,
and not the result of ht:delete-session-value
, that is nil, hence
the “prog1/ progn” dance.