Login

ClojureScript - Single Page Application - A simple approach

Article Summary
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.".

This article details enhancement to our client tier implementation using ClojureScript. Clojurescript is a compiler which takes Clojure source code and emits (generates) JavaScript. Please see our prior article Leveraging Clojurescript for details on Clojurescript, as well as details on the client's business requirements.

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)

The JavaScript client dynamically renders web pages in the web browser. The JavaScript client is referred to as a "single page app".

To the user, the application appears to be displaying multiple web pages. However, technically, the web browser has loaded one single html document (and JavaScript engine instance).

In a traditional web application, the server generates html pages. Each time the web browser requests a new "page", the web browser loads a new instance of the JavaScript loader. On a desktop web browser, the traditional architecture works fine.

SPA and Mobile Devices

On a mobile web browser loading a new instance of the JavaScript engine is not a trivial task. SPA loads a single JavaScript engine (once). Thus, SPA is beneficial on a mobile web browser.

In addition, a SPA requires less traffic going across the network.

On a mobile client, both of these optimizations (reduced network traffic, a single javascript engine instance) are worth considering.

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
    render-add-new-author[] function.
  • render-add-new-author[]
    • 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.
    • crud-response-handler[]
      • 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.

Data Structures

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.

AuthorList is a Clojurescipt vector. A vector is similar to an array, but it's not an actual array. Thus, we make a copy of the AuthorList data and convert to an array (using into-array[]). We now have a JavaScript array we can work with.

We then use the Javascript array member function "indexOf" to determine the Author's position in the array.

We then use the Javascript arry member function "splice" to remove the element.

Finally we convert the resulting JavaScript array back in to a Clojurescript vector (I.E. we replace the data in AuthorList).

Summary

We've implemented full CRUD functionality, including AJAX calls and dynamic page display. All with just Clojurescript. We use Google's Closure library to remove all references to DOM objects we are discarding (see previous article for details ) . Other than that, the whole client is done with vanilla JavaScript.

For me, the implementation works well.

It should be noted, our user interface (UI) is pretty simple. We've used the Pure CSS framework to do the heavy lifting for our layout. Perhaps if we add a more complex user interface (E.G. a popup navigation menu) we can use either some of the Closure components or another JavaScript library. But for right now, a simple approach seems to work well.

Article references.
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

About the Author:
Lorin M Klugman - I'm an experienced developer. My main interest is in new technology. Please use our contact box here if you are interested in hiring me. Please no recruiters :)