Skip to content

Commit 2531656

Browse files
committed
new page: flash messages
1 parent 1763b61 commit 2531656

File tree

4 files changed

+413
-0
lines changed

4 files changed

+413
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
2+
;;;
3+
;;; Demo of flash messages:
4+
;;; anywhere in a route, easily add flash messages to the session.
5+
;;; They are rendered at the next rendering of a template.
6+
;;; They are removed from the session once rendered.
7+
;;;
8+
9+
;; inspired by https://github.com/rudolfochrist/booker/blob/main/app/controllers.lisp
10+
11+
(uiop:add-package-local-nickname :ht :hunchentoot)
12+
13+
(djula:add-template-directory ".")
14+
15+
(defparameter *flash-template* (djula:compile-template* "flash-template.html"))
16+
17+
(defparameter *port* 9876)
18+
19+
(defvar *server* nil "Our Hunchentoot acceptor")
20+
21+
(defun flash (type message)
22+
"Add a flash message in the session.
23+
24+
TYPE: can be anything as you do what you want with it in the template.
25+
Here, it is a string that represents the Bulma CSS class for notifications: is-primary, is-warning etc.
26+
MESSAGE: string"
27+
(let* ((session (ht:start-session))
28+
(flash (ht:session-value :flash session)))
29+
(setf (ht:session-value :flash session)
30+
;; With a cons, REST returns 1 element
31+
;; (when with a list, REST returns a list)
32+
(cons (cons type message) flash))))
33+
34+
;;; delete flash after it is used.
35+
(defmethod ht:handle-request :after (acceptor request)
36+
(ht:delete-session-value :flash))
37+
38+
#+another-solution
39+
(defun render (template &rest args)
40+
(apply
41+
#'djula:render-template* template nil
42+
(list*
43+
:flashes (or (ht:session-value :flash)
44+
(list (cons "is-primary" "No more flash messages were found in the session. This is a default notification.")))
45+
args)))
46+
47+
48+
(easy-routes:defroute flash-route ("/flash/" :method :get) ()
49+
#-another-solution
50+
(djula:render-template* *flash-template* nil
51+
:flashes (or (ht:session-value :flash)
52+
(list (cons "is-primary" "No more flash messages were found in the session. This is a default notification."))))
53+
#+another-solution
54+
(render *flash-template*)
55+
)
56+
57+
(easy-routes:defroute flash-redirect-route ("/tryflash/") ()
58+
(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.")
59+
(ht:redirect "/flash/"))
60+
61+
(defun start (&key (port *port*))
62+
(format t "~&Starting the web server on port ~a~&" port)
63+
(force-output)
64+
(setf *server* (make-instance 'easy-routes:easy-routes-acceptor :port port))
65+
(ht:start *server*))
66+
67+
(defun stop ()
68+
(ht:stop *server*))
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
+++
2+
title = "Flash messages"
3+
weight = 36
4+
+++
5+
6+
Flash messages are temporary messages you want to show to your
7+
users. They should be displayed once, and only once: on a subsequent
8+
page load, they don't appear anymore.
9+
10+
![](/building-blocks/flash-messages.png)
11+
12+
They should specially work across route redirects. So, they are
13+
typically created in the web session.
14+
15+
Handling them involves those steps:
16+
17+
- create a message in the session
18+
- have a quick and easy function to do this
19+
- give them as arguments to the template when rendering it
20+
- have some HTML to display them in the templates
21+
- remove the flash messages from the session.
22+
23+
## Getting started
24+
25+
If you didn't follow the tutorial, quickload those libraries:
26+
27+
```lisp
28+
(ql:quickload '("hunchentoot" "djula" "easy-routes"))
29+
```
30+
31+
We also introduce a local nickname, to shorten the use of `hunchentoot` to `ht`:
32+
33+
```lisp
34+
(uiop:add-package-local-nickname :ht :hunchentoot)
35+
```
36+
37+
Add this in your .lisp file if you didn't already, they
38+
are typical for our web demos:
39+
40+
~~~lisp
41+
(defparameter *port* 9876)
42+
(defvar *server* nil "Our Hunchentoot acceptor")
43+
44+
(defun start (&key (port *port*))
45+
(format t "~&Starting the web server on port ~a~&" port)
46+
(force-output)
47+
(setf *server* (make-instance 'easy-routes:easy-routes-acceptor :port port))
48+
(ht:start *server*))
49+
50+
(defun stop ()
51+
(ht:stop *server*))
52+
~~~
53+
54+
55+
## Create flash messages in the session
56+
57+
This is our core function to quickly pile up a flash message to the web session.
58+
59+
The important bits are:
60+
61+
- we ensure to create a web session with `ht:start-session`.
62+
- the `:flash` session object stores *a list* of flash messages.
63+
- we decided that a flash messages holds those properties:
64+
- its type (string)
65+
- its message (string)
66+
67+
68+
~~~lisp
69+
(defun flash (type message)
70+
"Add a flash message in the session.
71+
72+
TYPE: can be anything as you do what you want with it in the template.
73+
Here, it is a string that represents the Bulma CSS class for notifications: is-primary, is-warning etc.
74+
MESSAGE: string"
75+
(let* ((session (ht:start-session)) ;; <---- ensure we started a web session
76+
(flash (ht:session-value :flash session)))
77+
(setf (ht:session-value :flash session)
78+
;; With a cons, REST returns 1 element
79+
;; (when with a list, REST returns a list)
80+
(cons (cons type message) flash))))
81+
~~~
82+
83+
Now, inside any route, we can call this function to add a flash message to the session:
84+
85+
```lisp
86+
(flash "warning" "You are liking Lisp")
87+
```
88+
89+
It's easy, it's handy, mission solved. Next.
90+
91+
## Delete flash messages when they are rendered
92+
93+
For this, we use Hunchentoot's life cycle and CLOS-orientation:
94+
95+
```lisp
96+
;; delete flash after it is used.
97+
;; thanks to https://github.com/rudolfochrist/booker/blob/main/app/controllers.lisp for the tip.
98+
(defmethod ht:handle-request :after (acceptor request)
99+
(ht:delete-session-value :flash))
100+
```
101+
102+
which means: after we have handled the current request, delete the `:flash` object from the session.
103+
104+
## Render flash messages in templates
105+
106+
### Set up Djula templates
107+
108+
Create a new `flash-template.html` file.
109+
110+
~~~lisp
111+
(djula:add-template-directory "./")
112+
(defparameter *flash-template* (djula:compile-template* "flash-template.html"))
113+
~~~
114+
115+
{{% notice info %}}
116+
117+
You might need to change the current working
118+
directory of your Lisp REPL to the directory of your .lisp file, so
119+
that `djula:compile-template*` can find your template. Use the short
120+
command `,cd` or `(swank:set-default-directory "/home/you/path/to/app/")`.
121+
See also `asdf:system-relative-pathname system directory`.
122+
123+
{{% /notice %}}
124+
125+
### HTML template
126+
127+
This is our template. We use [Bulma
128+
CSS](https://bulma.io/documentation/elements/notification/) to pimp it
129+
up and to use its notification blocks.
130+
131+
```html
132+
<!DOCTYPE html>
133+
<html>
134+
135+
<head>
136+
<meta charset="utf-8">
137+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
138+
<meta name="viewport" content="width=device-width, initial-scale=1">
139+
<title>WALK - flash messages</title>
140+
<!-- Bulma Version 1-->
141+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/css/bulma.min.css" />
142+
</head>
143+
144+
<body>
145+
<!-- START NAV -->
146+
<nav class="navbar is-white">
147+
<div class="container">
148+
<div class="navbar-brand">
149+
<a class="navbar-item brand-text" href="#">
150+
Bulma Admin
151+
</a>
152+
<div class="navbar-burger burger" data-target="navMenu">
153+
<span></span>
154+
</div>
155+
</div>
156+
<div id="navMenu" class="navbar-menu">
157+
<div class="navbar-start">
158+
<a class="navbar-item" href="#">
159+
Home
160+
</a>
161+
<a class="navbar-item" href="#">
162+
Orders
163+
</a>
164+
</div>
165+
</div>
166+
</div>
167+
</nav>
168+
<!-- END NAV -->
169+
170+
<div class="container">
171+
<div class="columns">
172+
<div class="column is-6">
173+
174+
<h3 class="title is-4"> Flash messages. </h3>
175+
176+
<div> Click <a href="/tryflash/">/tryflash/</a> to access an URL that creates a flash message and redirects you here.</div>
177+
178+
{% for flash in flashes %}
179+
180+
<div class="notification {{ flash.first }}">
181+
<button class="delete"></button>
182+
{{ flash.rest }}
183+
</div>
184+
185+
{% endfor %}
186+
187+
</div>
188+
</div>
189+
</div>
190+
191+
</body>
192+
193+
<script>
194+
// JS snippet to click the delete button of the notifications.
195+
// see https://bulma.io/documentation/elements/notification/
196+
document.addEventListener('DOMContentLoaded', () => {
197+
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
198+
const $notification = $delete.parentNode;
199+
200+
$delete.addEventListener('click', () => {
201+
$notification.parentNode.removeChild($notification);
202+
});
203+
});
204+
});
205+
</script>
206+
207+
</html>
208+
```
209+
210+
Look at
211+
212+
```
213+
{% for flash in flashes %}
214+
```
215+
216+
where we render our flash messages.
217+
218+
Djula allows us to write `{{ flash.first }}` and `{{ flash.rest }}` to
219+
call the Lisp functions on those objects.
220+
221+
We must now create a route that renders our template.
222+
223+
224+
## Routes
225+
226+
The `/flash/` URL is the demo endpoint:
227+
228+
```lisp
229+
(easy-routes:defroute flash-route ("/flash/" :method :get) ()
230+
(djula:render-template* *flash-template* nil
231+
:flashes (or (ht:session-value :flash)
232+
(list (cons "is-primary" "No more flash messages were found in the session. This is a default notification.")))))
233+
```
234+
235+
It is here that we pass the flash messages as a parameter to the template.
236+
237+
In your application, you must add this parameter in all the existing
238+
routes. To make this easier, you can:
239+
- use Djula's [default template variables](https://mmontone.github.io/djula/djula/Variables.html#Default-template-variables), but our parameters are to be found dynamically in the current request's session, so we can instead
240+
- create a "render" function of ours that calls `djula:render-template*` and always adds the `:flash` parameter. Use `apply`:
241+
242+
~~~lisp
243+
(defun render (template &rest args)
244+
(apply
245+
#'djula:render-template* template nil
246+
;; All arguments must be in a list.
247+
(list*
248+
:flashes (or (ht:session-value :flash)
249+
(list (cons "is-primary" "No more flash messages were found in the session. This is a default notification.")))
250+
args)))
251+
~~~
252+
253+
Finally, this is the route that creates a flash message:
254+
255+
```lisp
256+
(easy-routes:defroute flash-redirect-route ("/tryflash/") ()
257+
(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.")
258+
(ht:redirect "/flash/"))
259+
```
260+
261+
## Demo
262+
263+
Start the app with `(start)` if you didn't start Hunchentoot already,
264+
otherwise it was enough to compile the new routes.
265+
266+
You should see a default notification. Click the "/tryflash/" URL and
267+
you'll see a flash message, that is deleted after use.
268+
269+
Refresh the page, and you won't see the flash message again.
29.8 KB
Loading

0 commit comments

Comments
 (0)