diff --git a/dev/workshop/core.cljs b/dev/workshop/core.cljs index a873618..0708e2d 100644 --- a/dev/workshop/core.cljs +++ b/dev/workshop/core.cljs @@ -358,11 +358,11 @@ (simple-benchmark [] (rds/renderToString - (r/createElement - react-children-benchmark - #js {:foo "bar"} - (r/createElement "div" #js {:style #js {:backgroundColor "green"}} "foo") - (r/createElement "div" nil "bar"))) + (helix/jsxs react-children-benchmark + #js {:foo "bar" + :children #js [(helix/jsx "div" #js {:style #js {:backgroundColor "green"} + :children "foo"}) + (helix/jsx "div" #js {:children "bar"})]})) iterations))) helix-time (hooks/use-memo diff --git a/src/helix/core.clj b/src/helix/core.clj index dee6a00..e91729e 100644 --- a/src/helix/core.clj +++ b/src/helix/core.clj @@ -2,8 +2,14 @@ (:require [helix.impl.analyzer :as hana] [helix.impl.props :as impl.props] + [cljs.tagged-literals :as tl] [clojure.string :as string])) +(defn- jsx-children [coll] + (let [s (seq coll)] + (if (and s (next s)) + (tl/->JSValue coll) + (first coll)))) (defmacro $ "Create a new React element from a valid React type. @@ -44,19 +50,29 @@ (:native (meta type))) type (if (keyword? type) (name type) - type)] - (cond - (map? (first args)) - `^js/React.Element (.createElement - (get-react) - ~type - ~(if native? - `(impl.props/dom-props ~(first args)) - `(impl.props/props ~(first args))) - ~@(rest args)) - - :else `^js/React.Element (.createElement (get-react) ~type nil ~@args)))) - + type) + has-props? (or (map? (first args)) + (nil? (first args))) + children (if has-props? + (rest args) + args) + props (if (map? (first args)) + (if native? + `(impl.props/dom-props ~(first args) ~(jsx-children children)) + `(impl.props/props ~(first args) ~(jsx-children children))) + (tl/->JSValue (cond-> {} + (not-empty children) + (assoc :children (jsx-children children))))) + has-key? (when has-props? + (contains? (first args) :key)) + the-key (when has-key? + (:key (first args))) + emit-fn (if (next children) + `jsxs + `jsx)] + (if has-key? + `^js/React.Element (~emit-fn ~type ~props ~the-key) + `^js/React.Element (~emit-fn ~type ~props)))) (defmacro <> "Creates a new React Fragment Element" diff --git a/src/helix/core.cljs b/src/helix/core.cljs index 7fd23c0..07b7ade 100644 --- a/src/helix/core.cljs +++ b/src/helix/core.cljs @@ -4,7 +4,8 @@ [helix.impl.props :as impl.props] [helix.impl.classes :as helix.class] [cljs-bean.core :as bean] - ["react" :as react]) + ["react" :as react] + ["react/jsx-runtime" :as jsx-runtime]) (:require-macros [helix.core])) @@ -37,6 +38,8 @@ ;; a dynamic arity dispatch. See https://github.com/Lokeh/helix/issues/20 (defn ^js/React get-react [] react) +(def jsx jsx-runtime/jsx) +(def jsxs jsx-runtime/jsxs) (defn $ "Create a new React element from a valid React type. @@ -55,20 +58,30 @@ native? (or (keyword? type) (string? type) (:native (meta type))) + has-props? ^boolean (or (map? ?p) + (nil? ?p)) + children* ^seq (if has-props? + ?c + args) + children (if (next children*) + (into-array children*) + (first children*)) + props* (cond-> {} + has-props? (conj ?p) + (some? children) (assoc :children children)) + props (if native? + (impl.props/-dom-props props*) + (impl.props/-props props*)) + key (:key props*) + emit-fn (if (next children*) + jsxs + jsx) type' (if (keyword? type) (name type) type)] - (if (map? ?p) - (apply create-element - type' - (if native? - (impl.props/-dom-props ?p) - (impl.props/-props ?p)) - ?c) - (apply create-element - type' - nil - args)))) + (if (some? key) + (emit-fn type' props key) + (emit-fn type' props)))) (def ^:deprecated $$ diff --git a/src/helix/dom.cljc b/src/helix/dom.cljc index ff564af..0ace6b4 100644 --- a/src/helix/dom.cljc +++ b/src/helix/dom.cljc @@ -1,6 +1,7 @@ (ns helix.dom (:refer-clojure :exclude [map meta time]) (:require + [cljs.tagged-literals :as tl] [helix.core :as hx] [helix.impl.props :as impl.props]) #?(:cljs (:require-macros [helix.dom]))) @@ -161,18 +162,23 @@ Use the special & or :& prop to merge dynamic props in." [type & args] - (if (map? (first args)) - `^js/React.Element (.createElement - (hx/get-react) - ~type - (impl.props/dom-props ~(first args)) - ~@(rest args)) - `^js/React.Element (.createElement - (hx/get-react) - ~type - nil - ~@args))) - + (let [?p (first args) + has-props? (map? ?p) + children* (if has-props? + (rest args) + args) + multiple-children (next children*) + children (if multiple-children + (tl/->JSValue children*) + (first children*)) + props* (when has-props? ?p) + key (:key props*) + emit-fn (if multiple-children + `hx/jsxs + `hx/jsx)] + (if (some? key) + `^js/React.Element (~emit-fn ~type (impl.props/dom-props ~props* ~children) ~key) + `^js/React.Element (~emit-fn ~type (impl.props/dom-props ~props* ~children))))) #?(:clj (defn gen-tag [tag] diff --git a/src/helix/impl/props.cljc b/src/helix/impl/props.cljc index 4366bf0..7666680 100644 --- a/src/helix/impl/props.cljc +++ b/src/helix/impl/props.cljc @@ -172,8 +172,11 @@ (-dom-props {:style ["fs1"]}) ) -(defmacro dom-props [m] - (-dom-props m)) +(defmacro dom-props + ([m] `(dom-props ~m nil)) + ([m c] (-dom-props (cond-> m + c (assoc :children c) + true (dissoc :key))))) (defn -props @@ -205,5 +208,8 @@ (-props {:foo-bar "baz"}) ) -(defmacro props [m] - (-props m)) +(defmacro props + ([m] `(props ~m nil)) + ([m c] (-props (cond-> m + (some? c) (assoc :children c) + true (dissoc :key))))) diff --git a/test/helix/core_test.cljs b/test/helix/core_test.cljs index e61fa6e..8626fc6 100644 --- a/test/helix/core_test.cljs +++ b/test/helix/core_test.cljs @@ -90,3 +90,60 @@ (t/testing "can be used with IReset" (reset! ref "well done") (t/is (= "well done" @ref)))))) + +(t/deftest jsx-test + (t/testing "jsx transform" + ;; In this test, you will see comments of the equivalent JSX. You can + ;; compare the output of the JSX transform by copy-pasting the provided JSX + ;; and running it through @babel/plugin-transform-react-jsx + (t/testing "with no props or children" + (let [component-1 (macroexpand '($ :div)) ;
+ component-2 (macroexpand '($ "div")) + component-3 (macroexpand '($ "div" nil)) + component-4 (macroexpand '($ "div" nil nil)) ;
{null}
+ expected-1 '(helix.core/jsx "div" {}) + expected-2 '(helix.core/jsx "div" {"children" nil})] + (t/are [x y] (= x (js->clj y)) + expected-1 component-1 + expected-1 component-2 + expected-1 component-3 + ;; This ensures we are matching the behavior of react/createElement, + ;; which is also reflected in the modern JSX transform. + expected-2 component-4))) + (t/testing "with no props and a single child" + (let [component-1 (macroexpand '($ :div "Hello")) ;
Hello
+ component-2 (macroexpand '($ "div" nil "Hello")) + component-3 (macroexpand '($ "div" "Hello")) + expected '(helix.core/jsx "div" {"children" "Hello"})] + (t/are [x] (= expected (js->clj x)) + component-1 + component-2 + component-3))) + (t/testing "with props and children" + (let [component (macroexpand + ;; + ;; + ;; + ;; + '($ Stack {:direction "row"} + ($ Item) + ($ Item))) + ;; Ideally, we'd check the full macroexpansion, but this suffices. + expected '(helix.core/jsxs Stack + (helix.impl.props/props {:direction "row"} + [($ Item) + ($ Item)]))] + (t/is (= expected (js->clj component))))) + (t/testing "provides keys as expected" + (let [props {:key "key"} + component (macroexpand + ;;
+ '($ :div {:key "kee" :& props})) + expected '(helix.core/jsx "div" + (helix.impl.props/dom-props {:key "kee" + :& props} + nil) + "kee") + element ($ :div {:key "kee" :& props})] + (t/is (= expected (js->clj component))) + (t/is (= "key" (gobj/get element "key"))))))) diff --git a/test/helix/impl/props_test.cljc b/test/helix/impl/props_test.cljc index edd1523..b358981 100644 --- a/test/helix/impl/props_test.cljc +++ b/test/helix/impl/props_test.cljc @@ -143,7 +143,18 @@ #js {:foo "bar"})) (t/is (eq (impl/dom-props {:style ["bar"]}) - #js {:style #js ["bar"]})))) + #js {:style #js ["bar"]})) + + (t/testing "children are added to props" + (t/is (eq (impl/dom-props nil "are the best") + #js {:children "are the best"})) + (t/is (eq (impl/dom-props {} "are the best") + #js {:children "are the best"})) + ;; Should we convert to JS??? + (t/is (eq (impl/dom-props nil ["one" "two"]) + #js {:children ["one" "two"]})) + (t/is (eq (impl/dom-props {} ["one" "two"]) + #js {:children ["one" "two"]}))))) #?(:cljs @@ -183,7 +194,18 @@ (t/is (eq (impl/props {:foo "bar" & nil}) - #js {:foo "bar"})))) + #js {:foo "bar"})) + + (t/testing "children are added to props" + (t/is (eq (impl/props nil "are the best") + #js {:children "are the best"})) + (t/is (eq (impl/props {} "are the best") + #js {:children "are the best"})) + ;; Should we convert to JS??? + (t/is (eq (impl/props nil ["one" "two"]) + #js {:children ["one" "two"]})) + (t/is (eq (impl/props {} ["one" "two"]) + #js {:children ["one" "two"]}))))) (t/deftest test-normalize-class