Skip to content

fuzzyalej/cljs-workshop-doc

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 

Repository files navigation

ClojureScript Tutorial

Table of Contents

This tutorial consists on to provide an introduction to clojurescript from very basic setup, to more complex application in little incremental steps.

It includes:

  • Setup initial clojure app layout.

  • Setup initial clojurescript app layout.

  • First contact to clojurescript language.

  • Working with dom events.

  • Working with routing in the browser.

  • Working with ajax requests.

  • First contact with core.async.

  • Working with events and ajax using core.async.

  • First contact with om/reactjs.

  • Working with om and timetraveling.

  • Working with om and peristent state.

  • Little bonus: browser enabled repl.

Transversal:

  • Little touch on Google Closure Library along all tutorial.

  • ClojureScript features (not explained on the first contact with language).

  • Some differences with Clojure.

  • Clojure is designed as guest language (unlike funscript or similar, it not intends to translate host code to js, you can not import java.util.Date on clojurescript…​)

  • Language with own semantics (not like coffeescript, typescript, …​)

  • Good host interoperability.

  • Batteries included (clojure runtime & google closure library)

  • Expressivenes

  • Functional.

  • Lisp.

  • Macros.

  • Google Closure Compiler (advanced code compiling with dead code elimination)

  • core.async (coroutines and csp as a library)

  • …​ much more.

git clone https://github.com/niwibe/cljs-workshop
git checkout step0
  • Initial leiningen project template.

  • Add ring, compojure and other related dependencies.

  • Create routes and initial application entry point.

  • First run of hello world app.

resources/
resources/public/
resources/index.html
src/
src/clj/
src/clj/cljsworkshop/
src/clj/cljsworkshop/core.clj
project.clj
(defproject cljsworkshop "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "BSD (2-Clause)"
            :url "http://opensource.org/licenses/BSD-2-Clause"}
  :dependencies [[org.clojure/clojure "1.6.0"]

                 ;; Backend dependencies
                 [compojure "1.2.1"]
                 [ring/ring-core "1.3.1" :exclusions [javax.servlet/servlet-api]]
                 [ring/ring-servlet "1.3.1" :exclusions [javax.servlet/servlet-api]]
                 [ring/ring-defaults "0.1.2"]

                 [javax.servlet/javax.servlet-api "3.1.0"]
                 [info.sunng/ring-jetty9-adapter "0.7.2"]]

  :source-paths ["src/clj"]
  :main cljsworkshop.core)
  • Ring handler consists in a simple function that receives a req (hash-map) and return a response (also hash-map).

  • Compojure add routing handlers and some response helpers.

  • jetty9 is a embedded http/application server.

clj/cljsworkshop/core.clj
(ns cljsworkshop.core
  (:require [ring.adapter.jetty9 :refer [run-jetty]]
            [compojure.core :refer :all]
            [compojure.route :as route]
            [compojure.response :refer [render]]
            [clojure.java.io :as io]))

;; This is a handler that returns the
;; contents of `resources/index.html`
(defn home
  [req]
  (render (io/resource "index.html") req))

;; Defines a handler that acts as router
(defroutes app
  (GET "/" [] home)
  (route/resources "/static")
  (route/not-found "<h1>Page not found</h1>"))

;; The main entry point of application.
(defn -main
  [& args]
  (run-jetty app {:port 5050}))
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>CLJS Workshop</title>
  </head>
  <body>
    <h1>Hello World</h1>
  </body>
</html>

As we declared the main entry point on our project.clj, now we only should execute a run command of leiningen:

$ lein run
2014-12-08 14:03:49.623:INFO::main: Logging initialized @877ms
2014-12-08 14:03:52.992:INFO:oejs.Server:main: jetty-9.2.3.v20140905
2014-12-08 14:03:53.016:INFO:oejs.ServerConnector:main: Started ServerConnector@3149409c{HTTP/1.1}{0.0.0.0:5050}
2014-12-08 14:03:53.017:INFO:oejs.Server:main: Started @4283ms

Is a little introduction to ClojureScript language. Is a proper step before starting working with it.

Table 1. Types

Type name

Representation

String

"Hello World"

Long

2

Float

2.4

Keyword

:foo

Map

{:foo "bar"}

Vector

[1 2 "3"]

List

(1 2 "3")

Set

#{1 2 3}

Regex

#"^\w+$"

(.log js/console "hello world")
(enable-console-print!)
(println "hello world")
Declare module
(ns my.library)
Require a module
(ns my.library
  (:require [my.other :as other]))
Top level
(def myvar "foo")
Local
(let [myvar "foo"]
  (println myvar))
Simple function definition
(defn foo
  [a b c]
  c)

(foo 1) ;; WARNING: function called with incorrect
        ;; number of arguments

(foo 1 2 3) ;; => 3
Dispatch on arity
(defn foo
  ([a] "one")
  ([a b] "two")
  ([a b c] "three"))

(foo 1) ;; => "one"
(foo 1 2) ;; => "two"
(foo 1 2 3) ;; => "three"

;; Under advanced compilation direct dispatch to
;; arity. No arguments object manipulation
Variable number of arguments
(defn foo
  [a b & rest]
  rest)

(foo 1 2 3) ;; => [3]
(foo 1 2 3 4 5) ;; => [3 4 5]
Named parameters & default values
(defn foo
  [& {:keys [bar baz]
      :or {bar "default1"
           baz "default2"}}]
  (str bar "-" baz))

(foo) ;; => "default1-default2"
(foo :bar 1) ;; => "1-default2"
(foo :bar 1 :baz 2) ;; => "1-2"

Is always based on value. CLJS does not have coercive equality.

// == operator is coercive
1 == "1" // => true

// sometimes based on value
{} == {} // => false

["a"] === ["a"] // => false
(= 1 "1") ;; => false
(= {} {}) ;; => true
(= ["a"] ["a"]) ;; => true

In cljs locals are immutable:

This code throws an error:
(let [x 2]
  (set! x 3))
(def ^:dynamic x 5)

(defn print-value
  []
  (println "Current value:" x))

(print-value)
(binding [x 10]
  (print-value))
(print-value)

;; Will result in:
;; Current value: 5
;; Current value: 10
;; Current value: 5
Positional destructuring.
(def color [255 255 100 0.5])

(let [[r g _ a] color]
  (println r)
  (println a))

;; Will result in:
;; 255
;; 0.5
Hash map keys destructuring
(def m {:first "Bob"
        :middle "J"
        :last "Smith"})

(let [{:keys [first last]} m]
  (println first)
  (println last))

;; Will result in:
;; Bob
;; Smith
;; For example say you'd like to use RegExps
;; as functions

(extend-type js/RegExp
  IFn
  (-invoke
   ([this s]
     (re-matches this s))))

(filter #"foo.*" ["foo" "bar" "foobar"])
;; => ("foo" "foobar")

More resources:

Polymorphism a la carte.

Define a multimethod
(defmulti say-hello
  (fn [person]
    (:lang person :en)))

(defmethod say-hello :en
  [person]
  (format "Hello %s" (:name person)))

(defmethod say-hello :es
  [person]
  (format "Hola %s" (:name person)))
Playing with multimethod
(def person-alex {:lang :es :name "Alex"})
(def person-yen {:lang :en :name "Yen"})
(def person-anon {:name "Anonymous"})

(say-hello person-alex)
;; => "Hola Alex"

(say-hello person-yen)
;; => "Hello Yen"

(say-hello person-anon)
;; => "Hello Anonimous"
Clojure
(def foo (js-obj "bar" "baz"))
Javascript
var foo = {bar: "baz"};
Clojure
(set! (.-bar foo) "baz")
(.log js/console (.-bar foo))

;; aset means array set
(aset foo "abc" 17)
(.log js/console (aget foo "abc"))
Javascript
foo.bar = "baz";
console.log(foo.bar);

foo["abc"] = 17;
console.log(foo["abc"]);
Convert cljs types to js using clj->js function
(let [a {:a 1 :b {:c 1}}]
  (clj->js a))
Convert js types to cljs using js->clj function
(defn get-names [people]
  (let [people (js->clj people)
        names (map "name" people)]
    (clj->js names)))
Using reader macro for conver cljs to js:
(let [a #js [1 2 3]]
  (println (aget a 1)))

;; Will result in:
;; 2
Note
the #js reader macro is not recursive.
git reset --hard
git checkout step1
project.clj
:dependencies [;; ...
               [org.clojure/clojurescript "0.0-2411"]
               ;; ...]
project.clj
:plugins [[lein-cljsbuild "1.0.3"]]
:cljsbuild {:builds
            [{:id "app"
              :source-paths ["src/cljs"]
              :compiler {:output-to "resources/public/js/app.js"
                         :optimizations :whitespace
                         :pretty-print true}}]}

New tree structure on src/ directory for clojurescript sources.

src/cljs/
src/cljs/cljsworkshop/
src/cljs/cljsworkshop/core.cljs
core.cljs
(defn set-html! [el content]
  (set! (.-innerHTML el) content))

(defn main
  []
  (let [content "Hello World from Clojure Script"
        element (aget (js/document.getElementsByTagName "main") 0)]
    (set-html! element content)))
<body>
  <main></main>
  <script src="/static/js/app.js"></script>
</body>
[3/5.0.7]niwi@niwi:~/cljs-workshop> lein cljsbuild auto
Compiling ClojureScript.
Compiling "resources/public/js/app.js" from ["src/cljs"]...
Successfully compiled "resources/public/js/app.js" in 3.396 seconds.
git reset --hard
git checkout step2
<main>
  <section>
    <span>Clicks: </span>
    <span id="clicksnumber"><span>
  </section>
  <button id="button">Click me</button>
</main>
(ns cljsworkshop.core
  (:require [goog.events :as events]
            [goog.dom :as dom]))

(defn main
  []
  (let [counter (atom 0)
        button  (dom/getElement "button")
        display (dom/getElement "clicksnumber")]

    ;; Set initial value
    (set! (.-innerHTML display) @counter)

    ;; Assign event listener
    (events/listen button "click"
                   (fn [event]
                     ;; Increment the value
                     (swap! counter inc)
                     ;; Set new value in display element
                     (set! (.-innerHTML display) @counter)))))
  • ClojureScript uses Google Closure Library for modules/namespace: each ClojureScript file reprensents a google closure module

  • The :require statement on ns can loads any google closure module or your defined module that the compiler can find in the path (see project.clj for path…​)

  • Google Closure Library comes with ClojureScript. You don’t need add it as dependency.

  • Works in advanced mode of google closure compiler (that eliminates unused code).

git reset --hard
git checkout step3

secretary is a routing library for clojure script.

project.clj
:dependencies [;; ...
               [secretary "1.2.1"]]
(ns cljsworkshop.core
  (:require-macros [secretary.core :refer [defroute]])
  (:require [goog.events :as events]
            [goog.dom :as dom]
            [secretary.core :as secretary])
  (:import goog.History))

(def app (dom/getElement "app"))

(defn set-html! [el content]
  (set! (.-innerHTML el) content))

(defroute home-path "/" []
  (set-html! app "<h1>Hello World from home page.</h1>"))

(defroute some-path "/:param" [param]
  (let [message (str "<h1>Parameter in url: <small>" param "</small>!</h1>")]
    (set-html! app message)))

(defroute "*" []
  (set-html! app "<h1>Not Found</h1>"))

(defn main
  []
  ;; Set secretary config for use the hashbang prefix
  (secretary/set-config! :prefix "#")

  ;; Attach event listener to history instance.
  (let [history (History.)]
    (events/listen history "navigate"
                   (fn [event]
                     (secretary/dispatch! (.-token event))))
    (.setEnabled history true)))

(main)
  • ClojureScript macros should be written in Clojure (not ClojureScript) but should emit ClojureScript code.

  • Should be imported separatedly, using (:require-macros ...) statement on ns.

  • Google closure classes should be imported with (:import ...) statement.

git reset --hard
git checkout step4
Partial content from core.cljs
(ns cljsworkshop.core
  (:require-macros [secretary.core :refer [defroute]])
  (:require [goog.events :as events]
            [goog.dom :as dom]
            [secretary.core :as secretary])
  (:import goog.History
           goog.Uri
           goog.net.Jsonp))


(def search-url "http://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=")
(def home-html
  (str "<h1>Wikipedia Search:</h1>"
       "<section>"
       "  <input id=\"query\" placeholder=\"Type your search...\" />"
       "  <button id=\"searchbutton\">Search</button>"
       "  <ul id=\"results\"></ul>"
       "</section>"))

(defn render-results [results]
  (let [results (js->clj results)]
    (reduce (fn [acc result]
              (str acc "<li>" result "</li>"))
            ""
            (second results))))

(defn do-jsonp
  [uri callback]
  (let [req (Jsonp. (Uri. uri))]
    (.send req nil callback)))

(defroute home-path "/" []
  (set-html! app home-html)
  (let [on-response     (fn [results]
                          (let [html (render-results results)]
                            (set-html! (dom/getElement "results") html)))

        on-search-click (fn [e]
                          (let [userquery (.-value (dom/getElement "query"))
                                searchuri (str search-url userquery)]
                            (do-jsonp searchuri on-response)))]

    (events/listen (dom/getElement "searchbutton") "click" on-search-click)))

Is a CSP library with steroids.

Before start with core.async, we will try to solve one simple problem:

  1. Request 1 url page.

  2. Wait 1second

  3. Request 2 url page.

  4. Return result both results.

Let start introducing a problem using ES5 (EcmaScript 5) or shortly javascript of today. For it, firstly define the following utils functions:

function timeout(ms) {
  return new Promise(function(resolve) {
    setTimeout(resolve, ms);
  });
}

function httpGet(url) {
  return new Promise(function(resolve) {
    var req = new XMLHttpRequest();
    req.open("GET", url, false);
    req.send(null);
    req.onreadystatechange = function() {
      if (req.readyState == 4) {
        resolve(xhr.responseText);
      }
    }
  });
}

And implement the solution:

function doStuff() {
   return httpGet("http://page1/").then(function(response) {
    return timeout(1000).then(function() {
      return response;
    });
   })
   .then(function(response1) {
     return httpGet("http://page2/").then(function(response2) {
       return {response1: response1,
               response2: response2};
     });
   })
}
Now you can use it so:
doStuff().then(function(result) {
  console.log(result.response1);
  console.log(result.response2);
});

Obviously, it can be done better, but nobody will save us from callbacs.

But, what is cooking for ES7? (ES7? but ES6 still not ready? WTF)

Same example but using the draft proposal for ES7
async function doStuff() {
  var response1, response2;

  response1 = await httpGet("http://page1/");
  await timeout(1000):
  response2 = await httpGet("http://page2/");
  return {response1: response1,
          response2: response2};
}
Now you can use it so:
(async function() {
  var result = await doStuff()
  console.log(result.response1);
  console.log(result.response2);
})();

Now looks much better.

Notes:

  • This can be "emulated" with generators, but them are not designed for this purpose.

Now having the background of ES7 example, let see same thing but using core.async library with clojure script.

Define the missing util function.
(defn http-get [uri]
  (let [out (chan)
        req (XhrIo. (Uri. uri))]
    (events/listen req "success" #(put! out (.getResponseText (.-target %))))
    (.send req (Uri. uri))
    out))
Define the doStuff like function with main logic.
(defn do-stuff
  []
  (go
    (let [response1 (<! (http-get "http://page1/"))
          _         (<! (timeout 1000))
          response2 (<! (http-get "http://page2/"))]
      {:response1 response1
       :response2 response2})))
Now see an example of how use it.
(go
  (let [result (<! (do-stuff))]
    (.log js/console (.-response1 result))
    (.log js/console (.-response2 result))))

You can see that the code that is asyncrono by nature, it seems to be like synchronous.

(go
  [... do something asynchronously ...])
  • always return a channel.

  • put in a returned channel the restul of last expression.

  • executes asynchronously.

(chan)
  • creates a new channel

  • does not support nil values

  • nil return value means channel is closed

  • support different buffering strategies: fixed size, unbound (default), sliding, dropping.

(go
  (<! (timeout 100))
  (.log js/console "finished"))
  • <! represents a callback-less take!

  • >! represents a callback-less put!

  • in clojure them have blocking version of them: <!! and >!! and they does not requires of go macro, because they blocks the current thread.

git reset --hard
git checkout step5
  • Callbacks sucks.

  • Unclear execution flow.

  • We can do it better!

  • with core.async, async code looks sync ;)

project.clj
:dependencies [;; ...
               [org.clojure/core.async "0.1.346.0-17112a-alpha"]]
Partial content from core.cljs
(ns cljsworkshop.core
  (:require-macros [secretary.core :refer [defroute]]
                   [cljs.core.async.macros :refer [go]])
  (:require [goog.events :as events]
            [goog.dom :as dom]
            [secretary.core :as secretary]
            [cljs.core.async :refer [<! put! chan]])
  (:import goog.History
           goog.Uri
           goog.net.Jsonp))

(defn render-results [results]
  (let [results (js->clj results)]
    (reduce (fn [acc result]
              (str acc "<li>" result "</li>"))
            ""
            (second results))))

(defn listen [el type]
  (let [out (chan)]
    (events/listen el type (fn [e] (put! out e)))
    out))

(defn jsonp [uri]
  (let [out (chan)
        req (Jsonp. (Uri. uri))]
    (.send req nil (fn [res] (put! out res)))
    out))

(defroute home-path "/" []
  ;; Render initial html
  (set-html! app home-html)

  (let [clicks (listen (dom/getElement "searchbutton") "click")]
    (go (while true
          (<! clicks)
          (let [uri     (str search-url (.-value (dom/getElement "query")))
                results (<! (jsonp uri))]
            (set-html! (dom/getElement "results")
                       (render-results results)))))))

Now the code looks sync:

  1. Waits a click.

  2. Make a request to wikipedia.

  3. Renders result.

A synchronous code makes it easier to reason about itself.

git reset --hard
git checkout step6
  • Reactjs (functional approach for rendering dom)

  • Global state management facilities built in.

  • Customizable semantics. Fine grained control over how components store state.

  • Out of the box snapshotable and undoable and these operations have no implementation complexity and little overhead.

Befor see a complex app, we’ll try understand the basic of om components.

(ns mysamplens
  (:require [om.core :as om :include-macros true]
            [sablono.core :as html :refer-macros [html]]))

(defn mycomponent
  [app owner]
  (reify
    ;; Mainly serves for debugging. Specifies the
    ;; display name of react component on react
    ;; debugging tools for Chrome.
    om/IDisplayName
    (display-name [_]
      "my-component")

    ;; Set the initial component state.
    om/IInitState
    (init-state [_]
      {:message "Hello world from local state"})

    ;; Render the component with current local state.
    om/IRenderState
    (render-state [_ {:keys [message]}]
      (html [:section
             [:div message]
             [:div (:message app)]]))))

reify, what is this?

reify creates an anonymos object that implement one or more protocols.

om components consists in any object that implements the om/IRender or om/IRenderState protocols. Implementations for other protocols is optional.

In previous examples we have used a few number of protocols. Om comes with few other but them comes out of this first example scope.

*Now, having defined a compoment, it a time to mount it.

(defonce state {:message "Hello world from global state."})

;; "app" is a id of dom element at index.html
(let [el (gdom/getElement "app")]
  (om/root mycomponent state {:target el}))
git reset --hard
git checkout step7
  • The state of aplication is serializable, that makes easy and in deterministic way to reproduce a concrete state of the application.

  • The union of ClojureScript and Reactjs makes some task, that is usually considered very complex, very easy and painless, such as the time traveling or the undo in a few lines of code.

Desgining the application with global state management facilities of om, we can easy make a snapshot of the current state.

In Clojure(Script) an atom can be listened for changes:

;; Global applicatioon state
(def tasklist-state (atom {:entries []}))

;; Undo application state. An atom that will store
;; the snapshots of tasklist-state initialized with
;; initial @tasklist-state.
(def undo-state (atom {:entries [@tasklist-state]})

;; Watch a tasklist-state changes and snapshot them
;; into undo-state.
(add-watch tasklist-state :history
  (fn [_ _ _ n]
    (let [entries (:entries @undo-state)]
      (when-not (= (last entries) n)
        (swap! undo-state #(update-in % [:entries] conj n))))))

Now, each change in our application, is saved as snapshot in an other atom, and with simple button we can revert the last change and restore the previous one.

For it, we are created an other om component…​
(defn do-undo
  [app]
  (when (> (count (:entries @app)) 1)
    ;; remove the last spapshot from the undo list.
    (om/transact! app :entries pop)

    ;; Restore the last snapshot into tasklist
    ;; application state
    (reset! tasklist-state (last (:entries @undo-state)))))

(defn undo
  [app owner]
  (reify
    om/IRender
    (render [_]
      (html [:input {:type "button" :default-value "Undo"
                     :on-click (fn [_] (do-undo app))}]))))
git reset --hard
git checkout step8

Now having experimented with timetraveling, let’s try an little experimet making the state persistent. For it we will use the previous example and html5 localstorage.

project.clj
:dependencies [;; ...
               [hodgepodge "0.1.0"]]
Add the corresponding :require entry for hodgepodge
(ns cljsworkshop.core
  (:require [...]
            [hodgepodge.core :refer [local-storage]]))
Add additional watcher to tasklist-state atom responsible of the persistence
;; Watch tasklist-state changes and
;; persists them in local storege.
(add-watch tasklist-state :persistece
  (fn [_ _ _ n]
    (println "Event:" n)
    (assoc! local-storage :taskliststate n)))
Add code for restore stored state on app initialization.
;; Watch tasklist-state changes and
;; Get the persisted state, and if it exists
;; restore it on tasklist and undo states.
(when-let [state (:taskliststate local-storage)]
  (reset! tasklist-state state)
  (reset! undo-state {:entries [state]}))
git reset --hard
git checkout step9
project.clj
:dependencies [;; ...
               [com.cemerick/piggieback "0.1.3"]
               [weasel "0.4.2"]]
:repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
Add the corresponding :require entry for hodgepodge
(ns cljsworkshop.core
  (:require [...]
            [weasel.repl :as ws-repl]))

(ws-repl/connect "ws://localhost:9001")
Start the standard lein repl.
$ lein repl
Start the browser enabled repl
user> (require 'weasel.repl.websocket)
nil
user> (cemerick.piggieback/cljs-repl
        :repl-env (weasel.repl.websocket/repl-env
                   :ip "0.0.0.0" :port 9001))
Try evaluate the current app state
cljs.user=> (in-ns 'cljsworkshop.core)
cljsworkshop.core
cljsworkshop.core=> @tasklist-state
{:entries [{:completed false, :created-at "2014-12-08T11:32:10.677Z", :subject "task 1"}]}
nil

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Makefile 100.0%