Login

Leveraging ClojureScript.

This article is part of a series. The previous article Leverage Ruby on Rails and ClojureScript detailed the project's business requirements. In this article I'll detail the application's mobile client implementation. The mobile client implementation is written in ClojureScript.

Complete source code is available on my GitHub repository here.

What is ClojureScript?

Clojure is a Lisp dialect. Clojure's default implementations run on the Java Virtual Machine (JVM) and Microsoft's CLR.

ClojureScript is a compiler, written in Clojure. The ClojureScript compiler emits (generates) JavaScript.

The programmer writes client code in ClojureScript. The programmer compiles the Clojurescript. The ClojureScript compiler generates JavaScript.

Why ClojureScript

There is a popular theory that holds the less source code statements, the lesser chance for bugs, the greatest number of functions can be provided (AKA -- an application can be more complex.) Clojure and ClojureScript expressions are very short and concise. Let's look at an example.

The following code adds a HTML document ready listener. The ready listener then sends a set asynchronous HTTP requests to our server. Each request to the server is assigned a separate "handler function". The handler functions (not shown here), parse the server's response.

(defn doc-ready-handler []
  (let[ ready-state (. js/document -readyState)]
    (if (= "complete" ready-state)
      (do
        (add-router)
        (doget "GET" "/books" ajax-response-handler)
        (doget "GET" "/authors" ajax-response-handler)
        (doget "GET" "/reviews" ajax-response-handler)
        (doget "GET" "/categories" ajax-response-handler)
        (doget "GET" "/book_categories" ajax-response-handler)
      ))))

(defn on-doc-ready []
  (aset  js/document "onreadystatechange" doc-ready-handler ))

(on-doc-ready)

The first thing you might note is, I am using vanilla JavaScript services. There are many high level JavaScript libraries which ease the development process. Libraries such as JQuery and BackBone have plenty of use cases. The ClojureScript community has already contributed libraries that provide calls the JQuery via ClojureScript.

One of the main focuses of this project is a learning experience. I am not adverse to high level libraries. High level libraries like JQuery and Backbone have very good use cases. I'm using vanilla JavaScript here to follow the "walk then run" philosophy. Much of this project is simply a learning device.

Translation Please
Let's translate the above ClojureScript code.

  • "defn" defines a function. We defined 2 functions, doc-ready-handler and on-doc-ready.
  • "aset" assigns a value to a an application object. In the function "on-doc-ready", we assign a "ready state" listener to our document.
  • "js/document" is the connotation for our document. "js/" designates name space. In this case, the name space is js (vanilla JavaScript). We are instructing ClojureScript to issue a vanilla JavaScript instruction.
  • "doget" is a call to custom function (shown below)

The "doc-ready-handler" function is called by our document several times as the document is loaded in to the web browser. The "doc-ready-handler" function reads the "readyState" document property to determine whether the document is "ready". In other words, this is the same functionality as JQuery's ready() function.

The last expression, (on-doc-ready), runs the function "on-doc-ready".

We are calling a function "doget" several times. That function sends an asynchronous HTTP GET request to the server. The function sets the GET request headers so that the server responds with a JavaScript Object Notation (JSON) response. Finally, the function assigns a listener callback function ("handler-function"). "handler-function" processed the server's response when it arrives. Here is my implementation:

(defn doget [request-type url handler-function ]
  (let [x  (js/XMLHttpRequest.)  ]
    (aset  x "onreadystatechange" handler-function )
    (.open x request-type url)
    (.setRequestHeader x "Content-Type" "application/json" )
    (.setRequestHeader x "Accept" "application/json" )
    (.send x)))

If I want to use ClojureScript, what do I need to know?

A good question.
In the above code, let's talk about what the ClojureScript compiler validates and what it doesn't validate.

The ClojureScript compiler validates your Clojure statements. If you are missing delimiters, the ClojureScript compiler issues an exception and indicates your code can not be compiled. The ClojureScript compiler will also issue a warning (with line numbers) if your code is using a undeclared variable. For example, you misspell "doc-ready-handler" as "doc-ready-handler".

However, the ClojureScript compiler does not validate the document properties, JavaScript object properties or CSS properties.

For example, if you try and add an inline CSS rule to an element with misspelled CSS attribute, your application will silently fail. For example. you misspell "font-weight" as "fnt-wait". Your code will compile, but when you inspect the html source, your inline css will not have font instruction. So your html tag might have a "style" processing instruction, but the value for the "style" instruction is not going to contain a misspelled fnt-wait rule. You have to figure out, "fnt-wait" should be "font-weight".

Same applies to JavaScript properties. Lets say I called the AJAX methods out of order. I called "setRequestHeader" before I opened the xhr object. The ClojureScript compiler compiles the code, but during run time, the web browser will throw an exception.

Thus, back to original question, "What do I need to know?".

Well, if you want to implement a very lean client that uses vanilla JavaScript, then, you need to know how to code vanilla JavaScript.

Same applies for CSS and the DOM (Document Object Model). If you want to manipulate CSS or the DOM directly, you need to know what a valid DOM expression is, you need to know what a valid CSS rule and expression is.

And finally, of course, you need to be familiar with the ClojureScript language and build tools.

Do I need to know Java or CLR?

If you are running ClojureScript on a Java Virtual Machine (JVM), it helps to know Java, but it's not a requirement. The same applies to Microsoft's CLR, it helps, but it is not a requirement.

When the ClojureScript compiler can't compile your code, a stack trace is displayed. The stack trace may reference intermediary code the compiler is generating. So if are familiar with the intermediary platform code (E.G. Java language or CLR constructs), the stack trace is easier to dissect.

What about Google's JavaScript Compiler and Google's Closure library?

The ClojureScript compiler uses the Google JavaScript compiler. Google's JavaScript compiler is bundled with the Google "Closure" library. Don't confuse "Closure" (with an S ) with "Clojure" (with a J). The ClojureScript build tools take care of the integration and compilation for you.

Thus, you can use the high level functions provided by the Google "Closure" JavaScript library. However, you are not required to use the "Closure" functions.

Compiler Levels

In my example code, I am not using any explicit high level calls.

However, when you are building your application, it helps to understand how the build process works and how the "Closure" library and compiler fit in to the process.

When you build your ClojureScript application, you can set the optimization level of the compiler. Levels can be set to:

  • "white space"
  • "simple"
  • "advanced".

I started out with "white space". I ended up with "advanced" compilation. You run all three modes through a JavaScript source code debugger. The difference is, it's easiest to identify your ClojureScript expressions in "white space" mode.

Let's look at the output portions for our "ready-state-handler" function;

White Space:

book_review.books.doget = function doget(request_type, url, handler_function) {
  var x = new XMLHttpRequest;
  x["onreadystatechange"] = handler_function;
  x.open(request_type, url);
  x.setRequestHeader("Content-Type", "application/json");
  x.setRequestHeader("Accept", "application/json");
  return x.send()
};
book_review.books.doc_ready_handler = function doc_ready_handler() {
  var ready_state = document.readyState;
  if(cljs.core._EQ_.call(null, "complete", ready_state)) {
    book_review.books.add_router.call(null);
    book_review.books.doget.call(null, "GET", "/books/mob/index", book_review.books.ajax_response_handler);
    book_review.books.doget.call(null, "GET", "/authors", book_review.books.ajax_response_handler);
    book_review.books.doget.call(null, "GET", "/reviews", book_review.books.ajax_response_handler);
    book_review.books.doget.call(null, "GET", "/categories", book_review.books.ajax_response_handler);
    return book_review.books.doget.call(null, "GET", "/book_categories", book_review.books.ajax_response_handler)
  }else {
    return null
  }
};
book_review.books.on_doc_ready = function on_doc_ready() {
  return document["onreadystatechange"] = book_review.books.doc_ready_handler
};
book_review.books.on_doc_ready.call(null);

Simple:

book_review.books.doget = function(a, b, c) {
  var d = new XMLHttpRequest;
  d.onreadystatechange = c;
  d.open(a, b);
  d.setRequestHeader("Content-Type", "application/json");
  d.setRequestHeader("Accept", "application/json");
  return d.send()
};
book_review.books.doc_ready_handler = function() {
  return cljs.core._EQ_.call(null, "complete", document.readyState) ? (book_review.books.add_router.call(null), book_review.books.doget.call(null, "GET", "/books/mob/index", book_review.books.ajax_response_handler), book_review.books.doget.call(null, "GET", "/authors", book_review.books.ajax_response_handler), book_review.books.doget.call(null, "GET", "/reviews", book_review.books.ajax_response_handler), book_review.books.doget.call(null, "GET", "/categories", book_review.books.ajax_response_handler),
  book_review.books.doget.call(null, "GET", "/book_categories", book_review.books.ajax_response_handler)) : null
};
book_review.books.on_doc_ready = function() {
  return document.onreadystatechange = book_review.books.doc_ready_handler
};
book_review.books.on_doc_ready.call(null);

Advanced:

function nl(a) {
  var b = new XMLHttpRequest;
  b.onreadystatechange = ml;
  b.open("GET", a);
  b.setRequestHeader("Content-Type", "application/json");
  b.setRequestHeader("Accept", "application/json");
  return b.send()
}
document.onreadystatechange = function() {
  if(J.b("complete", document.readyState)) {
    var a = lj("report"), b = lj("footer-menu");
    a.addEventListener("click", Nk, g);
    b.addEventListener("click", Ok);
    nl("/books/mob/index");
    nl("/authors");
    nl("/reviews");
    nl("/categories");
    return nl("/book_categories")
  }
  return j
};

Size of Generated JavaScript
Let's look at file sizes:

  • White space: 1058289 bytes.
  • Simple: 700038 bytes
  • Advanced: 145061

Now, the sizes above are for my full application. See the full source on my GitHub repository. But let's see how the sizes compare.

  • From white space to simple, size is reduced by:
    • 1058289 - 700038 = 358251 bytes
    • So about a 33% reduction in size.
  • From white space to advanced.
    • 1058289 - 145061 = 913228
    • About an 87 percent size reduction.

Coding for "Advanced" ClojureScript compilation.

Getting and Setting Properties.

Here is the same ClojureScript expression coded 2 different ways:

(. raw_list -authors)

(aget raw_list "authors")

Both expressions are evaluating a JSON object. They are checking to the see if the JSON object "raw_list" contains a member property called "authors".

When you are coding (and testing) in the lowest compilation level, "white space" both expression work fine. However, when you switch to "advanced", only the second version works as expected.

The above is an example of a object "getter". We are trying to get something from an object. Let's look at the reverse, we want to "set", put something in to an object. Same thing, both expressions do the same thing:

(set! (. dom -innerHTML) content))

(aset dom "innerHTML" contet)

In both expressions we are setting the innerHTML of a dom element. Again, only the second expression works as expected in "Advance" compilation mode.

I wanted to point that out. I read plenty of example of code from various internet sources which used the first expression form. Again, those expressions are valid, but the compiler in "Advanced" mode does not produce the expected results. Thus, if you are going to ultimately use "Advance" mode, use the second expression for (aget and aset).

Managing Memory

First a little background. After I got my ClojureScript application running, I profiled it using Google's Chromium Developer tools. Specifically, the "Timeline" tool The "TimeLine" measures memory use, but it also details the number of DOM nodes you application is using. I became concerned as I saw the number go DOM nodes continue to rise. I started to research the issue.

I was using a un-ordered list (<ul>) with an id of "report-details" to display each new query. So, when I wanted to replace a list of Book titles, with a list of Authors, I would do the following. I would capture a list of the "report-details" child elements and then issue a DOM removeChild. I then would create a new set of elements which contained the Author information and then append them to "report-details". That worked, at least the display worked. But as I detail below, the "removed" elements were only removed from the document. The garbage collector was not removing them from memory.

I'll also add that my application's architecture was very simple. I wasn't mainlining a set of "view" objects which kept DOM element references. So, the only references, were the elements (objects) themselves.

I tend use the Mozilla organizations documentation. So I started my research for removeChild. The Mozilla references makes a point that "removeChild" does not remove the child from memory. A different article here explains that you need to remove all references to an object. Once all references are removed, the garbage collector can reclaim the memory. In many examples, I found folks setting an object to null. For example:

var someObject = {};
someObject.name = "Joe";
someObject.name = null;
someObject = null;

That's simple enough to write in straight JavaScript, however, I was little confused. I couldn't find a way to get ClojureScript to emit that simple express (E.G. object = NULL).

I perused the Google Clojure library documentation. I found someting close, but not quite what I wanted. The Google Object provide both a remove() , and a clear() method. I looked at the source. both methods were deleting the properties of the object, however there was no null assignment (E.G. obj = null). I also looked at the ClojureScript source code, line 1131, there is method called js-delete(). Again, like Google's object js-delete need a property as an argument. I tried js-delete in my code and I got an expression like node[property] = null. Close, but I not quite there.

After thinking about the issue a while. I started to wonder whether I was missing something obvious. Missing something that was obvious to everyone. Obvious to everyone, except, me.

Both Google and ClojureScript had methods which removed properties from an object. Hmmm, what if removing all the properties from an object had the same effect as "object = null". Maybe I didn't need a "object = null" assignment. Maybe deleting all of the objects properties was the same as a null assignment.

So, I tested the new theory. I added a method:

(defn set-null [node]
  (gobject/clear node)
)

set-null simply calls the Google library function object.clear. I then added a call to set-null for each element I was removing from the DOM. I then re-tested my application with "TimeLine", bingo, that was it.

Using Google's Chromium Development Tools to measure resource consumption

"TimeLine" (a part of Google's Chromium Developer tools) details when events like 'Garbage Collection (GC)" occur. I found the number of DOM nodes drop significantly soon after a GC event. In addition, I realize "Timeline" also contains a link that let's the developer invoke the GC event directly. The is an icon and line called "Collect Garbage".

I don''t know if that was a Homer Simpson "doh" moment, or a genious like "ahah I now understand" moment? Either way, I thought it was interesting!

Here is my completed code which clears the "report-detail" <ul> container:

(defn empty-elem [elem]
  (let [  child-list (aget elem "children")
          child-seq (array-seq child-list)
          cnt (-count child-seq)
          index-vals (range cnt)]
          (doseq [index index-vals]
            (do
                (gdom/removeNode  (-nth child-seq index))
                (gobject/clear  (-nth child-seq index))
            )
          )))

Looking at the code through a Microscope

I create a new array of the elements I want to remove. The reason for a second array collection is, I am preventing a clash with the vector's iterator. You don't want to open an iterator and then start deleting elements the iterator is pointing to. That approach leads to unexpected results.

Instead, we create a simple array to hold the elements we want to delete. We then address each element by it's position in the array (E.G. first element is located at position zero). We are able to loop through the array without using an iterator object.

In addition, as I select each element. I then perform 2 actions:

  • Remove (detach) the element from the document
  • Remove references (properties) to the element. Thus, marking it for garbage collection.
  • The statement "child-seq (array-seq child-list)" creates an array called "child-seq". The elements stored in the "child-list" vector are then copied in to the "child-seq" array.
  • I need to simulate a standard "C" for loop (E.G. for (int index = 0; index < num_elements; index++) {...". So I first get the number of elements with the following statement "cnt (-count child-seq)".
  • Now I need to simulate the index++ (increment index). The statement "index-vals (range cnt)" create a list of numbers. So if we had 5 books the list would look like this (0 1 2 3 4).
  • The statement " (doseq [index index-vals]" iterates through the list (0 1 2 3 4). During each iteration, the local variable "index" is assigned the value in our "index-vals" list. So the first iteration "index" is assigned 0. The second iteration, index is assigned 1, etc, etc.
  • Thus, I simulated the for loop.
  • Now I can address each element in the array with the following statement "(-nth child-seq index)" .
  • I pass the element to Google Clojure library function removeNode with " (gdom/removeNode (-nth child-seq index))".
  • I pass the element to Google Clojure library function clear object with " (gobject/clear (-nth child-seq index))".

Structuring the Application Logic
ClojureScript is a "functional language". Therefore the program logic focus on functions. However, we do have some data structures. For this application, I maintained a very simple architecture. We have 4 entities (authors, books, categories, reviews) and one relationship (book_categories). Each of the entities (and the book_category) is implemented with the same pattern

I use Author as the example. First we start by defining a Author structure:

(defrecord Author [ id  first_name last_name book_count])

Next, an Author collection:

(def AuthorList (atom []))

Note! "atom[]" is a mutable vector collection.

We now need a method which converts the JSON representation of an Author to or ClojureScript representation:

(defn jsonToAuthor [jsonObj]
  (let [  id (aget jsonObj "id") book_count (aget jsonObj "book_count")
           first_name (aget jsonObj "first_name") last_name  (aget jsonObj "last_name")]
    (Author. id  first_name last_name book_count )))

A method to a single Author to our Author collection (list):

(defn addAuthor
  ([id first_name last_name book_count]  
    (swap! AuthorList conj (Author. id first_name last_name book_count)))
  ([jsonObj] (swap! AuthorList conj (jsonToAuthor jsonObj)) )
)

Note! You can write a single ClojureScript method with multiple signatures. In this case, addAuthor will either accept a single JSON object, or a list of attributes (fields, arguments).

Our AuthorList is a simple vector. Our BookList, CategoryList, ReviewList and BookCategoryList are all coded the same way. The application requires some dynamic queries. First a generic method to retrieve a structure out the collection by id:

(defn get-list-item [list id]
  (let [  derefed-list (deref list)
          results (filter ( fn[n]  (= (str (:id n )) id)) derefed-list )]
      (first results))
)

We also need to retrieve related structures. For example, when displaying the details of book review, I want to display the Book title and the Author name along with the review details. That means we need to get related data out 3 different collections.

  • Review
  • Book
  • Author

Here is a function to get a related structure:

(defn get-related-list-item [list id]
  (let [  derefed-list (deref list)
          results (filter ( fn[n]  (=  (:id n ) id)) derefed-list )]
      (first results))
)

Here is how the function get-related-list-item is used to gather our Book Review Details:

(defn render-review-details [review]
.....
  author (get-related-list-item AuthorList (:author_id review))
....
  book (get-related-list-item BookList (:book_id review)) 
....

Managing Listeners
As I mentioned above. The overall architecture of the ClojureScript client is very simple. I load two listeners upon document ready:

  • A menu listener. I have a simple menu located at the bottom (footer). The menu listener listens from click events on the footer menu.
  • A report listener. The "report-details" <ul%gt; container is also created on document ready. The application dynamically populates the "report-details" container. Child elements are assigned "data" attributes. The data attributes represent both what report is being requested and what the report parameter is (E.G. book_id). Either the element clicked or it's parent always contains the pertinent information.

Thus, I manage the listeners by keeping a static set. Here is my menu listener implementation:

(defn menu-listener [event]
  (let
    [ target-elem (. event -target) target-id (. target-elem -id)]
    (cond
      (= target-id "author-list-menu-item") (render-author-list)  
      (= target-id "book-list-menu-item") (render-book-list)
      (= target-id "category-list-menu-item") (render-category-list)
      (= target-id "review-list-menu-item") (render-review-list)
    )))

The menu-listener accepts a single argument (event). The "event" argument is a DOM event. As I mentioned at the top of this article, you need to know something about to the underlying technology you are programming. Here, I already established (knew) that a DOM event contained a "target" property. I am a also referencing the optional HTML element property "id".

Again, I mentioned at the top of the article. You could use a ClojureScript library which wraps around a high-level DOM manipulation library like jQuery. I'm not adverse to that strategy, I simply wanted to learn how to "walk the run" :).

Managing the User Interface (UI)
I am using the Pure UI CSS framework to layout the client UI. If you peruse the full source code for this project, you'll see that I am assigned Pure CSS classes to many of the HTML elements. Pure is very lightweight, and simple to extend.

I added a small CSS file to customize the layout. For example. I wanted to reduce the footed menu padding when the application is rendered on a small mobile device. Here is the simple media query and CSS rule set I implemented;

@media all and (max-width: 480px) {
    .pure-menu li a {
         padding: 5px 8px;
    }
}

The ClojureScript Build Work Flow
I wanted to conclude this article with a brief mention of the build process. I used the Leiningen build tool. Leiningen will generate the boiler plate for a new Clojure project. You then edit the project file (.prj), and add the ClojureScript plugin and a reference to library dependencies. So here is my project definition:

(defproject book-review "Rel 1.0"
  :description "Book Review Site"
  :url "http://public-action.org"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.5.1"]]
  :plugins [[lein-cljsbuild "0.3.2"]]
  :cljsbuild {
    :builds [{:source-paths ["src/cljs"]
              :compiler {:output-to "resources/public/books_cljs.js"
                         :libs ["closure/library/third_party/closure"]
                         :optimizations :advanced
                         :pretty-print true}}]}
)

Note! At the bottom we have the "optimizations" setting. You can change "advanced" to "whitespace" or "simple". Leiningen contains a help facility. I chose to "auto" build mode. That means the ClojureScript compiler remains running, it caches the intermediary compile results. When you update your ClojureScript source (write file to disk), the compiler generates a new JavaScript file.

I ran a few xterm sessions:

  • VI to edit my ClojureScript source code.
  • Leiningen in "auto" build mode.
  • A bash command file to copy the JavaScript file in to my ROR asset section.

You invoke the following command line to start "auto" build:

$> lein cljsbuild auto

I should note that I running Clojure, ClojureScript and Leiningen on the Java Virtual Machine (JVM). Thus, Leiningen is using Apache Maven to manage dependencies. Thus, I could automate the process of copying the JavaScript result to my ROR installation. For this article, I wanted to keep the focus on ClojureScript. Besides, often forget that Java package managers like Ant or Maven are optional to a JVM build. Anything you do with Ant or Maven, you can do from the command line manually. The build tools simple automate those tasks.

Again, you can peruse the complete source code on my GitHub repository here.

Note! The article "Leveraging Ruby on Rails" details my server implementation using Ruby on Rails Version 4.

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 :)