Skip to content

Commit

Permalink
WIP on next-gen props translation
Browse files Browse the repository at this point in the history
  • Loading branch information
darwin committed Jan 31, 2020
1 parent cd7bdc9 commit 596648c
Show file tree
Hide file tree
Showing 10 changed files with 538 additions and 11 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
node_modules
/.shadow-cljs
/package-lock.json
/.cpcache
/.nrepl-port
14 changes: 11 additions & 3 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
{:paths ["src"]
:deps {ilk {:mvn/version "1.0.0"}
cljs-bean {:mvn/version "1.5.0"}}}
{:paths ["src" "dev"]
:deps {ilk {:mvn/version "1.0.0"}
devcards {:mvn/version "0.2.5"}
cljs-bean {:git/url "https://github.com/mfikes/cljs-bean.git"
:sha "3ccb2cf75f0e0a28e2a1af904075c87bfdea78fb"}
pjstadig/humane-test-output {:mvn/version "0.9.0"}

thheller/shadow-cljs {:mvn/version "RELEASE"}
binaryage/devtools {:mvn/version "RELEASE"}

}}
57 changes: 57 additions & 0 deletions dev/props2.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
(ns props2
(:require [clojure.test :refer [deftest is are testing run-tests]]
[helix.impl.props2 :refer [translate-props props-bean]]
[goog.object :as gobj]))

(enable-console-print!)

(defn serialize [v]
(.stringify js/JSON v))

(defn xf [s]
(str ">" s "-postfix<"))

(deftest test-translate-props
(testing "basic static props translation"
(are [props res] (= (serialize res) (serialize (translate-props props)))
; basic static cases
{:name "v"} #js {:name "v"}
{:kebab-name "v"} #js {"kebabName" "v"}
{:class "v"} #js {"className" "v"}
{:for "v"} #js {"htmlFor" "v"}
{:style "v"} #js {"style" "v"}
{:style {:background-color "red"}} #js {"style" #js {"backgroundColor" "red"}}

; basic dynamic cases
{(xf "name") "v"} #js {">namePostfix<" "v"}
{(xf "kebab-name") "v"} #js {">kebabNamePostfix<" "v"}

)))

(deftest test-props-bean
(testing "basic reverse props translation"
(are [props res] (= res (props-bean props))
#js {"someValueAndSomething" "v"} {:some-value-and-something "v"}
#js {"SomeValue" "v"} {:Some-value "v"}
#js {"className" "v"} {:class "v"}
#js {"htmlFor" "v"} {:for "v"}
#js {"aria-some-thing" "v"} {:aria-some-thing "v"}
#js {"data-some-thing" "v"} {:data-some-thing "v"}
#js {"style" #js {"backgroundColor" "red"}} {:style {:background-color "red"}}))
(testing "props-bean should not be recursive except for style"
(let [nested-val #js {"nestedValue1" #js {"nestedValue2" "v"}}]
(are [props res] (= res (props-bean props))
#js {"value" nested-val} {:value nested-val}
#js {"style" nested-val} {:style {:nested-value1 {:nested-value2 "v"}}}))))

(run-tests)

;(js-debugger)
;(js/console.log (str (macroexpand '(translate-props {(xf "name") "value"}))))
;(js/console.log (translate-props {(xf "name-x-y-z-last") "value"}))

#_(js/console.log (props-bean #js {"className" "c"
"htmlFor" "h"
"aria-some-thing" "a"
"data-some-thing" "d"
"style" #js {"backgroundColor" "red"}}))
12 changes: 12 additions & 0 deletions public/dev/props2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="UTF-8">
<link href="style.css" rel="stylesheet" type="text/css">
</head>
<body>
<script src="js/shared.js" ></script>
<script src="js/props2.js"></script>
</body>
</html>
15 changes: 9 additions & 6 deletions shadow-cljs.edn
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

:nrepl {:port 8777}

:dependencies
[[binaryage/devtools "0.9.7"]
[devcards "0.2.5"]
[ilk "1.0.0"]
[cljs-bean "1.5.0"]]
:deps true
;:dependencies
;[[binaryage/devtools "0.9.7"]
; [devcards "0.2.5"]
; [ilk "1.0.0"]
; [cljs-bean "1.5.0"]]

:builds
{:dev {:target :browser
Expand All @@ -17,6 +18,8 @@
:modules {:shared {}
:main {:entries [workshop]
:depends-on #{:shared}}
:props2 {:entries [props2]
:depends-on #{:shared}}
:refresh {:entries [refresh-example]
:depends-on #{:shared}}}
:compiler-options {:devcards true}
Expand Down Expand Up @@ -46,4 +49,4 @@
;; :modules {:main {:entries [hx.benchmark]}}
;; :devtools {:http-root "public/benchmark"
;; :http-port 8800}}
}}}
}}
3 changes: 2 additions & 1 deletion src/helix/core.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
(:refer-clojure :exclude [type])
(:require [goog.object :as gobj]
[helix.impl.props :as impl.props]
[helix.impl.props2 :as impl.props2]
["./impl/class.js" :as helix.class]
[cljs-bean.core :as bean]
["react" :as react])
Expand Down Expand Up @@ -94,7 +95,7 @@

(defn- extract-cljs-props
[o]
(bean/bean o))
(impl.props2/props-bean o))



Expand Down
6 changes: 5 additions & 1 deletion src/helix/impl/props.cljc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns helix.impl.props
(:require [clojure.string :as string]
#?(:clj [helix.impl.props2])
#?@(:cljs [[cljs-bean.core :as b]
[goog.object :as gobj]]))
#?(:cljs (:require-macros [helix.impl.props])))
Expand Down Expand Up @@ -96,8 +97,11 @@
(-native-props {:asdf "jkl" :style 'foo})
)

;(defmacro native-props [m]
; (-native-props m))

(defmacro native-props [m]
(-native-props m))
`(helix.impl.props2/translate-props ~m))


(defn -props
Expand Down
208 changes: 208 additions & 0 deletions src/helix/impl/props2.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
(ns helix.impl.props2
(:require [clojure.string :as string]
[clojure.pprint :refer [pprint]])
(:import (clojure.lang Named)
(cljs.tagged_literals JSValue)
(java.util.regex Pattern)))
;
; see motivation https://github.com/Lokeh/helix/issues/9
;
; translating props to js land:
; * we should strive to do as much work at compile-time
;
; some conventions:
; * fn prefixed with "gen-" are code generating
; * macros should be minimal wrappers around gen-functions
; * fallback to dynamic cases should be performed by calling "dynamic-" counterparts
; * to prevent code bloat, do not generate unnecessary dynamic code, implement dynamic- helpers in cljs ns instead
;

; -- constants --------------------------------------------------------------------------------------------------------------

(def ^:const NO_TRANSLATION 0)
(def ^:const STYLE_TRANSLATION 1)

(def kebab-to-camel-re #"-(\w)")
(def special-case-prop-name-re #"^(aria-|data-|class$|for$|style$).*")

; TODO: this is a workaround to generate stable code
; FIXME: this must be resolved somehow
;
; from #clojurescript slack
;darwin: I have a macro which is processing a map, I’m generating cljs code based on visited keys and values.
; My issue is that I need to generate the code in the same order as individual keys/vals appeared in the source code.
; Naively iterating over a map value in macro seem to not preserve the order. Any ideas?
; btw. I’m using reduce over the map’s [k v] (edited)
;alexmiller: maps are not ordered
; so this isn't going to work with a map as is
;darwin: yep, looks like I’m toasted
;alexmiller: if you look at something like let , the use of a vector for bindings enforces a semantic order (edited)
;darwin: thanks for the suggestion, this is part of an effort to translate react props passed as cljs map to js conventions
; (at compile time), not sure if this a-vector-instead-of-a-map proposal would be palatable in this case (edited)
;
(defn force-as-sorted-map [m]
(let [x (into (sorted-map-by #(compare (str %1) (str %2))) m)]
x))

; -- helpers ----------------------------------------------------------------------------------------------------------------

(defn kebab-to-camel [name]
(assert name)
(assert (> (count name) 0))
(if (= \' (.charAt name 0))
(subs name 1)
(string/replace name kebab-to-camel-re #(string/upper-case (second %)))))

(defn coerce-key-to-string [k]
(cond
(symbol? k) nil
(instance? Named k) (name k)
; TODO: decide if we want string keys to be excluded from translation, then they should be quoted here
(string? k) k))

(defn cljs-primitive? [v]
(or (nil? v)
(boolean? v)
(string? v)
(number? v)
(float? v)
(keyword? v)
(instance? Pattern v)
; TODO: review this and maybe add other cases of primitive values shared between clj and cljs
))

; -- style translation ------------------------------------------------------------------------------------------------------

(declare gen-translate-style)

(defn gen-translate-style-key [k]
(if-some [name (coerce-key-to-string k)]
(kebab-to-camel name)
`(dynamic-translate-style-key ~k)))

(defn gen-translate-style-value [v]
(gen-translate-style v))

(defn gen-translate-style-kv [[k v]]
[(gen-translate-style-key k) (gen-translate-style-value v)])

(defn gen-translate-style [v]
(cond
(map? v) `(~'js-obj ~@(mapcat gen-translate-style-kv (force-as-sorted-map v)))
(vector? v) `(~'array ~@(map gen-translate-style v))
(cljs-primitive? v) v
:else `(dynamic-translate-style ~v)))

; -- props translation ------------------------------------------------------------------------------------------------------

(defn key->translation-mode [k]
(if (= (coerce-key-to-string k) "style")
STYLE_TRANSLATION
NO_TRANSLATION))

(defn gen-translate-prop-value [k v]
(assert (= 0 NO_TRANSLATION))
(assert (= 1 STYLE_TRANSLATION))
(case (key->translation-mode k)
0 v
1 (gen-translate-style v)))

(defn gen-translate-prop-key-statically [k]
(if-some [name (coerce-key-to-string k)]
; static case
(if-some [match (re-matches special-case-prop-name-re name)]
(case (second match)
("aria-" "data-") name
"class" "className"
"for" "htmlFor"
"style" "style")
(kebab-to-camel name))
; defer dynamic case codegen
))


(defn prop-translation-reducer [[bindings members] [k v]]
(if-some [translated-static-k (gen-translate-prop-key-statically k)]
; static case
(let [translated-v (gen-translate-prop-value k v)]
[bindings (conj members translated-static-k translated-v)])
; dynamic case
; - note that dynamic key forces use to take dynamic code path for value translation as well
; because key value is determining translation mode (and that will be known during runtime)
(let [k-sym (gensym "key-")]
; we need to hoist the key to prevent double evaluation
[(conj bindings k-sym k) (conj members
`(dynamic-translate-prop-key ~k-sym)
`(dynamic-translate-prop-value ~k-sym ~v))])))

(defn gen-translate-clean-props-map [m]
(assert (map? m))
(let [[bindings kv-members] (reduce prop-translation-reducer [[] []] (force-as-sorted-map m))
js-map `(~'js-obj ~@kv-members)]
(if (seq bindings)
`(let [~@bindings]
~js-map)
js-map)))

(defn gen-translate-props-map [m]
(if-some [spread-sym (m '&)]
(let [clean-m (dissoc m '&)]
`(js/Object.assign ~(gen-translate-clean-props-map clean-m) (dynamic-translate-props ~spread-sym)))
(gen-translate-clean-props-map m)))

(defn gen-translate-props [v]
(if (map? v)
(gen-translate-props-map v)
`(dynamic-translate-props ~v)))

; -- macros -----------------------------------------------------------------------------------------------------------------

(defmacro translate-props [v]
(gen-translate-props v))

; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

(comment

(gen-translate-prop-key "aria-something")
(gen-translate-prop-key "data-something")
(gen-translate-prop-key "data1-something")
(gen-translate-prop-key "class")
(gen-translate-prop-key "for")
(gen-translate-prop-key "class1")
(gen-translate-prop-key "forx")
(gen-translate-prop-key "kebab-name-last")
(gen-translate-prop-key "'kebab-name-last")

(pprint (macroexpand '(translate-props {:name "value"})))

(pprint (macroexpand '(translate-props {:name "value"
:kebab-name "kebab-value"
:regex-value #"a regex"
:complex-value {:should-not-camelize {:should-not-camelize 2}}
:js-map (JSValue. {:js "map"})
:js-vector (JSValue. ["js" "vector"])
symbol-name "symbol-val"
(str "some" "code") "symbol-from-code"
"string-name" "string-name"
:class "class-value"
:for "for-value"
:style {:color "color-value"
:nesting-should-work {:background-color "val-from-nesting"}
(str "style" "code") {:background-color "val-from-style-code"}
:background-color (str "red")}
}

)))

(pprint (macroexpand '(translate-props {:style {:background-color "value"
:nested-map {:nested-key "nested-val"}}})))

(pprint (macroexpand '(translate-props (merge spring {:key index
:castShadow true
:receiveShadow true}))))

)



Loading

0 comments on commit 596648c

Please sign in to comment.