├── .gitignore
├── README.md
├── example
├── example.html
├── project.clj
└── src
│ └── example
│ └── core.cljs
├── project.clj
├── src
└── flupot
│ ├── core.clj
│ ├── core.cljs
│ ├── core
│ └── parsing.clj
│ ├── dom.clj
│ └── dom.cljs
└── test
└── flupot
├── dom_test.clj
└── dom_test.cljs
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | classes
3 | checkouts
4 | pom.xml
5 | pom.xml.asc
6 | *.jar
7 | *.class
8 | .lein-*
9 | .nrepl-port
10 | .hgignore
11 | .hg/
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flupot
2 |
3 | A ClojureScript library for creating [React][] elements, in a similar
4 | style to [Om][]'s `om.dom` namespace.
5 |
6 | [react]: https://facebook.github.io/react/
7 | [om]: https://github.com/omcljs/om
8 |
9 | ## Installation
10 |
11 | Add the following to your project `:dependencies`:
12 |
13 | [flupot "0.4.0"]
14 |
15 | ## Basic Usage
16 |
17 | Require the `flupot.dom` namespace:
18 |
19 | ```clojure
20 | (ns flupot.example
21 | (:require [flupot.dom :as dom]))
22 | ```
23 |
24 | There is a function for each DOM element:
25 |
26 | ```clojure
27 | (dom/div (dom/p "Hello World"))
28 | ```
29 |
30 | If the first argument is a map, it's used as the element's attributes:
31 |
32 | ```clojure
33 | (dom/div {:class "foo"} (dom/p "Hello World"))
34 | ```
35 |
36 | Special React options like `:key` are also supported.
37 |
38 | The `class` attribute may be specified as a collection:
39 |
40 | ```clojure
41 | (dom/p {:class ["foo" "bar"]} "Hello World")
42 | ```
43 |
44 | And the `style` attribute may be specified as a map:
45 |
46 | ```clojure
47 | (dom/p {:style {:color :red}} "Hello World")
48 | ```
49 |
50 | If one of the child arguments is a seq, it's expanded out automatically:
51 |
52 | ```clojure
53 | (dom/ul
54 | (for [i (range 5)]
55 | (dom/li {:key i} i)))
56 | ```
57 |
58 | ## Advanced Usage
59 |
60 | Flupot can also be used to define your own wrappers around React
61 | elements or similar libraries (such as [react-pixi][]). You probably
62 | won't need to do this! But just in case...
63 |
64 | There are two macros that allow you to do this: `defelement-fn` and
65 | `defelement-macro`.
66 |
67 | `defelement-fn` generates a function around an element method, with an
68 | optional attribute transformation function:
69 |
70 | ```clojure
71 | (require '[flupot.core :refer [defelement-fn]])
72 |
73 | (defelement-fn span
74 | :elemf React.DOM.span
75 | :attrf cljs.core/clj->js)
76 | ```
77 |
78 | This generates a function `span` that wraps `React.DOM.span`. The
79 | attribute map is transformed with the `cljs.core/clj->js` function.
80 |
81 | Complementing this is `defelement-macro`. This generates a macro that
82 | will try to pre-compile as much as possible. If you give the macro the
83 | same name as the function defined by `defelement-fn`, ClojureScript
84 | will choose the macro when possible, and fall back to the function
85 | otherwise.
86 |
87 | ```clojure
88 | (require '[flupot.core :refer [defelement-macro]])
89 |
90 | (defelement-macro span
91 | :elemf React.DOM.span
92 | :attrf cljs.core/clj->js
93 | :attrm flupot.core/clj->js)
94 | ```
95 |
96 | This macro has third keyword argument, `:attrm`, which defines a
97 | function that is applied inside the macro. The `flupot.core/clj->js`
98 | function mimics `cljs.core/clj->js`, except that it attempts to
99 | perform as much as the conversion as possible during compile time.
100 |
101 | [react-pixi]: https://github.com/Izzimach/react-pixi/
102 |
103 | ## License
104 |
105 | Copyright © 2016 James Reeves
106 |
107 | Distributed under the Eclipse Public License either version 1.0 or (at
108 | your option) any later version.
109 |
--------------------------------------------------------------------------------
/example/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/project.clj:
--------------------------------------------------------------------------------
1 | (defproject flupot/example "0.1.0-SNAPSHOT"
2 | :description "FIXME: write description"
3 | :url "http://example.com/FIXME"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :dependencies [[org.clojure/clojure "1.7.0"]
7 | [org.clojure/clojurescript "1.7.228"]
8 | [flupot "0.4.0"]
9 | [brutha "0.2.0"]]
10 | :plugins [[lein-cljsbuild "1.1.3"]]
11 | :cljsbuild
12 | {:builds {:main {:source-paths ["src"]
13 | :compiler {:output-to "target/main.js"
14 | :optimizations :whitespace
15 | :main example.core}}}})
16 |
--------------------------------------------------------------------------------
/example/src/example/core.cljs:
--------------------------------------------------------------------------------
1 | (ns example.core
2 | (:require [brutha.core :as br]
3 | [flupot.dom :as dom]))
4 |
5 | (enable-console-print!)
6 |
7 | (defn content []
8 | (let [p dom/p
9 | a {:style {:color :blue}}
10 | c ["foo" :bar 'baz]]
11 | (dom/div
12 | {:class "test"}
13 | (dom/p "Hello " (dom/strong {:onclick #(js/alert "World")} "World"))
14 | (p "Testing functions")
15 | (p (list "Testing " "functions " "with " "lists"))
16 | (dom/ul (for [i (range 1 6)] (dom/li {:key i} i)))
17 | (dom/p {:style {:color :red}} "Testing style")
18 | (dom/p a "Testing programmatic style")
19 | (dom/p "Testing " (list "lists " (list "within " "lists")))
20 | (dom/p {:class ["foo" :bar 'baz]} "Multiple classes")
21 | (dom/p {:class c} "Multiple classes in symbol"))))
22 |
23 | (let [app (.getElementById js/document "app")]
24 | (br/mount (content) app))
25 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject flupot "0.4.0"
2 | :description "ClojureScript functions for creating React elements"
3 | :url "https://github.com/weavejester/flupot"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :dependencies [[org.clojure/clojure "1.7.0"]
7 | [org.clojure/clojurescript "1.7.228" :scope "provided"]
8 | [cljsjs/react-dom "15.1.0-0"]]
9 | :plugins [[lein-cljsbuild "1.1.3"]]
10 | :cljsbuild
11 | {:builds
12 | {:main
13 | {:source-paths ["src"]
14 | :compiler {:output-to "target/main.js"}}}})
15 |
--------------------------------------------------------------------------------
/src/flupot/core.clj:
--------------------------------------------------------------------------------
1 | (ns flupot.core
2 | (:require [flupot.core.parsing :as p]))
3 |
4 | (defn clj->js [x]
5 | (cond
6 | (map? x)
7 | `(cljs.core/js-obj ~@(mapcat (partial map clj->js) x))
8 | (or (vector? x) (set? x))
9 | `(cljs.core/array ~@(map clj->js x))
10 | (or (string? x) (number? x))
11 | x
12 | (keyword? x)
13 | (name x)
14 | :else
15 | `(cljs.core/clj->js ~x)))
16 |
17 | (defmacro defelement-fn
18 | [name & {:keys [elemf attrf]
19 | :or {attrf 'cljs.core/clj->js}}]
20 | `(defn ~name [opts# & children#]
21 | (let [args# (cljs.core/array)]
22 | (if (map? opts#)
23 | (.push args# (~attrf opts#))
24 | (do (.push args# nil)
25 | (.push args# opts#)))
26 | (doseq [child# children#]
27 | (flupot.core/push-child! args# child#))
28 | (.apply ~elemf nil args#))))
29 |
30 | (defn- flat-dom-form [elemf attrf attrm opts children]
31 | (cond
32 | (map? opts)
33 | `(~elemf ~(attrm opts) ~@children)
34 | (p/literal? opts)
35 | `(~elemf nil ~opts ~@children)
36 | :else
37 | `(let [opts# ~opts]
38 | (if (map? opts#)
39 | (~elemf (~attrf opts#) ~@children)
40 | (~elemf nil opts# ~@children)))))
41 |
42 | (defn- nested-dom-form [elemf attrf attrm opts children]
43 | (let [child-syms (map (fn [c] [(if-not (p/literal? c) (gensym)) c]) children)
44 | arguments (map (fn [[s c]] (or s c)) child-syms)
45 | bindings (filter first child-syms)
46 | args-sym (gensym "args__")]
47 | `(let [~@(mapcat identity bindings)]
48 | (if (or ~@(map (fn [[sym _]] `(seq? ~sym)) bindings))
49 | (let [~args-sym (cljs.core/array)]
50 | ~(cond
51 | (map? opts)
52 | `(.push ~args-sym ~(attrm opts))
53 | (p/literal? opts)
54 | `(do (.push ~args-sym nil)
55 | (.push ~args-sym ~opts))
56 | :else
57 | `(let [opts# ~opts]
58 | (if (map? opts#)
59 | (.push ~args-sym (~attrf opts#))
60 | (do (.push ~args-sym nil)
61 | (.push ~args-sym opts#)))))
62 | ~@(for [[s c] child-syms]
63 | (if s `(flupot.core/push-child! ~args-sym ~s) `(.push ~args-sym ~c)))
64 | (.apply ~elemf nil ~args-sym))
65 | ~(flat-dom-form elemf attrf attrm opts arguments)))))
66 |
67 | (defn compile-dom-form [elemf attrf attrm opts children]
68 | (if (every? p/literal? children)
69 | (flat-dom-form elemf attrf attrm opts children)
70 | (nested-dom-form elemf attrf attrm opts children)))
71 |
72 | (defmacro defelement-macro
73 | [name & {:keys [elemf attrf attrm]
74 | :or {attrf 'cljs.core/clj->js
75 | attrm 'flupot.core/clj->js}}]
76 | `(defmacro ~name [opts# & children#]
77 | (compile-dom-form '~elemf '~attrf ~attrm opts# children#)))
78 |
--------------------------------------------------------------------------------
/src/flupot/core.cljs:
--------------------------------------------------------------------------------
1 | (ns flupot.core)
2 |
3 | (defn push-child! [args child]
4 | (if (seq? child)
5 | (doseq [c child]
6 | (push-child! args c))
7 | (.push args child)))
8 |
--------------------------------------------------------------------------------
/src/flupot/core/parsing.clj:
--------------------------------------------------------------------------------
1 | (ns flupot.core.parsing)
2 |
3 | (defn quoted? [x]
4 | (and (list? x) (= 'quote (first x))))
5 |
6 | (defn literal? [x]
7 | (or (quoted? x) (not (or (symbol? x) (list? x)))))
8 |
--------------------------------------------------------------------------------
/src/flupot/dom.clj:
--------------------------------------------------------------------------------
1 | (ns flupot.dom
2 | (:refer-clojure :exclude [map meta time])
3 | (:require [clojure.core :as core]
4 | [clojure.string :as str]
5 | [flupot.core :as flupot]
6 | [flupot.core.parsing :as p]))
7 |
8 | (def tags
9 | '[a abbr address area article aside audio b base bdi bdo big blockquote body br
10 | button canvas caption cite code col colgroup data datalist dd del details dfn
11 | dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5
12 | h6 head header hr html i iframe img input ins kbd keygen label legend li link
13 | main map mark menu menuitem meta meter nav noscript object ol optgroup option
14 | output p param picture pre progress q rp rt ruby s samp script section select
15 | small source span strong style sub summary sup table tbody td textarea tfoot th
16 | thead time title tr track u ul var video wbr])
17 |
18 | (def ^:private attr-opts
19 | {:accept-charset :acceptCharset
20 | :accesskey :accessKey
21 | :allowfullscreen :allowFullScreen
22 | :autocomplete :autoComplete
23 | :autofocus :autoFocus
24 | :autoplay :autoPlay
25 | :class :className
26 | :colspan :colSpan
27 | :contenteditable :contentEditable
28 | :contextmenu :contextMenu
29 | :crossorigin :crossOrigin
30 | :datetime :dateTime
31 | :enctype :encType
32 | :formaction :formAction
33 | :formenctype :formEncType
34 | :formmethod :formMethod
35 | :formnovalidate :formNoValidate
36 | :formTarget :formtarget
37 | :hreflang :hrefLang
38 | :for :htmlFor
39 | :http-equiv :httpEquiv
40 | :maxlength :maxLength
41 | :mediagroup :mediaGroup
42 | :novalidate :noValidate
43 | :onabort :onAbort
44 | :onblur :onBlur
45 | :oncancel :onCancel
46 | :oncanplay :onCanPlay
47 | :oncanplaythrough :onCanPlayThrough
48 | :onchange :onChange
49 | :onclick :onClick
50 | :oncontextmenu :onContextMenu
51 | :oncompositionend :onCompositionEnd
52 | :oncompositionstart :onCompositionStart
53 | :oncompositionupdate :onCompositionUpdate
54 | :oncopy :onCopy
55 | :oncut :onCut
56 | :ondblclick :onDoubleClick
57 | :ondrag :onDrag
58 | :ondragend :onDragEnd
59 | :ondragenter :onDragEnter
60 | :ondragexit :onDragExit
61 | :ondragleave :onDragLeave
62 | :ondragover :onDragOver
63 | :ondragstart :onDragStart
64 | :ondrop :onDrop
65 | :ondurationchange :onDurationChange
66 | :onemptied :onEmptied
67 | :onencrypted :onEncrypted
68 | :onended :onEnded
69 | :onerror :onError
70 | :onfocus :onFocus
71 | :oninput :onInput
72 | :onkeydown :onKeyDown
73 | :onkeypress :onKeyPress
74 | :onkeyup :onKeyUp
75 | :onload :onLoad
76 | :onloadeddata :onLoadedData
77 | :onloadedmetadata :onLoadedMetadata
78 | :onloadstart :onLoadStart
79 | :onmousedown :onMouseDown
80 | :onmouseenter :onMouseEnter
81 | :onmouseleave :onMouseLeave
82 | :onmousemove :onMouseMove
83 | :onmouseout :onMouseOut
84 | :onmouseover :onMouseOver
85 | :onmouseup :onMouseUp
86 | :onpaste :onPaste
87 | :onpause :onPause
88 | :onplay :onPlay
89 | :onplaying :onPlaying
90 | :onprogress :onProgress
91 | :onratechange :onRateChange
92 | :onscroll :onScroll
93 | :onseeked :onSeeked
94 | :onseeking :onSeeking
95 | :onselect :onSelect
96 | :onstalled :onStalled
97 | :onsubmit :onSubmit
98 | :onsuspend :onSuspend
99 | :ontimeupdate :onTimeUpdate
100 | :ontouchcancel :onTouchCancel
101 | :ontouchend :onTouchEnd
102 | :ontouchmove :onTouchMove
103 | :ontouchstart :onTouchStart
104 | :onvolumechange :onVolumeChange
105 | :onwaiting :onWaiting
106 | :onwheel :onWheel
107 | :rowspan :rowSpan
108 | :spellcheck :spellCheck
109 | :srcdoc :srcDoc
110 | :srcset :srcSet
111 | :tabindex :tabIndex
112 | :usemap :useMap})
113 |
114 | (defn- mapm [fk fv m]
115 | (reduce-kv (fn [m k v] (assoc m (fk k) (fv v))) {} m))
116 |
117 | (defmacro generate-attr-opts []
118 | (flupot/clj->js (mapm name name attr-opts)))
119 |
120 | (defn- dom-symbol [tag]
121 | (symbol "js" (str "React.DOM." (name tag))))
122 |
123 | (defmacro define-dom-fns []
124 | `(do ~@(for [t tags]
125 | `(flupot/defelement-fn ~t
126 | :elemf ~(dom-symbol t)
127 | :attrf attrs->react))))
128 |
129 | (defn- boolean? [v]
130 | (or (true? v) (false? v)))
131 |
132 | (defn- to-str [x]
133 | (cond
134 | (keyword? x) (name x)
135 | (p/quoted? x) (to-str (second x))
136 | :else (str x)))
137 |
138 | (defn- fix-class [m]
139 | (let [cls (:class m)]
140 | (cond
141 | (and (or (vector? cls) (set? cls)) (every? p/literal? cls))
142 | (assoc m :class (str/join " " (core/map to-str cls)))
143 | (or (nil? cls) (string? cls) (number? cls) (boolean? cls))
144 | m
145 | :else
146 | (assoc m :class `(flupot.dom/fix-class ~cls)))))
147 |
148 | (defn- attrs->react [m]
149 | (flupot/clj->js (mapm #(name (attr-opts % %)) identity (fix-class m))))
150 |
151 | (defmacro define-dom-macros []
152 | `(do ~@(for [t tags]
153 | `(flupot/defelement-macro ~t
154 | :elemf ~(dom-symbol t)
155 | :attrf attrs->react
156 | :attrm attrs->react))))
157 |
158 | (define-dom-macros)
159 |
--------------------------------------------------------------------------------
/src/flupot/dom.cljs:
--------------------------------------------------------------------------------
1 | (ns flupot.dom
2 | (:refer-clojure :exclude [map meta time])
3 | (:require-macros [flupot.dom :as dom])
4 | (:require cljsjs.react
5 | [clojure.string :as str]
6 | [flupot.core :as flupot]))
7 |
8 | (def ^:private attr-opts
9 | (dom/generate-attr-opts))
10 |
11 | (defn- fix-class [v]
12 | (if (sequential? v)
13 | (str/join " " (cljs.core/map #(if (keyword? %) (name %) (str %)) v))
14 | (clj->js v)))
15 |
16 | (defn- attrs->react [attrs]
17 | (reduce-kv
18 | (fn [o k v]
19 | (let [k (name k)]
20 | (if (= k "class")
21 | (aset o "className" (fix-class v))
22 | (aset o (or (aget attr-opts k) k) (clj->js v)))
23 | o))
24 | (js-obj)
25 | attrs))
26 |
27 | (dom/define-dom-fns)
28 |
--------------------------------------------------------------------------------
/test/flupot/dom_test.clj:
--------------------------------------------------------------------------------
1 | (ns flupot.dom-test
2 | (:require [clojure.test :refer :all]
3 | [clojure.walk :as walk]
4 | [flupot.dom :as dom]))
5 |
6 | (def ^:private gensym-regex #"(_|[a-zA-Z0-9\-\'\*]+)#?_+(\d+_*#?)+(auto__)?$")
7 |
8 | (defn- gensym? [s]
9 | (and (symbol? s) (re-find gensym-regex (name s))))
10 |
11 | (defn- normalize-gensyms [expr]
12 | (let [counter (atom 0)
13 | re-gensym (memoize (fn [_] (symbol (str "__norm__" (swap! counter inc)))))]
14 | (walk/postwalk #(if (gensym? %) (re-gensym %) %) expr)))
15 |
16 | (deftest test-inline-macros
17 | (testing "literal option map"
18 | (is (= (macroexpand-1 '(flupot.dom/div {:class "foo"} "bar"))
19 | '(js/React.DOM.div (cljs.core/js-obj "className" "foo") "bar"))))
20 |
21 | (testing "event listeners"
22 | (is (= (macroexpand-1 '(flupot.dom/div {:onclick f} "foo"))
23 | '(js/React.DOM.div (cljs.core/js-obj "onClick" (cljs.core/clj->js f)) "foo"))))
24 |
25 | (testing "literal style attribute"
26 | (is (= (macroexpand-1 '(flupot.dom/div {:style {:background-color "red"}} "foo"))
27 | '(js/React.DOM.div
28 | (cljs.core/js-obj "style" (cljs.core/js-obj "background-color" "red"))
29 | "foo"))))
30 |
31 | (testing "symbols in style attribute"
32 | (is (= (macroexpand-1 '(flupot.dom/p {:style {:color x}} "foo"))
33 | '(js/React.DOM.p
34 | (cljs.core/js-obj "style" (cljs.core/js-obj "color" (cljs.core/clj->js x)))
35 | "foo"))))
36 |
37 | (testing "classes as literal vectors"
38 | (is (= (macroexpand-1 '(flupot.dom/p {:class ["foo" :bar 'baz]} "foo"))
39 | '(js/React.DOM.p (cljs.core/js-obj "className" "foo bar baz") "foo"))))
40 |
41 | (testing "classes as literal sets"
42 | (is (= (macroexpand-1 '(flupot.dom/p {:class #{"foo" :bar 'baz}} "foo"))
43 | '(js/React.DOM.p (cljs.core/js-obj "className" "foo bar baz") "foo"))))
44 |
45 | (testing "classes as symbols"
46 | (is (= (macroexpand-1 '(flupot.dom/p {:class c} "foo"))
47 | '(js/React.DOM.p
48 | (cljs.core/js-obj "className" (cljs.core/clj->js (flupot.dom/fix-class c)))
49 | "foo"))))
50 |
51 | (testing "literal arguments with no option map"
52 | (is (= (macroexpand-1 '(flupot.dom/div "foo" "bar"))
53 | '(js/React.DOM.div nil "foo" "bar"))))
54 |
55 | (testing "ambiguous option map"
56 | (is (= (normalize-gensyms (macroexpand-1 '(flupot.dom/span foo "bar" "baz")))
57 | (normalize-gensyms
58 | `(let [opts# ~'foo]
59 | (if (map? opts#)
60 | (js/React.DOM.span (flupot.dom/attrs->react opts#) "bar" "baz")
61 | (js/React.DOM.span nil opts# "bar" "baz")))))))
62 |
63 | (testing "ambiguous first child"
64 | (is (= (normalize-gensyms (macroexpand-1 '(flupot.dom/span {} bar "baz")))
65 | (normalize-gensyms
66 | `(let [bar# ~'bar]
67 | (if (or (seq? bar#))
68 | (let [args# (cljs.core/array)]
69 | (.push args# (cljs.core/js-obj))
70 | (flupot.core/push-child! args# bar#)
71 | (.push args# "baz")
72 | (.apply js/React.DOM.span nil args#))
73 | (js/React.DOM.span (cljs.core/js-obj) bar# "baz")))))))
74 |
75 | (testing "ambiguous last child"
76 | (is (= (normalize-gensyms (macroexpand-1 '(flupot.dom/span "bar" baz)))
77 | (normalize-gensyms
78 | `(let [baz# ~'baz]
79 | (if (or (seq? baz#))
80 | (let [args# (cljs.core/array)]
81 | (do (.push args# nil)
82 | (.push args# "bar"))
83 | (flupot.core/push-child! args# baz#)
84 | (.apply js/React.DOM.span nil args#))
85 | (js/React.DOM.span nil "bar" baz#)))))))
86 |
87 | (testing "ambiguous options and children"
88 | (is (= (normalize-gensyms (macroexpand-1 '(flupot.dom/span foo bar baz)))
89 | (normalize-gensyms
90 | `(let [bar# ~'bar, baz# ~'baz]
91 | (if (or (seq? bar#) (seq? baz#))
92 | (let [args# (cljs.core/array)]
93 | (let [opts# ~'foo]
94 | (if (map? opts#)
95 | (.push args# (flupot.dom/attrs->react opts#))
96 | (do (.push args# nil)
97 | (.push args# opts#))))
98 | (flupot.core/push-child! args# bar#)
99 | (flupot.core/push-child! args# baz#)
100 | (.apply js/React.DOM.span nil args#))
101 | (let [opts2# ~'foo]
102 | (if (map? opts2#)
103 | (js/React.DOM.span (flupot.dom/attrs->react opts2#) bar# baz#)
104 | (js/React.DOM.span nil opts2# bar# baz#))))))))))
105 |
--------------------------------------------------------------------------------
/test/flupot/dom_test.cljs:
--------------------------------------------------------------------------------
1 | (ns flupot.dom-test
2 | (:require [cljs.test :refer-macros [is deftest testing]]
3 | [flupot.dom :as flupot]))
4 |
5 | (deftest a-test
6 | (testing "FIXME, I fail."
7 | (is (= 0 1))))
8 |
--------------------------------------------------------------------------------