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.

Warning

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"))
Info

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. Use apply:
(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.