Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

cueball

Generating css directly with Clojure is great; before cssgen arrived, for example, I had to have all style sets (e.g. desktop/mobile) ready at once and communicate with the client indirectly (i.e. through html attributes). Now, I can just let my server make the decisions, generating and composing html/css at will.

Why not do the same with client-side scripting? Generating client-side scripts, hiccup style:

[:button {:onclick (cljs (js/alert "hi!"))} "click me!"]

cueball leverages the ClojureScript compiler so that you can inline scripts from server code. While it's always best to keep large chunks of client-side logic properly seperated, this can give you a bit more flexibility in terms of communicating with the client (i.e., rather than using html attributes). Eventually, it could also be used in situations where libraries (e.g. for MongoDB) accept javascript for scripting.

The cljs syntax also supports unquoting, so you can splice in calls to server-side functions in order to generate and modify code on the fly:

(cljs (* (+ 5 3) 5))
;=> "((5 + 3) * 5);\n"

(cljs (* ~(+ 5 3) 5))
;=> "(8 * 5);\n"

(cljs (map inc (list -1 ~@(range 9) 9 10)))
;=> "cljs.core.map.call(
;        null,
;        cljs.core.inc,
;        cljs.core.list.call(null,-1,0,1,2,3,4,5,6,7,8,9,10));\n"

;; Pass a settings map to the client code
(defn settings-map [] {:mobile-site true})

(cljs (set! client/settings-map ~(settings-map)))
;=> "client.settings_map = 
;        cljs.core.ObjMap.fromObject(
;            [\"\\uFDD0'mobile-site\"],
;            {\"\\uFDD0'mobile-site\":true});\n"

If you want to :use other libraries, you can create a namespace. It's best to use the script macro to put this into the <head> of all of your pages.

(script (ns my-ns))
;=> <script type="text/javascript">
;       goog.provide('my-ns');
;       goog.require('cljs.core');
;   </script>

(script
  (def x 5)
  (defn f [n] (* n 2)))
;=> <script type="text/javascript">
;       my-ns.x = 5;
;       my-ns.f = (function f(n){
;           return (n * 2);
;       });
;   </script>

An interesting use for cueball might be to create libraries of self-contained widgets which don't require the user to touch clojurescript at all:

;; An input field which will display a default message until it is used.
(defn field-reset [id s]
  (cljs (#(let [f (js/$ ~(str "#" id))
                 v (.prop f "value")]
             (.prop f "value" ~s)))))

(defpartial field [id msg]
  (text-field {:value msg
               :onfocus (field-reset id  "")
               :onblur  (field-reset id msg)}
              id))

;; Usage
(html [:body (field "name" "enter your name")])

usage

For now, just copy cueball.clj over to your source folder. It's best to use it with something like noir-cljs, as you need to make sure that ClojureScript is bootstrapped on the client side (unless you avoid all core functions). lein run this project to launch a noir server with some examples on.

This is partly an experiment in making the join between Clojure and -Script more seamless; it would be great to have a framework with capabilities like opa's one day, and this is a (small) step in that direction.

(ns cueball
(:use [cljs.compiler :exclude [munge macroexpand-1]]
clojure.walk))
;; Implementation of `quote` with unquoting
(defn seqable? [e]
(if-not (string? e)
(try (seq e)
(catch Exception e false))))
(defn empty' [coll]
(if (= (type coll) clojure.lang.MapEntry)
[]
(empty coll)))
(defn unquote-form? [expr]
(and (seq? expr)
(= (first expr) `unquote)))
(defn unquote-splicing-form? [expr]
(and (seq? expr)
(= (first expr) `unquote-splicing)))
(defmacro q
"Like quote, but supports unquote (`~`, `~@`)."
[expr]
(cond
(unquote-form? expr) (second expr)
(seq? expr) (reduce #(if (unquote-splicing-form? %2)
`(concat ~(second %2) ~%1)
`(conj ~%1 (q ~%2)))
()
(reverse expr))
(seqable? expr) `(into ~(empty' expr) (q ~(seq expr)))
(symbol? expr) `'~expr
:else expr))
;; Clojure compilation.
(defn no-lazy [coll]
(prewalk #(if (= (type %) clojure.lang.LazySeq)
(apply list %)
%)
coll))
(def cljs-ns (atom *cljs-ns*))
(defn env []
{:ns (@namespaces @cljs-ns) :context :statement :locals {}})
(defn compile-form
"Compile clojure forms into javascript.
Namespace all non-core fns."
[code]
(with-core-cljs
(with-out-str
(binding [*cljs-ns* @cljs-ns]
(->> code no-lazy (analyze (env)) emit)
(reset! cljs-ns *cljs-ns*)))))
;; Functions to use.
(defmacro cljs [& forms]
`(compile-form (q (do ~@forms))))
(defmacro script [& forms]
`(str "<script type=\"text/javascript\">\n"
(cljs ~@forms)
"</script>"))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment