Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/peterhudec/cljsx
cljsx
https://github.com/peterhudec/cljsx
Last synced: 24 days ago
JSON representation
cljsx
- Host: GitHub
- URL: https://github.com/peterhudec/cljsx
- Owner: peterhudec
- Created: 2019-03-29T18:20:52.000Z (over 5 years ago)
- Default Branch: master
- Last Pushed: 2022-06-22T06:25:14.000Z (over 2 years ago)
- Last Synced: 2024-08-04T21:39:56.178Z (3 months ago)
- Language: Clojure
- Size: 342 KB
- Stars: 14
- Watchers: 1
- Forks: 0
- Open Issues: 7
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- awesome-clojurescript - cljsx - jsx.html) for Clojure and ClojureScript, also works with [Inferno](https://infernojs.org), [Nerv](https://nerv.aotu.io/), [Preact](https://preactjs.com/), [Snabbdome](https://github.com/snabbdom/snabbdom) and others. (Awesome ClojureScript / [React.js](https://facebook.github.io/react/) Interface)
README
# `cljsx` The Missing [JSX] Clojure Macro
[![Clojars Project](https://img.shields.io/clojars/v/cljsx.svg)](https://clojars.org/cljsx)
[![Build Status](https://travis-ci.org/peterhudec/cljsx.svg?branch=master)](https://travis-ci.org/peterhudec/cljsx)`cljsx` tries to make it as easy as possible to use plain, unwrapped [React]
(or any other virtual dom) and all the related JavaScript libraries in
ClojureScript by mimicking the syntax of [JSX]. It's mainly meant to be used
with the amazing [shadow-cljs] with its effortless usage of plain NPM packages,
but it works just as well with [Figwheel] and Clojure.## TL;DR
It's just a macro which transforms occurences of _JSX expressions_ like
`("Hello" "World!")` into
`(createElement "div" {:className "foo"} "Hello" "World!")`.
`cljsx` doesn't really care about what `createElement` points to. It only tries
to figure out whether it's a JavaScript or Clojure function so it can convert
the arguments with [clj->js] if needed.### Features
* Not yet another [React] wrapper, just a macro.
`.
* Works with all _vdom_ libraries which have the `createElement` signature.
* JSX expressions can appear anywhere and can be arbitrarily nested. That's why
it works so well with libraries like [React Router] or [Material-UI].
Whole namespaces can be wrapped in the macro.
* Has _props spread_ operator similar to `
* Supports _truthy prop_ shorthand as in ``.
* Automatically converts from and to JS if needed.
* Built with [spec], so you'll know early when something goes wrong.Let's say you want to write a SPA directly in [React], not using any wrapper.
Maybe you want to use [React Router] or some other JavaScript library
and you don't want to be dealing with incompatibility issues with wrappers like [reagent].
You would directly use the `react/createElement` function which you assign to `h` for brevity:```clj
(ns shadow-cljs-example.main
(:require ["react" :as react]
["react-dom" :as react-dom]
["react-router-dom" :as rr]))(def h react/createElement)
(react-dom/render
(h rr/BrowserRouter nil
(h rr/Route nil
(fn [route-props]
(h react/Fragment nil
(h "h1" nil (-> route-props .-location .-pathname (subs 1)))
(h "ul" nil
(map #(h "li" #js{:key %}
(h rr/Link #js{:to %} %))
["foo" "bar" "baz"]))))))
(js/document.querySelector "#mount-point"))
```The `cljsx.core/jsx>` macro call in the following example just expands
to something very similar and equivalent to the example above,
except that the code is a bit more readable and looks more familiar
to someone with [React] background.```clj
(ns shadow-cljs-example.main
(:require ["react" :refer [createElement Fragment]]
["react-dom" :as react-dom]
["react-router-dom" :as rr]
[cljsx.core :refer [jsx> fn-clj]]))(jsx>
(react-dom/render
(
(
(fn-clj [{{path :pathname} :location}]
(<>
((subs path 1))
(
(map #(
( %))
["foo" "bar" "baz"]))))))
(js/document.querySelector "#mount-point")))
```And this is what it would look like written in [reagent].
Notice the `:>` in front of every [React Router] component,
the need to wrap the return value of the `rr/Route` callback
in `r/as-element` and also, that using the anonymous function
shorthand is a bit awkward with the [hiccups] vector markup,
all of which just add noise to the code.```clj
(ns shadow-cljs-example.main
(:require [reagent.core :as r]
["react-router-dom" :as rr]))(r/render
[:> rr/BrowserRouter
[:> rr/Route
(fn [route-props]
(r/as-element
[:<>
[:h1 (-> route-props .-location .-pathname (subs 1))]
[:ul (map #(identity ^{:key %} [:li
[:> rr/Link {:to %} %]])
["foo" "bar" "baz"])]]))]]
(js/document.querySelector "#mount-point"))
```## Motivation
If you think about it, [JSX] is just a _reader macro_, which merely adds
syntactic sugar to JavaScript. Oddly enough in ClojureScript, the problem of
working with [React] is mostly approached by inventing all sorts of wrappers,
which more often than not come bundled with their own state managemet.
The most idiomatic way to express [React] DOM trees in Clojure seems to be the
[hiccups] format of nested vectors, which probably stems from the obsession with
data in Clojure (list's don't seem to be data enough).`cljsx.core/jsx>` is the missing macro. It's the main workhorse of the package
alongside a bunch of helpers for taming the JavaScript conversion.
The main goal of the package is expressiveness, readability and familiarity with
[React] idioms. `cljsx` is not tied to [React] in any way other than through the
signature of the `createElement` function and it works with all [JSX]
compatible libraries like [Inferno], [Nerv], [Preact] or [Snabbdom].## Usage
The main thing `cljsx` provides is the `jsx>` macro and its brethren `react>`,
`snabbdom>`, `inferno>` etc. The macro will recognize _JSX expressions_ in the
code passed to it and will expand them into `createElement` calls. Anything else
will be kept unchanged:```clj
(macroexpand (cljs/jsx> ()))
;; => (createElement "div" nil)
```So if you want the above example to work with [React], you should _refer_
`createElement` from the `react` namespace. Similarly, if you intend to use the
`<>` _fragment tag_, you should also refer the `Fragment` name from the same
namespace.```clj
(ns my-react-app.main
(:require [cljsx.core :refer [jsx>]]
[react :refer [createElement Fragment]]
[react-dom :as react-dom]))(react-dom/render
;; The JSX macro call
(jsx>
;; This s-expression is recognized as a JSX expression, because the item
;; at the function call position is recognized as a JSX tag by the macro.
(<>
;; This is not recognized as a jsx-expression, because the first item of
;; the s-expression is not a JSX tag.
(str "Hello, ")
;; This is a JSX expression, because the first item is a JSX tag.
("CLJSX!")))
(js/document.querySelector "#mount-point"));; The jsx> macro call expands to
(createElement Fragment nil
(createElement "h1" nil "Hello, CLJSX!"))
```The `cljsx.core/jsx>` macro expands to `createElement` and `Fragment` calls, which in
JSX parlance is called _pragma_. If you will be using `cljsx` with [React], you
can save some keystrokes by using the `cljsx.core/react>` macro, which works exactly
as the `jsx>` macro, except that the _pragma_ is bound to `react/createElement`
and `react/Fragment`.```clj
(ns my-react-app.main
(:require [cljsx.core :refer [react>]]
[react :as react]
[react-dom :as react-dom]))(react-dom/render
(react>
(<>
("Hello, CLJSX!")))
(js/document.querySelector "#mount-point"))
```There are other macros with _pragmas_ bound to other [JSX] compatible frameworks.
* `inferno>` bound to `inferno-create-element/createElement` and
`inferno/Fragment`.
* `nervjs>` bound to `nervjs/createElement` and `Fragment`
* `preact>` bound to `preact/createElement` and `Fragment`
* `snabbdom>` bound to `snabbdom/createElement` and `Fragment`Notice that the last three macros are bound to unprefixed `Fragment`. This is
because none of these frameworks seems to actually have a notion of a _fragment_.
Having it bound to `Fragment` allows you to choose how the fragment will be
interpreted. You could for example fall back to a _div_ with
`(def Fragment "div")` or make it a function which throws an exception, or just
let it fail on the undeclared `Fragment` var.You can even create your own JSX macro with the `cljsx.core/defjsx` macro, which is
how all the aforementioned macros are defined. Note that as `defjsx` is a macro
which creates macros, it needs to be called in a `clj` or `cljc` file.```clj
;; my_jsx_app/main.clj
(ns my-jsx-app.main
(:require [cljsx.core :as cljsx]))(cljsx/defjsx my-jsx-macro my-create-element my-fragment)
(def my-fragment :fragment)
(defn my-create-element [tag props & children]
{:tag tag
:props props
:children children})(my-jsx-macro
(<>
("Hello, CLJSX!")))
;; The above statement returns
{:tag :fragment
:props nil
:children [{:tag "h1"
:props nil
:children ["Hello, CLJSX!"]}]}
```### What is a JSX Expression?
Any list whose first element is a _JSX tag_ is recognized as a _JSX_ expression.
### What is a JSX Tag?
Any symbol in the function call position starting with the `<` character,
followed by an alphanumeric character and optionally ending with the `>`
character is a _JSX tag_. The string of characters between the opening `<` and
the optional closing `>` is the _tag name_ e.g. `<>`, ``, ``,
``, ``, ``, `<<>`,
`<..foo>`. This is to fail early on [spec] violation, as opposed to undeclared
var errors.#### Intrinsic Tags
`JSX tags` with all lowercase, alphanumeric _names_ are _intrinsic_ and will
expand to a `createElement` call with the tag name in a string literal e.g.
``, ``, ``, ``, ``.
```clj
(macroexpand (cljsx/jsx> ()))
;; => (createElement "foo" nil)
```#### Reference Tags
`JSX tags` whose names are capitalized e.g. `` are _reference tags_ and
will be expanded with the name as symbol. Tag names can be namespaced in the
same way as symbols with the only exception, that the last part must be
capitalized e.g. ``, ``, in which case they are also
considered to be _reference tags_.```clj
(macroexpand (cljsx/jsx> ()))
;; => (createElement Foo nil)(macroexpand (cljsx/jsx> ()))
;; => (createElement foo.bar/Baz nil)(macroexpand (cljsx/jsx> ()))
;; => (createElement foo/bar.Baz nil)
```#### Invalid Tags
Anything else is recognized as an _invalid tag_ and will be reported by the
[spec] during compile time.### Simple JSX Expressions Without _Props_
If a tag ends with the `>` character, it has no _props_ or _attributes_ and the
_JSX expression_ expands to a `createElement` call with `nil` as the second
argument.```clj
(macroexpand (cljsx/jsx> ()))
;; => (createElement "foo" nil)(macroexpand (cljsx/jsx> ()))
;; => (createElement Bar nil)
```### JSX Expressions With _Props_
If the tag does *not* end with the `>` character, the `jsx>` macro will
will expect a _sequence of props_ after the tag, terminated by the `>` symbol.
A _prop_ is either:* A single keyword
* A keyword followed by a non-keyword expression
* The `...` symbol followed by a _spreadable_ expression.Note that `cljsx` doesn't do any _prop_ name conversion e.g. hyphenization, etc,
as it is none of its business. If you wan't to write [React] props hyphenized,
or, you wan't to use `:class` instead of `:className`, you can wrap
`react/createElement` in your own `createElement` implementation
where you can intercept the props and forward them transformed to [React]:```clj
(ns shadow-cljs-example.main
(:require ["react" :as react]
["react-dom" :as react-dom]
[camel-snake-kebab.core :as csk]
[cljsx.core :as cljsx]))(defn createElement [tag props & children]
(apply react/createElement
tag
(->> props
(reduce-kv #(assoc %1 (csk/->camelCase %2) %3) {})
clj->js)
children))(cljsx/jsx>
(react-dom/render
(
"Bar")
(js/document.querySelector "#mount-point")))
```#### Key-value _Props_
A key-value prop is a keyword followed by any non-keyword expression e.g
`:foo 123`, `:bar baz`, `:bing (+ 1 2 3)`. _JSX expressions_ with this kind of
props will be expanded to a `createElement` call with the second argument being
a map of the props.```clj
(cljsx/jsx>
());; Expands to
(createElement "a" {:href "http://example.com" :target "_blank"})
``````clj
(cljsx/jsx>
());; Expands to
(createElement "a" {:onClick #(println "click")
:disabled (not enabled)})
```#### Shorthand Truthy _Props_
As JSX, `cljsx` supports shorthand _truthy props_, which is any keyword
followed by either another keyword or the `>` or `...` symbols.
A _shorthand prop_ will be expanded to a value of `true`.```clj
(cljsx/jsx>
())(cljsx/jsx>
());; Both expand to
(createElement "button" {:disabled true :onClick #(println "click")})
```#### The `...` _Spread_ Operator
As JSX, `cljsx` supports _spreading_ a map (or anything _spreadable_) to the
_props_. Anything followed by the `...` symbol will be merged with the rest of
the props using [merge] in the order
they appear in the _props_ sequence.```clj
(def props {:baz 33})(cljsx/jsx>
());; Expands to
(createElement Foo (merge {:foo 11 :bar 22}
{:foo 111}
{:baz 33}
props
[:bing 444]
{:bing true}))
```### Children
All expression after a _simple tag_ or the first `>` symbol after a _props tag_
in a _JSX expression_ are treated like _children_ and are added as parameters
right after _props_ to the expanded _JSX expression_.```clj
(cljsx/jsx>
("Hello, World!"));; Expands to
(createElement "div" nil "Hello, World!")(cljsx/jsx>
( "Hello," " Aqua" " World!"));; Expands to
(createElement "span" {:style {:color "aqua"}} "Hello," " Aqua" " World!")
```### Fragment
As [JSX], `cljsx` supports the `<>` _fragment_ tag and as in [JSX] it can't have
_props_. A _JSX expression_ with a fragment tag expands to a `createElement`
call with the `Fragment` symbol as the tag argument.```clj
(cljsx/jsx>
(
(<>
(- "Foo")
(- "Bar")
(- "Baz"))))
;; Expands to
(createElement "ul" nil
(createElement Fragment nil
(createElement "li" nil "Foo")
(createElement "li" nil "Bar")
(createElement "li" nil "Baz")))
```If you need to pass _props_ to the tag, e.g. the `key` prop in [React],
you can do it by using `Fragment` directly without the `<>` syntax.```clj
(cljsx/jsx>
(
("Foo")));; Expands to
(createElement Fragment {:key 123}
(createElement "div" nil "Foo"))
```
### Nesting_JSX expressions_ can appear anywhere inside of the macro and can be arbitrarily
deeply nested. They can appear as values in all the datastructure literals like
lists, vectors, maps, sets. They can even appear as keys in maps. Moreover, they
can appear as _prop_ values, which is a common pattern in [React] libraries like
[React Router] and [Material-UI]. You can even use it as values in spreads as
long as it returns a value accepted by [merge].```clj
(let [Header (fn [{:keys [children]}]
("Hello, " children "!"))
Body (fn [{:keys [items]}]
(
(map #(
(second %))
items)))
footer ("Enjoy!")]
(
( "CLJSX")
( "Foo")
:bar ("Bar")
:baz ("Baz")} >)
footer))
```The `jsx>` macro accepts any number of expressions. Multiple expressions
will be expanded wrapped in a [do](https://clojuredocs.org/clojure.core/do)
form. You can actually wrapp a whole namespace in the macro:```clj
(ns my-shadow-cljs-react-app.main
(:require ["react" :refer [createElement Fragment]]
["react-dom" :refer [render]]
[cljsx.core :refer [jsx>]]))(jsx>
(defn Header []
("Hello, CLJSX!"))
(defn Body []
(
"Lorem ipsum dolor sit amet."))
(defn Footer []
("Enjoy!"))
(defn App []
(
()
()
()))(render
()
(js/document.querySelector "#mount-point")))
```### Escaping
The `jsx>` macro uses some symbols as part of its DSL like the `...` as
the _props spread_ operator, and the `>` as the end of _props tag_.
It also specially treats symbols on the function call position whose names start
with the `<` character, which it recognises as _JSX tags_.The macro doesn't have any special escape mechanism, but it relies on what's
already available in Clojure. So if you need to pass the `>` function as a value
of a prop, or you happen to have a var assigned to a symbol starting with the
`<` character, you can dereference the vars with the `@` reader macro:```clj
(ns my-shadow-cljs-react-app.main
(:require ["react" :refer [createElement Fragment]]
["react-dom" :refer [render]]
[cljsx.core :refer [jsx>]]
;; You can not use the ... symbol in def, but you can import it.
[made-up-three-dots-exporting-namespace :refer [...]]))(defn
[] "My name is")
(def "My name is ")(defn SortableList [{:keys [comparator items]}]
(jsx>
(
(map #(- %) (sort comparator items)))))
(jsx>
(render
;; Would be interpreted as props spread operator if not escaped.
(