Page Map
  1. To Do List Web Page Tutorial Using ISSR
  2. Setup
  3. Making A List (of tasks)
  4. Checking It Twice
  5. Global Variables Are Naughty
  6. Make Adding Tasks Nicer
  7. Clear Text Box
  8. Press Enter To Add A Task
  9. What Is Next?

To Do List Web Page Tutorial Using ISSR

Try out the demo. Take a look at the full code if you have any trouble following.


We will use the Hunchenissr implementation of ISSR for this tutorial, so make sure that you have a Common Lisp compiler installed as well as Quicklisp. Then load the libraries we need: hunchenissr and markup (to generate HTML). Open a file index.lisp and enter the following to load the libraries and import the required symbols.

(ql:quickload '(hunchenissr markup))
(defpackage todo-demo
  (:use #:cl #:markup)
  (:import-from #:hunchentoot
  (:import-from #:hunchenissr

Enter the package name-space and enable HTML syntax.

(in-package #:todo-demo)

Start our Hunchentoot HTTP server on port 8080 with websockets listening on port 4433, and serving static files in the resources/ folder. Make sure you are serving issr.js in the resources/ folder.

(defparameter server
  (start (make-instance 'easy-acceptor
                        :port 8080
                        :document-root "resources/")
         :ws-port 4433)) 

Making A List (of tasks)

For now we will use a global variable for our list.

(defparameter todos (list)) 

Define an endpoint to go to in the browser.

(define-easy-handler (todo :uri "/todo")
    (;; GET parameter names go here
  (let (;; define local variables here

Setup ISSR connection. This just loads the small amount of JavaScript needed and connects the page to the server via websocket. See the documentation for more information.

     <script src="/issr.js"></script>
     <script noupdate="t">
       ,(format nil "connect(~a, 'ws', ~a)" *id* *ws-port*)

Render the todos using HTML generation. (Yes I know it is an empty list right now.) For now we will assume the todos is just a list of strings. The ,() calls a Lisp function and dumps the result into HTML. The ,@() calls a Lisp function and dumps each element of the resulting list into HTML.

  <h1>To Do List</h1>
    ,@(loop for todo in todos
              ,(progn todo)

Add a text box and a button for adding new tasks to the list. The name attribute of the input will be used to identify what the user typed in the text box. The action attribute of the button will be used to know when the button has been clicked. The onclick attribute of the button says to update the page when button is clicked and the this argument says to include the information that the button (with action="add-new-task" and value="add") was clicked.

    <input name="new-task"
           placeholder="New Task"/>
    <button action="add-new-task"

The way the information about the button is passed to the server is though GET parameters. action="add-new-task" and value="add" becomes ?add-new-task=add. Let's make use of this by adding some GET parameters.

(define-easy-handler (todo :uri "/todo")
    (;; GET parameter names go here
     add-new-task new-task) 

This tells our server (Hunchentoot) to expect a variable add-new-task in the query string like so: ?add-new-task=<something> and to parse it so that the value of the variable add-new-task is <something>. Now, when rr function is called by clicking the button, the add-new-task variable will have the value "add" as a string, and the new-task variable will be whatever the user typed in the text box.

We don't want to add an empty task, so let's use a local variable to calculate if we should add the contents of the text box.

(let (;; define local variables here
        (and add-new-task new-task
             (string= add-new-task "add")
             (not (str:blankp new-task))))) 

Now that we can easily know when we should add an item to the todo list, let us add it.

(when adding-new-task
  (setf todos (append todos (list new-task)))) 

You should be able to add items to the list now.

Checking It Twice

To mark items off the list, we will use a toggle button that will strike out the text. We also need to store the information of which items have been checked already. To keep track of which items are checked, instead of todos being a list of strings, it will be a list of lists where the first item of each sub-list is the checked state and the second item of each sub-list is the string. The idea is to be able to do (first todo) to get whether or not the todo do has been marked as completed, and (second todo) to get the actual task string. It should look like this:

((t "task1")    ;; checked
 (nil "task2")) ;; not checked 

To make sure that string already in todos don't interfere with this new schema, go ahead and convert it with map at the REPL:

TODO-DEMO> (setq todos (map 'list
                            (lambda (todo)
                              (list nil todo))

We will add the button right in front of the text in the li tag. To know which task to toggle, we will use the index of the task as the value of the button. To know if the text should be a strike tag or a regular span tag, we just check the first element of the sub-list (same with the button text).

,@(loop for todo in todos
        for index from 0 below (length todos)
          <button action="check"
            ,(if (first todo)
                 "Mark Not Done"
                 "Mark Done")
          ,(if (first todo)
               <strike>,(second todo)</strike>
               <span>,(second todo)</span>)

Update our GET parameters so that when the button is clicked the value of the check variable will be the index of task we need to toggle.

(define-easy-handler (todo :uri "/todo")
    (;; GET parameter names go here
     add-new-task new-task check) 

Right after we add a new task, we will check if we need to toggle the state of a task and do the toggle if necessary. Since HTML attributes are always strings, we have to parse-integer on the check variable.

(when (and check (not (str:emptyp check)))
  (let* ((check (parse-integer check))
         (todo (elt todos check)))
    (setf (first todo)
          (not (first todo))))) 

Global Variables Are Naughty

Instead of having a global todo list that everyone who goes to your web page will share, we will use a session variable so each user can have his or her own todo list.

To make this change all we need to do is remove the global todos (at the REPL),

(defparameter todos (list))

TODO-DEMO> (setq todos nil)
TODO-DEMO> (unintern 'todos) 

start the session at the beginning of our easy-handler, make the todos list as a local variable retrived from the session,

(let (;; define local variables here
      (todos (session-value 'todos)) 

and update the session variable (do this after toggling the checks).

  (setf (session-value 'todos) todos) 

Now, each person should have a unique todo list.

Make Adding Tasks Nicer

Clear Text Box

I found it to be quite nice to empty the text box when adding to the list. All we have to do is add two attributes to the text box. The value attribute set to empty string when we are adding a new task, and the update attribute to force the client to update the value. Since the server is unaware that the value of the text box actually ever changed, it thinks the value being empty string is nothing new and won't update it, but the update attribute (non nil) will force the update.

<input name="new-task"
       value=(when adding-new-task
       placeholder="New Task"/> 

Press Enter To Add A Task

Wouldn't it be nice to press the Enter button to add a new task instead of pressing the add-task button? This is where knowledge of HTML events and a slight bit of JavaScript is useful. We will use the onkeydown event and check the key that was pressed. The Enter button is keycode 13, so when the button the user presses is 13, we make a rr call that is exactly like the one called from the add-task button.

<input name="new-task"
       value=(when adding-new-task
       placeholder="New Task"
       onkeydown="if (event.keyCode == 13)

We have to use a JavaScript object with action and value instead of just this because this would refer to the input (with name="new-task") rather than the button (with action="add-new-task").

What Is Next?