Tuesday, September 4, 2012

Intro to ClojureScript - Getting Started

I’ve been curious about ClojureScript for awhile now but for whatever reason I hadn’t taken the plunge. A few weeks back I attended a at talk at a TriClojure meetup titled ‘A ClojureScript Experience Report’. Jason broke down his experience creating an app using ClojureScript ins such a way that it motivated me to start learning ClojureScript.

This is the first in a series of posts that I will write that chronicles my journey down the ClojureScript path. I will be building out a basic app to lookup hockey stats by the last name of a player. Keep in mind I’m learning ClojureScript as I write these posts. If you have experience with ClojureScript and you see me making errors or not doing things in a idiomatic way please feel free to point out the errors of my ways.

Since ClojureScript is a bit of a mouthful I will use the abbreviation cljs for the remainder of this post.

The Goal

By the end of this post you will have an environment that is ready to create cljs apps. You will also have the beginnings of our hockey stats lookup app.

The Setup

Before you can start writing you app you’ll need to get your environment setup. The first step is to get Leiningen installed. Follow the directions on the page and you’ll be ready to create your first project.

Creating a cljs project is done the same way you create a Clojure project:

lein new cljs-intro

This will create Clojure project with the following layout:


README
project.clj
src
    cljs_intro
         core.clj
test
   cljs_intro
         test
               core.clj

In order to keep our Clojure and cljs code seperate we need to change the directory layout of our project a little bit. We need to add two directories, move the existing Clojure code and create the directory for housing our HTML and cljs compiler output. If you are in the project’s home directory run the following commands:

  1. mkdir src/clj src/cljs
  2. mv src/cljs_intro src/clj
  3. mkdir -p resources/public

Now that we’ve created the directories and moved the code we need to update the project.clj file to reflect these changes and add cljs specific settings.

The project.clj file generated by the lein command looks like this:

First we need to add the plugin lein-cljsbuild to the project. lein-cljsbuild is the plugin that makes it possible to compile cljs apps. Adding lein-cljsbuild to our is as simple as adding the :plugin line to the project file. Here’s what the updated project.clj file looks like after adding the line.

Since we moved the source code location we need to update the project.clj file to reflect those changes. By adding the following line the file we inform lein where the Clojure code lives.

:source-path "src/clj"

Now its time to set up the cljs related settings. Here’s what we need to add to the project file:

Here we are we are telling the cljs compiler to output the results of the compilation into the file resources/public/hockey.js. Since we have an optimization setting of :whitespace all compiled javascript will be placed into a single file. There are four levels of optimization levels available: :none, :whitespace, :simple and :advanced. We will stick with the :whitespace option. The :whitespace setting removes comments and any unnecessary whitespace in the source code but leaves the remaining code alone. The book ClojureScript: Up and Running has a nice discussion about optimization. The :pretty-print option gives us output that is a little easier to read. Here’s what the updated project.clj file looks like:

To ensure we have everything correct in the project.clj file go ahead and start the Clojure repl by running this command:

lein repl

If everything is OK you should see something like this:

REPL started; server listening on localhost port 42236
user=>

Time to Write Code

Now that we have our project setup for cljs its time to start writing some code. For this post we will create the search form which will have a text field and a button. The ‘back end’ of the form will be written using cljs and will be responsible for responding to the search button click. When the button is clicked the cljs code will grab the value of the text field and displaying it in an alert box.

resources/public/index.html

Before we write the cljs code lets create the HTML page. Its a simple form that has a text box and a button. Here’s what the HTML looks like:

The only real line of note here is the script tag. It references the file that we will generate when we compile our cljs code.

src/cljs/search.cljs

The src/cljs/search.cljs file is where we will put the code to handle the user’s input. Once you have created the file open it in your favorite editor and add the following lines:

The above code sets the namespace for the code and brings in the domina and clojure.browser.event libraries. We need the domina library so we can interact with the DOM. The clojure.browser.event library is used to add the event handler for the button’s click event.

Before we write the search button's click event handler lets get a reference to the search button itself.

(def search-button (d/by-id "search-btn"))

We get a reference to the button by making use of domina's by-id function and store the reference to the DOM object in search-button. Now its time to create the event handler.

The clojure.browser.event/listen function creates the relationship between the search button’s click event and our handler function. We create a function to popup an alert box containing the contents of the text field.

Compile Time

To compile the cljs code we have two options: lein cljsbuild once or lein cljsbuild auto. The once option compiles our code like a ‘normal’ compiler. It runs, compiles the source and puts the output to the file that we indicated in the :output-to setting. If we compile with auto every time a cljs file is saved the compiler will run. For now we’ll stick with the once option. To compile our code go to anywhere in the project’s directory structure and enter:

lein cljsbuild once

Now open up the the HTML file we created in the browser and you should see a beautifully designed form that looks something like this:

Enter the string ‘ClojureScript!’ in the text field and click the ‘Get Stats!’ button. You should see something like this:

We now have the beginnings of our stats lookup app. The user interaction is in place but we don’t really have a lot of the ‘under the covers’ code in place. In my next post we will add code to the hockey database and run an actual query!

Summary

In this post we went over how to get your environment set up for ClojureScript. We also went over the necessary changes to the project directory structure and project.clj file. We also created a simple cljs file to handle the user’s input in our search form.

Whats Next?

In Intro to ClojureScript - Part 2 - Getting the Stats we will create the necessary back end to retrieve data from the database and display it on the page.

In Intro to ClojureScript - Part 3 - Incorporating ClojureScriptOne - We will add support for messaging. We will add a new feature with messaging.

Resources

ClojureScriptOne.com - A sample project to help people get familiar with ClojureScript.

ClojureScript Experience Report - Resources - Bits of information about Jason’s ClojureScript based application and ClojureScript.

ClojureScript: Up and Running - An early release book that discusses ClojureScript. I’ve read the released chapters and found it to be a good resource. Chapters 2 and 3 go over ClojureScript’s compilation, project structure, and other informative tidbits for both ClojureScript and Clojure. I’ve recommended the book to a colleague who is learning Clojure because of the way the authors describe data structures, immutability and sequences.

cljs-intro - My github repo that stores the code for this blog series.

8 comments:

  1. Hi, when I get to "lein cljsbuild once"
    It breaks with this exception:

    felipe-gerards-MacBook-Air:clojurescript1 fgerard$ lein cljsbuild once
    Compiling ClojureScript.
    java.lang.IllegalArgumentException: No implementation of method: :as-file of protocol: #'clojure.java.io/Coercions found for class: clojure.lang.Symbol
    at clojure.core$_cache_protocol_fn.invoke(core_deftype.clj:527)
    at clojure.java.io$fn__8184$G__8179__8189.invoke(io.clj:35)
    at clojure.java.io$file.invoke(io.clj:413)
    at leiningen.core.classpath$normalize_path.invoke(classpath.clj:150)
    at leiningen.core.classpath$get_classpath$iter__400__404$fn__405.invoke(classpath.clj:164)
    at clojure.lang.LazySeq.sval(LazySeq.java:42)
    at clojure.lang.LazySeq.seq(LazySeq.java:60)
    at clojure.lang.ChunkedCons.chunkedNext(ChunkedCons.java:59)
    at clojure.lang.ChunkedCons.next(ChunkedCons.java:43)
    at clojure.lang.LazySeq.next(LazySeq.java:92)
    at clojure.lang.RT.next(RT.java:587)
    at clojure.core$next.invoke(core.clj:64)
    at clojure.string$join.invoke(string.clj:138)
    at leiningen.core.eval$classpath_arg.invoke(eval.clj:149)
    at leiningen.core.eval$shell_command.invoke(eval.clj:153)
    at leiningen.core.eval$fn__921.invoke(eval.clj:163)
    at clojure.lang.MultiFn.invoke(MultiFn.java:167)
    at leiningen.core.eval$eval_in_project.invoke(eval.clj:199)
    at clojure.lang.Var.invoke(Var.java:423)
    at clojure.lang.AFn.applyToHelper(AFn.java:167)
    at clojure.lang.Var.applyTo(Var.java:532)
    at clojure.core$apply.invoke(core.clj:601)
    at leiningen.cljsbuild.subproject$eval_in_project.invoke(subproject.clj:87)
    at leiningen.cljsbuild$run_local_project.invoke(cljsbuild.clj:26)
    at leiningen.cljsbuild$run_compiler.invoke(cljsbuild.clj:48)
    at leiningen.cljsbuild$once.invoke(cljsbuild.clj:103)
    at leiningen.cljsbuild$cljsbuild.doInvoke(cljsbuild.clj:193)
    at clojure.lang.RestFn.invoke(RestFn.java:425)
    at clojure.lang.Var.invoke(Var.java:419)
    at clojure.lang.AFn.applyToHelper(AFn.java:163)
    at clojure.lang.Var.applyTo(Var.java:532)
    at clojure.core$apply.invoke(core.clj:603)
    at leiningen.core.main$resolve_task$fn__823.doInvoke(main.clj:73)
    at clojure.lang.RestFn.applyTo(RestFn.java:139)
    at clojure.lang.AFunction$1.doInvoke(AFunction.java:29)
    at clojure.lang.RestFn.applyTo(RestFn.java:137)
    at clojure.core$apply.invoke(core.clj:603)
    at leiningen.core.main$apply_task.invoke(main.clj:95)
    at leiningen.core.main$_main$fn__862.invoke(main.clj:156)
    at leiningen.core.main$_main.doInvoke(main.clj:155)
    at clojure.lang.RestFn.invoke(RestFn.java:421)
    at clojure.lang.Var.invoke(Var.java:419)
    at clojure.lang.AFn.applyToHelper(AFn.java:163)
    at clojure.lang.Var.applyTo(Var.java:532)
    at clojure.core$apply.invoke(core.clj:601)
    at clojure.main$main_opt.invoke(main.clj:324)
    at clojure.main$main.doInvoke(main.clj:427)
    at clojure.lang.RestFn.invoke(RestFn.java:457)
    at clojure.lang.Var.invoke(Var.java:427)
    at clojure.lang.AFn.applyToHelper(AFn.java:172)
    at clojure.lang.Var.applyTo(Var.java:532)
    at clojure.main.main(main.java:37)
    felipe-gerards-MacBook-Air:clojurescript1 fgerard$

    ReplyDelete
  2. Felipe,

    What version of lein are you using? Can you create a gist with your project.clj file and post the URL here?

    Thanks,

    Rob

    ReplyDelete
  3. @Rob: Maybe I'm too late, in my case I was getting that error because of those non-standard quotes in project.clj, for example have a look at :source-path “src/cljs” double quotes here are different from these :source-path "src/cljs".

    Hope this helps

    ReplyDelete
  4. Hi Rob -

    I followed all the steps you mentioned and have the exact project.clj file that you have. When I load the index.html file in my browser I get a javascript error:

    goog.require could not find: domina

    How should I include the javascript dependencies? I think it is missing domina.js.

    ReplyDelete
  5. Please disregard my earlier comment. I had forgotten to recompile my javascript after adding the domina dependency in the project.clj file.

    ReplyDelete
  6. no problems shuaybi - I've done that about 100 times myself.

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete