├── 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 |
198 |
199 |
200 |
201 | ``` 202 | 203 | For convenience, if the reader encounters a nested vector literal within a hiccup 204 | form, it will treat it as a child element and read it just like another hiccup 205 | form. This means we can write the above without repeating the `#h/e` tag over and 206 | over: 207 | 208 | ```clojure 209 | #h/e [:div 210 | [:div [:label "Name: " [:input {:type "text"}]]] 211 | [:div [:button {:type "submit"} "Submit"]]] 212 | ``` 213 | 214 | However, the hiccup reader will not continue to walk inside of anything but a 215 | vector. If we need to insert parens into the form in order to do something more 216 | dynamic we'll have to ensure that we return a React element ourselves. 217 | 218 | The following **will probably throw a runtime error**: 219 | 220 | ```clojure 221 | #h/e [:div 222 | [:div "The condition is:"] 223 | (if condition 224 | [:div "TRUE"] 225 | [:div "FALSE"])] 226 | ``` 227 | 228 | To fix it, we make sure our dynamic children are read as hiccup as well by 229 | tagging them: 230 | 231 | ```clojure 232 | #h/e [:div 233 | [:div "The condition is:"] 234 | (if condition 235 | #h/e [:div "TRUE"] 236 | #h/e [:div "FALSE"])] 237 | ``` 238 | 239 | This is the case for any other kind of form like `for`, `cond`, `map`, etc. 240 | 241 | 242 | 243 | ## License 244 | 245 | EPL 2.0 Licensed. Copyright Will Acton. 246 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | --------------------------------------------------------------------------------