├── deps.edn ├── public ├── examples │ ├── style.css │ └── index.html └── benchmark │ └── index.html ├── src ├── data_readers.clj ├── thump │ ├── example.clj │ ├── core.cljc │ ├── react.cljc │ ├── benchmark.cljs │ └── examples.cljs └── react.clj ├── .gitignore ├── project.clj ├── package.json ├── shadow-cljs.edn ├── CODE_OF_CONDUCT.md ├── README.md └── LICENSE /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"]} 2 | -------------------------------------------------------------------------------- /public/examples/style.css: -------------------------------------------------------------------------------- 1 | /* devcards-demo CSS */ 2 | -------------------------------------------------------------------------------- /src/data_readers.clj: -------------------------------------------------------------------------------- 1 | {hiccup/element thump.core/parse 2 | h/e thump.core/parse} 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | public/examples/js 2 | /.nrepl-port 3 | .shadow-cljs/ 4 | node_modules/ 5 | public/benchmark/js/ 6 | -------------------------------------------------------------------------------- /src/thump/example.clj: -------------------------------------------------------------------------------- 1 | (ns thump.example 2 | (:require [thump.core :as hiccup])) 3 | 4 | 5 | (defn hiccup-element [& xs] xs) 6 | 7 | (hiccup/compile [:div {:baz {:asdf :jjkl}} 8 | "foo" "bar" 9 | [:span "baz"]]) 10 | -------------------------------------------------------------------------------- /src/react.clj: -------------------------------------------------------------------------------- 1 | (ns react) 2 | 3 | (defn createElement 4 | ([el props & children] 5 | ;; `[~el ~(when props `(react/props ~@props)) ~@children] 6 | `{"$$typeof" "Symbol(react.element)" 7 | :type ~el 8 | :key ~(:key props) 9 | :props ~props})) 10 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject lilactown/thump "0.0.1" 2 | :description "A library for parsing hiccup forms using reader tagged literals." 3 | :url "https://github.com/Lokeh/thump" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v20.html"} 6 | :source-paths ["src"]) 7 | -------------------------------------------------------------------------------- /public/benchmark/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hiccup-tag", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "benchmark": "^2.1.4", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6" 10 | }, 11 | "devDependencies": { 12 | "shadow-cljs": "^2.8.39", 13 | "showdown": "^1.9.0" 14 | }, 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "", 19 | "license": "ISC" 20 | } 21 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths 2 | ["src" "test" "examples"] 3 | 4 | :dependencies 5 | [[binaryage/devtools "0.9.7"] 6 | [devcards "0.2.5"]] 7 | 8 | :builds {:examples {:target :browser 9 | :output-dir "public/examples/js" 10 | :asset-path "/js" 11 | :modules {:main {:entries [thump.examples]}} 12 | :compiler-options {:devcards true} 13 | :devtools {:http-root "public/examples" 14 | :http-port 8800 15 | :preloads [devtools.preload]}} 16 | :benchmark-browser {:target :browser 17 | :output-dir "public/benchmark/js" 18 | :asset-path "/js" 19 | :modules {:main {:entries [thump.benchmark]}} 20 | :devtools {:http-root "public/benchmark" 21 | :http-port 8801}}}} 22 | -------------------------------------------------------------------------------- /src/thump/core.cljc: -------------------------------------------------------------------------------- 1 | (ns thump.core 2 | #?(:cljs (:require [cljs.reader])) 3 | (:refer-clojure :exclude [compile])) 4 | 5 | (defn keyword->str [k] 6 | (let [kw-ns (namespace k) 7 | kw-name (name k)] 8 | (if (nil? kw-ns) 9 | kw-name 10 | 11 | (str kw-ns "/" kw-name)))) 12 | 13 | (defn ^:dynamic *hiccup-element* [el props children] 14 | `(~'hiccup-element ~el ~props ~children)) 15 | 16 | (declare parse) 17 | 18 | (defn maybe-parse-child [c] 19 | (if (vector? c) 20 | (parse c) 21 | c)) 22 | 23 | (defn parse [vec] 24 | (if-not (vector? vec) 25 | (throw (ex-info (str vec " is not a valid hiccup vector.") {})) 26 | (let [[el props & children] vec 27 | 28 | ;; parse 29 | el (if (keyword? el) (keyword->str el) el) 30 | props? (map? props) 31 | children? (not (nil? (seq children))) 32 | children (cond 33 | (and props? children?) children 34 | children? (cons props children) 35 | props? '() 36 | true (list props)) 37 | props (if props? 38 | props 39 | nil)] 40 | (*hiccup-element* el props (map maybe-parse-child children))))) 41 | 42 | (defmacro compile [vec] 43 | (parse vec)) 44 | 45 | (defn interpret [vec] 46 | (parse vec)) 47 | 48 | #?(:cljs 49 | (do (cljs.reader/register-tag-parser! 'hiccup/element parse) 50 | (cljs.reader/register-tag-parser! 'h/e parse))) 51 | -------------------------------------------------------------------------------- /src/thump/react.cljc: -------------------------------------------------------------------------------- 1 | (ns thump.react 2 | (:require [clojure.string :as str] 3 | #?@(:cljs [["react" :as react] 4 | [goog.object :as gobj]]) 5 | [thump.core]) 6 | #?(:cljs (:require-macros [thump.react] 7 | [thump.core]))) 8 | 9 | (defn keyword->str [k] 10 | (let [kw-ns (namespace k) 11 | kw-name (name k)] 12 | (if (nil? kw-ns) 13 | kw-name 14 | 15 | (str kw-ns "/" kw-name)))) 16 | 17 | (defn- camel-case* 18 | "Returns camel case version of the string, e.g. \"http-equiv\" becomes \"httpEquiv\"." 19 | [s] 20 | (if (or (keyword? s) 21 | (string? s) 22 | (symbol? s)) 23 | (let [[first-word & words] (str/split (name s) #"-")] 24 | (if (or (empty? words) 25 | (= "aria" first-word) 26 | (= "data" first-word)) 27 | (name s) 28 | (-> (map str/capitalize words) 29 | (conj first-word) 30 | str/join))) 31 | s)) 32 | 33 | (defn map-entry->obj-entry [[k v]] 34 | (case k 35 | :style ["style" #?(:clj `(~'clj->js ~v :keyword-fn camel-case*) 36 | :cljs (clj->js v :keyword-fn camel-case*))] 37 | :class ["className" #?(:clj `(if (string? ~v) ~v 38 | (->> ~v (remove nil?) (str/join " "))) 39 | :cljs (if (string? v) 40 | v 41 | (->> v (remove nil?) (str/join " "))))] 42 | :for ["htmlFor" v] 43 | [(-> k (keyword->str) (camel-case*)) v])) 44 | 45 | #?(:cljs (defn merge-obj+map [obj m] 46 | (doseq [[k v] (map map-entry->obj-entry m)] 47 | (if (gobj/containsKey obj k) 48 | ;; do nothing if object already contains key 49 | nil 50 | (gobj/set obj k v))) 51 | obj)) 52 | 53 | (defn props->obj [m] 54 | (if (contains? m '&) 55 | #?(:clj `(merge-obj+map (~'js-obj ~@(mapcat map-entry->obj-entry (dissoc m '&))) 56 | ~(get m '&)) 57 | :cljs (merge-obj+map (apply gobj/create (mapcat map-entry->obj-entry (dissoc m '&))) 58 | (get m '&))) 59 | #?(:clj `(~'js-obj ~@(mapcat map-entry->obj-entry m)) 60 | :cljs (apply gobj/create (mapcat map-entry->obj-entry m))))) 61 | 62 | (def create-element 63 | #?(:clj (fn [& xs] xs) 64 | :cljs react/createElement)) 65 | 66 | #?(:cljs (def Fragment react/Fragment)) 67 | 68 | (def custom-els 69 | {"<>" #?(:clj `Fragment 70 | :cljs react/Fragment)}) 71 | 72 | (defmacro hiccup-element [el props children] 73 | `(create-element ~(get custom-els el el) ~(props->obj props) ~@children)) 74 | 75 | #?(:cljs (defn hiccup-element [el props children] 76 | (apply react/createElement 77 | (get custom-els el el) 78 | (props->obj props) 79 | children))) 80 | 81 | #?(:cljs (defn interpret [vec] 82 | (binding [thump.core/*hiccup-element* hiccup-element] 83 | (thump.core/interpret vec)))) 84 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at hello@willacton.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/thump/benchmark.cljs: -------------------------------------------------------------------------------- 1 | (ns thump.benchmark 2 | (:require 3 | ["react" :as react :rename {createElement rce}] 4 | ["react-dom/server" :as rdom] 5 | ["benchmark" :as benchmark] 6 | [thump.react :as r :refer [hiccup-element]] 7 | [cljs.reader])) 8 | 9 | (defn react-render [{:keys [title body]}] 10 | (rce "div" #js {:className "card"} 11 | (rce "div" #js {:className "card-title"} title) 12 | (rce "div" #js {:className "card-body"} body) 13 | (rce "div" #js {:className "card-footer"} 14 | (rce "div" #js {:className "card-actions"} 15 | (rce "button" nil "ok") 16 | (rce "button" nil "cancel"))))) 17 | 18 | (defn tag-render [{:keys [title body]}] 19 | #h/n [:div {:class "card"} 20 | [:div {:class "card-title"} title] 21 | [:div {:class "card-body"} body] 22 | [:div {:class "card-footer"} 23 | [:div {:class "card-actions"} 24 | [:button "ok"] 25 | [:button "cancel"]]]]) 26 | 27 | (defn macro-render [{:keys [title body]}] 28 | (thump.core/compile 29 | [:div {:class "card"} 30 | [:div {:class "card-title"} title] 31 | [:div {:class "card-body"} body] 32 | [:div {:class "card-footer"} 33 | [:div {:class "card-actions"} 34 | [:button "ok"] 35 | [:button "cancel"]]]])) 36 | 37 | (defn runtime-render [{:keys [title body]}] 38 | (r/interpret 39 | [:div {:class "card"} 40 | [:div {:class "card-title"} title] 41 | [:div {:class "card-body"} body] 42 | [:div {:class "card-footer"} 43 | [:div {:class "card-actions"} 44 | [:button "ok"] 45 | [:button "cancel"]]]])) 46 | 47 | (defn runtime-reader-render [{:keys [title body]}] 48 | (binding [thump.core/*hiccup-element* hiccup-element] 49 | (cljs.reader/read-string 50 | (str "#h/n [:div {:class \"card\"} 51 | [:div {:class \"card-title\"} \"" title "\"] 52 | [:div {:class \"card-body\"} \"" body "\"] 53 | [:div {:class \"card-footer\"} 54 | [:div {:class \"card-actions\"} 55 | [:button \"ok\"] 56 | [:button \"cancel\"]]]]")))) 57 | 58 | (defn log-cycle [event] 59 | (println (.toString (.-target event)))) 60 | 61 | (defn log-complete [event] 62 | (this-as this 63 | (js/console.log this))) 64 | 65 | (set! js/Benchmark benchmark) 66 | 67 | (defn ^:export main [& args] 68 | (let [test-data {:title "hello world" 69 | :body "body"} 70 | test-data-js #js {:title "hello world" 71 | :body "body"}] 72 | (println (rdom/renderToString (react-render test-data))) 73 | (println (rdom/renderToString (tag-render test-data))) 74 | (println (rdom/renderToString (macro-render test-data))) 75 | (println (rdom/renderToString (runtime-render test-data))) 76 | (println (rdom/renderToString (runtime-reader-render test-data))) 77 | 78 | (when-not (= (rdom/renderToString (react-render test-data)) 79 | (rdom/renderToString (tag-render test-data)) 80 | (rdom/renderToString (macro-render test-data)) 81 | (rdom/renderToString (runtime-render test-data)) 82 | (rdom/renderToString (runtime-reader-render test-data)) 83 | ) 84 | (throw (ex-info "not equal!" {}))) 85 | 86 | (-> (benchmark/Suite.) 87 | (.add "react" #(react-render test-data)) 88 | (.add "tag" #(tag-render test-data)) 89 | (.add "macro" #(macro-render test-data)) 90 | (.add "runtime" #(runtime-render test-data)) 91 | (.add "runtime-reader" #(runtime-reader-render test-data)) 92 | (.on "cycle" log-cycle) 93 | (.on "complete" log-complete) 94 | (.run)))) 95 | -------------------------------------------------------------------------------- /src/thump/examples.cljs: -------------------------------------------------------------------------------- 1 | (ns thump.examples 2 | (:require [devcards.core :as dc :include-macros true] 3 | [thump.react :as r :refer [hiccup-element]])) 4 | 5 | 6 | ;; 7 | ;; Boilerplate 8 | ;; 9 | 10 | (defn ^:dev/after-load start [] 11 | (dc/start-devcard-ui!)) 12 | 13 | (defn ^:export init [] (start)) 14 | 15 | (when (exists? js/Symbol) 16 | (extend-protocol IPrintWithWriter 17 | js/Symbol 18 | (-pr-writer [sym writer _] 19 | (-write writer (str "\"" (.toString sym) "\""))))) 20 | 21 | ;; 22 | ;; Examples 23 | ;; 24 | 25 | (defn t [] 26 | #hiccup/element [:div "hello"]) 27 | 28 | (dc/defcard basic 29 | #h/e [t]) 30 | 31 | (dc/defcard basic-short 32 | ;; h/e is an alias of hiccup/element 33 | #h/e [:div "hi"]) 34 | 35 | (dc/defcard basic-nested 36 | ;; we don't need to tag static children 37 | #h/e [:div [:span "hi"] " " [:span "bye"]]) 38 | 39 | (dc/defcard more-nested 40 | ;; we don't need to tag static children 41 | #h/e [:div 42 | [:div {:style {:color "green"}} 43 | [:span "hi"]] 44 | " " 45 | [:div [:h4 "bye" [:span {:style {:color "red"}} "bye"]]]]) 46 | 47 | (dc/defcard basic-props 48 | #h/e [:div {:style {:background "purple"}} 49 | [:button {:on-click #(js/alert "hi")} "say hello"]]) 50 | 51 | (dc/defcard dynamic-props 52 | (let [props {:style {:background "red" :color "yellow"}}] 53 | #h/e [:div {:on-click #(js/alert "static") 54 | & props} "asdf"])) 55 | 56 | (dc/defcard dynamic-props-static-override 57 | (let [props {:style {:background "red" :color "yellow"} 58 | :on-click #(js/alert "dynamic")}] 59 | #h/e [:div {:style {:background "blue" :color "white"} 60 | & props} "asdf"])) 61 | 62 | (dc/defcard classes 63 | #h/e [:<> 64 | [:style ".a { color: green; } .b { background: purple; }"] 65 | [:div {:class "a"} "green"] 66 | [:div {:class "b"} "purple"] 67 | [:div {:class ["a" "b"]} "gross"]]) 68 | 69 | (dc/defcard lazy-seq-and-binding 70 | ;; we have to tag children that are bound dynamically 71 | (let [neg-1 #h/e [:li -1]] 72 | #h/e [:ul 73 | neg-1 74 | ;; no tagging needed, static child 75 | [:li 0] 76 | ;; we also have to tag children that are generated dynamically 77 | (for [n [1 2 3 4 5]] 78 | #h/e [:li {:key n} n])])) 79 | 80 | (dc/defcard from-read-string 81 | (binding [thump.core/*hiccup-element* hiccup-element] 82 | #h/e [:div 83 | (cljs.reader/read-string 84 | "#hiccup/element [:div {:style {:border \"1px solid #eee\"}} 85 | [:span {:style {:color \"green\"}} 86 | \"from reader!\"]]") 87 | (cljs.reader/read-string 88 | "#h/e [:div 89 | [:style \".a2 { color: green; } .b2 { background: purple; }\"] 90 | [:div {:class \"a2\"} \"green\"] 91 | [:div {:class \"b2\"} \"purple\"] 92 | [:div {:class [\"a2\" \"b2\"]} \"gross\"]]") 93 | (try (cljs.reader/read-string 94 | "#h/e [:div {& props} \"asdf\"]") 95 | (catch js/Error e 96 | #h/e [:div {:style {:color (if (= (ex-message e) "props is not ISeqable") 97 | "green" 98 | "red")}} 99 | (str "Dynamic props doesn't work: " (ex-message e) " " 100 | (if (= (ex-message e) "props is not ISeqable") 101 | "✅" 102 | "🚫"))]))])) 103 | 104 | (dc/defcard macro-compiler 105 | (thump.core/compile 106 | [:div 107 | "foo" 108 | [:button {:on-click #(js/alert "baz")} "bar"]])) 109 | 110 | (dc/defcard runtime-interpreter 111 | (r/interpret [:div 112 | "foo" 113 | [:button {:on-click #(js/alert "baz")} "bar"]])) 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # thump 2 | 3 | ## UNDER CONSTRUCTION 4 | 5 | A library for parsing hiccup forms using reader tagged literals. Currently supports React. 6 | 7 | ```clojure 8 | (ns my-app.core 9 | (:require [thump.core] 10 | [thump.react :refer [hiccup-element]])) 11 | 12 | (defn Mycomponent [props] 13 | (let [name (goog.object/get props "name")] 14 | #h/e [:div {:style {:color "green"}} 15 | [:span "Hello, " name] 16 | [:ul 17 | (for [n (range 10)] 18 | #h/e [:li {:key n} n])]])) 19 | 20 | (react-dom/render #h/e [MyComponent {:name "Sydney"}] 21 | (. js/document getElementById "app")) 22 | ``` 23 | 24 | 25 | ## Why reader tags? 26 | 27 | Reader tags are excellent for succinctly writing code-as-data. They can be used 28 | while writing code in our editor, as well as sent over the wire using the EDN 29 | reader. 30 | 31 | Reader tags are also much more performant than doing the hiccup parsing at 32 | runtime. Typically, runtime hiccup parsing involves: 33 | 34 | 1. Construct vectors representing hiccup data 35 | 2. Parse vectors and turn them into function calls. 36 | 3. Execute the functions to construct the target type (e.g. React elements) 37 | 38 | If your entire app is written using hiccup, these steps will be done for every 39 | single component in your tree. 40 | 41 | Using reader tags allows us to move steps **1** and **2** to compile time, so 42 | that our application only has to execute the functions to construct the target 43 | type at runtime. 44 | 45 | (Sidenote: for React developers, this is the exact same thing that JSX does!) 46 | 47 | 48 | ## Usage 49 | 50 | `thump` exports two reader tags at the moment: `hiccup/element`, which parses 51 | hiccup literals, and `h/e`, which is a shortened alias of `hiccup/element`. 52 | 53 | In order to use it, you must require the `thump.core` namespace at the top 54 | level of your application: 55 | 56 | ```clojure 57 | (ns my-app.core 58 | (:require [thump.core] 59 | ...)) 60 | ``` 61 | 62 | This will ensure the reader tags are registered with the ClojureScript compiler. 63 | 64 | ### With React 65 | 66 | `thump` is meant to be a general purpose hiccup syntax parsing library. An 67 | example implementation of a React extension is included with the library under 68 | the `thump.react` namespace. 69 | 70 | In order to use hiccup to create React elements, simply include the namespace 71 | and **refer the `hiccup-element` var**: 72 | 73 | ``` 74 | (my-app.feature 75 | (:require [thump.react :refer [hiccup-element]])) 76 | ``` 77 | 78 | We can then start creating React elements: 79 | 80 | ```clojure 81 | #hiccup/element [:div "foo"] 82 | ;; Executes => (react/createElement "div" nil "foo") 83 | ``` 84 | 85 | ### Elements 86 | 87 | Elements in the first position of a `hiccup/element` / `h/e`-tagged form are 88 | expected to be one of the following: 89 | 90 | - A keyword representing a DOM element: `:div`, `:span`, `:h1`, `:article` 91 | - A vanilla React component or one of the special React components like `Fragment` 92 | - A set of special keywords that `thump` exposes: 93 | - `:<>` as an alias for Fragments 94 | 95 | 96 | ### Props 97 | 98 | Props are expected to be passed in as map literals with keywords as keys, 99 | such as `{:key "value"}`. 100 | 101 | The top-level map will be rewritten as a JS object at compile time. Any nested 102 | Clojure data will be left alone. Keys are converted from kebab-case to camelCase. 103 | 104 | Example: 105 | 106 | ```clojure 107 | #h/e [:div {:id "thing-1" :some-prop {:foo #{'bar "baz"}}}] 108 | ;; => (react/createElement "div" 109 | ;; (js-obj "id" "thing-1" 110 | ;; "someProp" {:foo #{'bar "baz"}})) 111 | ``` 112 | 113 | There are 3 exceptions to this: 114 | - `:style` - will be recursively converted to a JS object via `clj->js` 115 | - `:class` - will be renamed to `className` and (if its a collection) joined as a string 116 | - `:for` - will be renamed to `htmlFor` 117 | 118 | Example of special cases: 119 | 120 | ```clojure 121 | #h/e [:div {:class ["foo" "bar"] 122 | :style {:color "green"} 123 | :for "thing"}] 124 | ;; => (react/createElement "div" 125 | ;; (js-obj "className" "foo bar" 126 | ;; "style" (clj->js {:color "green"}) 127 | ;; "htmlFor" "thing")) 128 | ``` 129 | 130 | ### Dynamic props 131 | 132 | Using `thump`, props must _always_ be a literal map. For instance, the 133 | following **will throw a runtime error**: 134 | 135 | ```clojure 136 | (let [props {:style {:color "red"}}] 137 | #h/e [:div props "foo"]) 138 | ``` 139 | 140 | When the tag reader encounters `props` in the hiccup form, it assumes it is a 141 | child element and passes it in to React's `createElement` function like so: 142 | 143 | ```clojure 144 | (let [props {:style {:color "red"}}] 145 | (react/createElement "div" nil props "foo")) 146 | ``` 147 | 148 | Since `props` is a map, not a React element, when used it will cause React to throw an "unknown element type" error. 149 | 150 | The only way to tell the tag reader to treat `props` as, well, props, is to 151 | write it literally within the hiccup form: 152 | 153 | ```clojure 154 | #h/e [:div {:style {:color "red"}} "foo"] 155 | ``` 156 | 157 | But **what if we want to assign them dynamically?** For example, we want to 158 | set some data conditionally: 159 | 160 | ```clojure 161 | (if condition 162 | {:style {:color "red"}} 163 | {:style {:color "green"}}) 164 | ``` 165 | 166 | Then we can tell the reader to merge our dynamically created map with the `&` prop: 167 | 168 | ```clojure 169 | (let [props (if condition 170 | {:style {:color "red"}} 171 | {:style {:color "green"}})] 172 | #h/e [:div {& props} "foo"]) 173 | ``` 174 | 175 | The value at the key `&` will be merged into the resulting props object at 176 | runtime so that we can do this kind of dynamic props creation. 177 | 178 | Keys are merged in such a way where the values in the map created statically 179 | take precedence. For example: 180 | 181 | ```clojure 182 | (let [props {:style {:color "red"} 183 | :on-click #(js/alert "hi")}] 184 | #h/e [:div {:style {:color "blue"} & props} 185 | "foo"]) 186 | ``` 187 | 188 | Results in props `#js {:style #js {:color "blue"} :onClick #(js/alert)}` being 189 | passed in to React. 190 | 191 | ### Nested hiccup 192 | 193 | Often, our hiccup is not just one layer deep. We often want to write a tree of 194 | elements like: 195 | 196 | ```html 197 |