Getting started with ClojureScript and Noir #2

21 Feb 2012

In a previous blog post we created the infrastructure necessary to create a Clojure/ClojureScript environment with lein-cljsbuild and noir, the aim of this post is to build on that foundation by developing a simple application that demonstrates some of the abilities that ClojureScript/Clojure can offer.

Please note that as I’m not really a frontend developer by trade, my code is probably far below the realms of “best practise”, I’m learning as I go along and these are generally just my notes on what I’ve found, so take from that of what you will.

The Application

In this post we’re going to discuss the basics of creating a crude twitter search application that will display tweets the user has requested from the server

Pre-requisites

The sourcecode for this can be retrieved from this repository it is built on the code written in the previous blogpost with some modifications.

UPDATE 22/02/2012: Make sure you checkout the correct branch, i.e.

git clone https://djhworld@github.com/djhworld/testproject.git
git checkout twitter-post2

I’ve run this project successfully on MacOSX and Windows XP (via Cygwin) but I’ve been hearing of people having trouble with Windows itself which I’m not yet sure how to fix.

This post is designed to be used as a reference for the changes that have been made but does not include all of them, which is why I recommend you read through the code yourself.

It is worth nothing that this is using jQuery placed under resources/public/js

Dependencies

To make life easier (and better!) we will be using some additional libraries. Most notably: -

These are referenced in project.clj as follows: -

:dependencies [[org.clojure/clojure "1.3.0"]
               [noir "1.2.1"]
               [clj-http "0.2.6"]
               [cheshire "2.2.0"]
               [jayq "0.1.0-SNAPSHOT"]
               [crate "0.1.0-SNAPSHOT"]
               [fetch "0.1.0-SNAPSHOT"]]


Server side view

To start with we need to develop the datafeed that the client will request when they search for a hashtag. The entry point for this will be a fetch “remote” method that will return a map of tweets as a result of the search request.

src/testproject/views/welcome.clj

(defremote load-tweets [hashtag]
  (when-let [tweets (tw/search hashtag)] ;do search request
    (let [{:keys [results]} tweets] ;get the results (tweets) from the response
      (map simplify-tweet results))))

With that completed now all we need to add is an entry point for our page by adding a new route to produce some HTML. Notice how nearly all the elements being generated will be empty - we will be using the client side to populate these dynamically, and CSS to style them accordingly.

(defpage "/welcome" []
  (common/layout
  [:div#header
   [:h2.pagetitle "Twitter Searcher"]]
     [:div#inputbar
       [:input#tag {:type "text"}]]
       [:div#tweetsbox
         [:div#tweets]]))


Client side

The client side logic will run as follows: -

To contact the server to retrieve the tweets and display them, we will use fetch to call our server-side function

(defn load-tweets []
  "Function that calls the server to retrieve tweets"
  (when-let [tag (get-hashtag)]
    (fm/remote (load-tweets tag) [tweets]
      (when-not (= [] tweets)
        (let [ftweets (flatten (map vals tweets))]
          (display-tweets ftweets))))))

But how do we trigger this event to happen? We want to call this when the user has hit the return key in the searchbox at the top.

Thankfully the jayq library provides a bind function to make this a little simpler, allowing you to bind events (e.g. keypress, click, mouseover etc) to functions. However to determine what key has been pressed, for example, you need to capture the details of the event that has occured and the only way to do this (as far as I can tell) is make the event-trigger function a partial to ensure jQuery will pass the JS argument to the function you tell it to.

I’ve added some convienience functions under a new namespace to make this process a little easier: -

(ns testproject.utils
  (:require [jayq.core :as jq]))

(def enter-key 13)

(defn- key-pressed [key-code func event]
  "If keypressed = keycode then call func"
  (when (= (.-keyCode event) key-code)
    (func)))

(defn return-key-pressed [f]
  (partial key-pressed enter-key f))

(defn get-value [selector]
  "Get the value of a selector, e.g. an input box"
  (let [value (jq/val selector)]
    (when (not= value "")
      value)))

Then we simply have to add this code at the bottom of our script to bind the event to when the enter key is pressed.

(jq/bind (selectors :hash-tag) :keypress (return-key-pressed load-tweets))

Finally to display our collection of tweets we will use the display-tweets function to append a series of <div> elements to the page. To create these elements we will use the crate library, which is just like Hiccup that we can use client side.

(def selectors
  (hash-map
    :tweet-list (jq/$ :#tweets)
    :hash-tag (jq/$ :#tag)))

(defpartial tweet [id img text user]
  [(keyword (str "div#tweet-" id ".tweet")) 
    [:img.tweet {:src img }] 
    [:p.user user] [:p.text text]])

(defn display-tweets [tweets]
  (jq/prepend (selectors :tweet-list)
    (crate/html [:ul
      (map
        (fn [{:keys [id profile-img text user]}] 
          (tweet id profile-img text user))
        tweets)])))


Conclusion

Feel free to try it out by running the project, ensure that your Clojurescript compiles and run your server with lein run and lein cljsbuild once and navigate your way to http://localhost:8080/welcome, type in a search term (how about ‘Clojure’) and hit the return key.

Obviously there are immediate holes in this design, what if the user searches for the same term twice? The same tweets will be appended to the page and the same request will be made every time to Twitter. To counteract the ideal implementation would be to store some ‘max-tweet-id’ in an atom and ensure that gets communicated to the Twitter API accordingly, ClojureScript supports this quite easily.

I understand this post has been very long but I hope it will demonstrate some of the things that are possible with ClojureScript and Clojure.