ClojureScript - Single Page Application - A simple approach
This article is part of a series. We use the "book club" project to explore various programming languages and frameworks. Details of the book club's business and data requirements are detailed in a prior article, "Leveraging Ruby on Rails and ClojureScript.".
In this installment, we add create,update and delete (CRUD) functionality to our client.
Source code and build files are located on GitHub, at the book-site-clojurescript repository.
Single Page Application (SPA)
SPA and Mobile Devices
In addition, a SPA requires less traffic going across the network.
HTML Document Structure
Our html layout is set with a mobile display in mind. We are using the Pure CSS framework to provide a responsive design.
We added a new area of our UI (at the top portion of the display screen). I called the new area (a div) "entity-menu". Entity menu contains a child "ul", identified as "entity-menu-details". The application changes the contents of "entity-menu-details" as the user navigates through the application.
<body> <header id="header-container" class="header" style="text-align:center;"> <h4 id="menu-header>"</h4> <div class="pure-menu pure-menu-open pure-menu-horizontal" id="entity-menu"> <ul id="entity-menu-details"> </ul> </div> </header> </pre> </div>
DOM Event Listeners
As in the prior version of the client we load the event listeners once. The event listeners target a container element. The container element and the corresponding listener are both maintained throughout the application's life-cycle.
The strategy is to keep the task on managing listeners simple. The number of listeners should remain constant.
We load all of the event listeners when the application starts. In a web browser, the "document ready" event signals the application start. The following snippet demonstrates.
(defn doc-ready-handler  (let[ ready-state (. js/document -readyState)] (if (= "complete" ready-state) (do (add-router) (do-ajax "GET" "../rest/export/book/all" ajax-response-handler) (do-ajax "GET" "../rest/export/authors/all" ajax-response-handler) (do-ajax "GET" "../rest/export/review/all" ajax-response-handler) (do-ajax "GET" "../rest/export/category/all" ajax-response-handler) (do-ajax "GET" "../rest/export/book_category/all" ajax-response-handler) )))) (defn on-doc-ready  (aset js/document "onreadystatechange" doc-ready-handler )) (on-doc-ready)
The last statement (on-doc-ready) is our application entry point.
(on-doc-ready) invokes the on-doc-ready function. The on-doc-ready function simply assigns the doc-read-handler function as the DOM listener for "ready state change".
Once the Document's ready state is "complete", the add-router function is called.
(defn add-router  (let [ report (by-id "report") menu (by-id "footer-menu") entity-menu-details (by-id "entity-menu-details")] (.addEventListener report "click" list-click-listener true) (.addEventListener menu "click" menu-listener) (.addEventListener entity-menu-details "click" entity-menu-listener) ))
The add-router function loads our event listeners.
The following snipplet demonstrates the "entity menu" listener function.
(defn entity-menu-listener [event] (let [ target-elem (. event -target) target-id (. target-elem -id) dataset (aget target-elem "dataset") ] (cond (= target-id "add-book-menu-item") (render-add-new-book) (= target-id "modify-book-menu-item") (render-modify-book (aget dataset "bookId") ) (= target-id "delete-book-menu-item") (render-delete-book (aget dataset "bookId") )
Note! We use the HTML5 "data" attributes to store the entity id. In the above example, we store the Book Id.
CRUD Process Flow (Function Call Sequence)
As mentioned at the top. The primary enhancement we made to our Clojurescript client is create, update and delete functions (CRUD).
In this section of the article, I thought it might be interesting to take an example CRUD operation, "Add New Author" (a successful operation). We'll walk through the function call sequence. This section is intended as a companion to the source code listing books.cljs.
I'll notate functions in bold. I won't include the function parameter list in the following notation. However, I will use the Clojure symbol of "" to suffix each function.
I'll notate user actions in italic.
Again, the idea is to get a sense of the function flow.
- The default display for the client is performed by render-book-list. render-book-list displays an "Add New Author" link at the top of the display.
- User clicks on "Add New Author".
- The entity-menu-listener function captures the the "click" event and routes the request to the
- Clears the "main content" area (removes elements and marks them for garbage collection).
- Creates a new set of html elements which represent the "Add New Author" dialog.
- Generated html includes
- An "id" attribute which defines what function should be submitted to the server.
- "id" attributes for each Author field (E.G. first name, last name).
- Renders the "Add New Author Dialog".
- The user keys in the author's first and last name.
- The user clicks the "submit" button.
- The list-click-listener function captures the click "button" DOM event. list-click-listener routes the request to process-submit.
- The process-submit function inspects the form/dialog values and routes the request to the submit-add-author function.
- submit-add-author  function
- Collects values the user entered in the "Add Author" dialog.
- Encodes string values, so the string values can be sent from the client to the server over the internet.
- Invokes the "do-ajax " function with the proper HTTP method (POST), URL, data and call-back function (crud-response-handler).
- The crud-response-handler function is a DOM readystate listener.
- The web browsers calls "crud-response-handler" each time the ajax "readystate" property changes.
- Inspects the readystate value.
- Parses the server's response when a readystate value of "4" is signaled by the web browser.
- Inspects the parsed response and determines whether the response is a success or exception response (failure).
- Invokes the crud-response function if the server returned an application success message.
- Invokes the render-error-message if the server returns an application failure message.
- Invokes the render-http-exception function if the server returns an HTTP communication exception.
- The crud-response function
- Determines which entity (or relationship) the response is related to.
- Invokes the corresponding function. For Authors, the request is routed to process-author-op
- The process-author-op function
- Inspects the "action" field of the response message
- Determines whether the successful operation was either a ADD, MODIFY or DELETE.
- In this case, we have an ADD, thus the request is routed to the addAuthor function.
- The addAuthor function
- Creates a new Author record.
- Modifies the AuthorList vector by appending the new Author record to AuthorList
- Control is returned to process-author-op
- process-author-op routes the request back to render-book-list
Rendering HTML Forms
Rendering forms with Clojurescript is actually quite simple. Since Clojurescript is a Lisp, it's easy to organize tasks into functions. Let's take a quick example:
(defn render-modify-book [book-id ] (let [ report-details (by-id "report-details") form (create-form) book (get-list-item-by-str BookList book-id) author-select-ctrl (author-select (:author_id book)) title-group (create-input-control-group (:title book) "title" "Book Title") id-elem (create-input-elem "hidden" (:id book) "book-id") button (create-command-button "Submit" "data-submit" "mod-book" "mod-book") ] (clear-menu-list) (set-menu-header "Modify Book") (append form title-group) (append form id-elem) (append form author-select-ctrl) (append form button) (append report-details form)))
The "Modify Book Form" contains a Author select control. To statement "author-select-ctrl (author-select (:author_id book))" created the Author select control. The statement "author-select-ctrl (author-select (:author_id book))" adds the Author select control to our form.
Let's look at function that creates the the Author select control.
(defn author-select (  (let [select-ctrl (create-elem "select") ] (doseq [author (deref AuthorList)] (append select-ctrl (create-option-elem (:id author) (format-name (:first_name author) (:last_name author) ))) ) (addAttribute select-ctrl "name" "author-select") (addAttribute select-ctrl "id" "author-select-id") select-ctrl )) ( [select-id] (let [select-ctrl (create-elem "select") ] (doseq [author (deref AuthorList)] (append select-ctrl (create-option-elem (:id author) (format-name (:first_name author) (:last_name author) ) select-id )) ) (addAttribute select-ctrl "name" "author-select") (addAttribute select-ctrl "id" "author-select-id") select-ctrl )) )
We can call the author-select function 2 different ways.
- With no parameters.We don't have to pre-select an Author
- With one parameter, the "select-id". The author_id (key) of the Author we need to pre-select
We simply iterate through the AuthorList vector, build and populate the <select> control.
I found it a challenge is to reconcile Clojure's un-mutable data structures with a small constrained resource like a mobile web browser. Clojure is designed for environments that have unlimited storage (E.G. cheap SSD memory). In a mobile web browser we don't have that luxury (we have limited memory which is managed by a garbage collector). Thus we have to revert to "mutable" data structures.
The most difficult function for me to implement was "delete". So let's look at the details.
(defn remove-item [vector-list item-id] (let[ array-instance (into-array (deref vector-list)) item (get-list-item-by-id vector-list item-id) item-index (.indexOf array-instance item) end-split (+ item-index 1)] (.splice array-instance item-index end-split) (reset! vector-list (vec array-instance))))
Again, let's use Authors as our example.
For me, the implementation works well.
As mentioned at the top, this article is part of a series focused on the "book club" project.
For this release of the book club project:
Changes to the Server tier are discuses in REST-MVC using Java
Testing the Server Tier discussed in Testing the Java REST-MVC Server Tier
Testing the Clojurescript Client is discussed in Testing Clojurescript - A simple approach
GitHub Repository for Server tier: book-site-jpa
GitHub Repository for Clojurescript Client: book-site-clojurescript
Open for work (contract or hire).
Drop Lorin a note. Click here for address.
Please no recruiters :)