ClojureScript library for templating React.createElement calls with keyword vectors.
Currently for my personal use. Future breaking changes possible.
With the release of React hooks, it is now strait forward to write React components in ClojureScript using ordinary functions, forgoing the need of wrapper libraries. However, writing components using direct calls to React.createElement is verbose. In JavaScript, JSX provides templating of React.createElement calls. Veil is meant to be the JSX equivalent for ClojureScript by providing a macro to template React.createElement calls with.
- No wrapping code! Vectors transform directly into
React.createElementcalls at compile time. - Easy to write functional components that interops well with other React features (hooks, context, memo, etc...) without the need of special code.
- Support for user-defined and React-defined components with the same consistent syntax.
- Very small codebase of less than 200 lines.
- You will be expose directly to React and how it works. Beware of situations where React expects JavaScript objects (and not Clojure maps)!
Veil is a deps project available from its git coordinate. Add the following to your deps.edn.
{:deps {dev.onionpancakes/veil
{:git/url "https://github.com/onionpancakes/veil.git"
:git/sha "<commit sha goes here>"}}}- Clojure 1.10.0 or later.
Reactmust be in scope.
Install npm deps:
$ npm iTo running tests, execute:
$ make testTo update test snapshots, execute:
$ make update-testsRequire Veil in your ClojureScript file.
(ns myproject.app
(:require [dev.onionpancakes.veil.core :as v]))Use the compile macro to transform keyword vectors into React.createElement calls.
(v/compile [:h1 "Hello World!"])
;; expands into
(js/React.createElement "h1" nil "Hello World!")Combine compile with defn to write functional components.
(defn MyComponent [props]
(v/compile
[:div
[:h1 "Hello World!"]
[:p "Foo bar baz"]]))Then combine functional components with hooks as needed.
(defn Example [props]
(let [[cur-count set-count!] (js/React.useState 0)]
(v/compile
[:div
[:p (str "You clicked " cur-count " times")]
[:button {:onClick #(set-count! (inc cur-count))}
"Click me"]])))Use compile where React.createElement would be needed.
(js/ReactDOM.render (v/compile [:MyComponent])
(js/document.getElementById "app"))Veil follows JSX's type semantics when determining the type of the element.
When Veil sees a capitalized tag, the keyword is converted to a symbol.
(v/compile [:MyComponent])
;; expands into
(js/React.createElement MyComponent)For components with capitalized tags, the namespace on the tag is preserved. This allows components to be referenced from other namespaces.
(v/compile [:my-ns/MyOtherComponent])
;; expands into
(js/React.createElement my-ns/MyOtherComponent)Use namespace aliases in keywords as normal.
(v/compile [::my-ns-alias/MyOtherComponent])Refer to components in the same namespace with global keywords or double colon keywords.
(v/compile [:MyOtherComponentHere])
;; or
(v/compile [::MyOtherComponentHere])Because capitalized tags transform to symbols, you can use dot access to access inner components.
(v/compile [:OuterThing.InnerThing])
;; expands to
(js/React.createElement OuterThing.InnerThing)A few examples using React's functionality.
https://reactjs.org/docs/fragments.html
(v/compile
[:js/React.Fragment
[:ChildA]
[:ChildB]
[:ChildC]])https://reactjs.org/docs/react-api.html#reactmemo
(def MyComponent
(js/React.memo
(fn [props]
(v/compile
;; render using props
))))https://reactjs.org/docs/refs-and-the-dom.html
(defn CustomTextInput [props]
(let [input-ref (js/React.createRef)]
(v/compile
[:div
[:input {:type "text"
:ref input-ref}]
[:input {:type "button"
:value "Focus the text input"
:onClick #(.. input-ref -current focus)}]])))If the second element is a keyword or map, it will be interpreted as props. Otherwise, props will be nil.
(v/compile [:div "foo"])
;; expands into
(js/React.createElement "div" nil "foo")React only works with JavaScript objects for props. Veil converts map props into JavaScript objects.
(v/compile [:div {:id "foo"}])
;; expands into
(js/React.createElement "div" #js {:id "foo"})Veil does not convert prop keys from kabob-case to camelCase. Use camelCase like normal React.
(v/compile [:div {:class-name "foo bar"}]) ; No
(v/compile [:div {:className "foo bar"}]) ; YesFor global keys, Veil does not transform their values. React expects JavaScript object for keys such as :style.
(v/compile [:div {:style {:color "green"}} "foo"]) ; No
(v/compile [:div {:style #js {:color "green"}} "foo"]) ; YesThe key ::v/classes must have a map as a value. The keys are classes and the values can be any expression. If the expression returns a truthy value, the class is included within :className.
(v/compile
[:div {::v/classes {:foo true
:bar false
:baz (= 0 0)}}])
;; Values are evaluated at runtime!
;; Keys with truthy values are joined into :className.
;; Example above will produce this on render.
<div class="foo baz"></div>Keywords props expand into :id and :className keys in the props object at compile time.
(v/compile [:div :#foo.bar.baz])
;; expands into
(js/React.createElement "div" #js {:id "foo"
:className "bar baz"})Keyword vectors inside props will not be transformed into React elements.
(v/compile
[:button {:onClick (fn []
;; Sends a vector, not an element.
(my-dispatch! [:action :foo]))}
"My Button"])There are a few ways to pass components to another component's props.
(v/compile
[:MyComponent {:children (v/compile
[[:ChildComponentA]
[:ChildComponentB]])}])(v/compile
(let [children [[:ChildComponentA]
[:ChildComponentB]]]
[:MyComponent {:children children}]))Use ^::v/skip to escape keyword vectors which should not be React elements.
(v/compile [:div (get my-map ^::v/skip [:not :an :element])])Map keys will not be transformed into React elements.
(v/compile
(let [my-map {[:not :an :element] ; Will not be an element.
[:div "foo"]}] ; Will be an element.
[:div (get my-map ^::v/skip [:not :an :element])]))