├── .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 | --------------------------------------------------------------------------------