Created
April 10, 2017 07:14
-
-
Save gmp26/c7319698bde377e1dc145876c9fc1662 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns rum.core | |
(:refer-clojure :exclude [ref]) | |
(:require-macros rum.core) | |
(:require | |
[cljsjs.react] | |
[cljsjs.react.dom] | |
[goog.object :as gobj] | |
[goog.functions :as gf] | |
[goog.array :as garr] | |
[rum.cursor :as cursor]) | |
(:import | |
(goog.structs Set))) | |
(defn call-2-1 | |
"Calls fn(state)" | |
[state fn] | |
;; Avoid CLJS's arity check generation, these are always just arity 1: | |
(js* "~{}(~{})" fn state)) | |
(defn call-2-1-static | |
"Returns a function that calls fn(state,arg)" | |
[arg] | |
(fn [state fn] | |
;; Again avoid arity check by CLJS: The :init methods are always 2 arity so the check | |
;; would be for nothing: | |
(js* "~{}(~{},~{})" fn state arg))) | |
(defn call-2-static-1 | |
"Calls fn(static,state)" | |
[static] | |
(fn [state fn] | |
;; Again avoid arity check by CLJS | |
(js* "~{}(~{},~{})" fn static state))) | |
(defn call-all | |
"We can leave this overloaded since vreset is a macro and will properly dispatch to the right | |
arity. So it's fast." | |
([state fns] | |
(reduce call-2-1 state fns)) | |
([state fns arg] | |
(reduce (call-2-1-static arg) state fns))) | |
(defn state | |
"Given React component, returns Rum state associated with it" | |
[comp] | |
(aget (.-state comp) ":rum/state")) | |
(defn gen-lifecycle-method | |
[fns] | |
(fn [] | |
(this-as this | |
(vswap! (state this) call-all fns)))) | |
(defn possibly-set-lifecycle! | |
"This is all done for performance... Smaller and more used functions can easier get optimized." | |
[spec name fns] | |
(when-not (empty? fns) | |
(gobj/set spec name (gen-lifecycle-method fns))) | |
nil) | |
(defn gen-render-fn | |
[wrapped-render] | |
(fn [] | |
(this-as this | |
(let [rum-state (state this) | |
;; Again we wrapped render is never a multi arity fn: | |
[dom next-state] (js* "~{}(~{})" wrapped-render @rum-state)] | |
(vreset! rum-state next-state) | |
dom)))) | |
(defn gen-will-receive-props-fn | |
[did-remount] | |
(fn [next-props] | |
(this-as this | |
(let [old-state @(state this) | |
state (merge old-state | |
(aget next-props ":rum/initial-state")) | |
next-state (reduce (call-2-static-1 old-state) state did-remount)] | |
;; allocate new volatile so that we can access both old and new states | |
;; in shouldComponentUpdate | |
(.setState this #js {":rum/state" (volatile! next-state)}))))) | |
(defn gen-will-component-update-fn | |
[will-update] | |
(fn [next-props next-state] | |
(this-as this | |
(let [new-state (aget next-state ":rum/state")] | |
(vswap! new-state call-all will-update))))) | |
(defn gen-should-component-update-fn | |
[should-update] | |
(fn [next-props next-state] | |
(this-as this | |
(let [old-state @(state this) | |
new-state @(aget next-state ":rum/state")] | |
(or (some #(% old-state new-state) should-update) false))))) | |
(defn gen-child-context-fn | |
"Not optimized since I dont use it..." | |
[child-context] | |
(fn [] | |
(this-as this | |
(let [state @(state this)] | |
(clj->js (transduce (map (gf/partialRight state)) merge {} child-context)))))) | |
(defn group-by-map-keys-js | |
"Given a (CLJS) vector of (CLJ) maps generates a (JS) object of | |
(JS) arrays. Where the keys are the keys of the given clj-maps and the values | |
the array of map values." | |
[mixins] | |
(reduce | |
(fn [obj lcm] | |
(assert (map? lcm) "Mixins needs to be a vector of maps.") | |
(reduce-kv | |
(fn [obj k f] | |
(assert (keyword? k) "Mixin key needs to be a keyword.") | |
(assert (ifn? k) "Mixins must be functions.") | |
(let [k (.-name k) | |
fs (gobj/get obj k #js[])] | |
(gobj/setIfUndefined obj k fs) | |
(.push fs f) | |
obj)) | |
obj | |
lcm)) | |
#js{} | |
mixins)) | |
(defn ajoin | |
"Joins two JS arrays. Does not mutate any of the two. Works fine with either being nil!" | |
[a0 a1] | |
(let [a0 (aclone (or a0 #js[]))] | |
(run! (js/Array.prototype.push.bind a0) a1) | |
(not-empty a0))) | |
;; From google closure compiler: | |
(def | |
^{:jsdoc | |
["@param {!Function} childCtor child class" | |
"@param {!Function} parentCtor Parent class"] | |
:doc "See closure compiler source."} | |
inherits | |
(js* "function(childCtor, parentCtor) { | |
/** @constructor */ | |
function tempCtor() {} | |
tempCtor.prototype = parentCtor.prototype; | |
childCtor.prototype = new tempCtor(); | |
/** @override */ | |
childCtor.prototype.constructor = childCtor; | |
// The following is pure optimization and not necessarily needed: | |
for (var p in parentCtor) { | |
if (Object.defineProperties) { | |
var descriptor = Object.getOwnPropertyDescriptor(parentCtor, p); | |
if (descriptor) { | |
Object.defineProperty(childCtor, p, descriptor); | |
} | |
} else { | |
// Pre-ES5 browser. Just copy with an assignment. | |
childCtor[p] = parentCtor[p]; | |
} | |
} | |
};")) | |
(defn build-class | |
[render mixins display-name] | |
(let [lcm (group-by-map-keys-js mixins) | |
;; We're using aget with strings here since the keywords (:will-mount etc) | |
;; will stay the same strings even after advanced compilation. | |
init (aget lcm "init") ;; state props -> state | |
constr (fn [props] | |
(this-as this | |
;; Call parent constructor: | |
(.call js/React.Component this props) | |
(set! (.-props this) props) | |
;; We don't have to do setState here! | |
(set! (.-state this) | |
#js{":rum/state" (volatile! | |
(-> (aget props ":rum/initial-state") | |
(assoc :rum/react-component this) | |
(call-all init props)))}) | |
this)) | |
wrap-render (aget lcm "wrap-render") | |
wrapped-render (if (empty? wrap-render) | |
render | |
(reduce call-2-1 render wrap-render)) | |
did-remount (aget lcm "did-remount") ;; old-state state -> state | |
should-update (aget lcm "should-update") ;; old-state state -> boolean | |
will-unmount (aget lcm "will-unmount") ;; state -> state | |
child-context (aget lcm "child-context") ;; state -> child-context | |
before-render (aget lcm "before-render") ;; state -> state | |
after-render (aget lcm "after-render") ;; state -> state | |
will-mount (ajoin (aget lcm "will-mount") before-render) ;; state -> state | |
will-update (ajoin (aget lcm "will-update") before-render) ;; state -> state | |
did-update (ajoin (aget lcm "did-update") after-render) ;; state -> state | |
did-mount (ajoin (aget lcm "did-mount") after-render) ;; state -> state | |
class-props (aget lcm "class-properties")] ;; custom properties+methods | |
(inherits constr js/React.Component) | |
;; Displayname gets set on the constructor itself: | |
(set! (.-displayName constr) display-name) | |
(let [proto (.-prototype constr)] | |
(gobj/extend proto #js{:componentWillReceiveProps (gen-will-receive-props-fn did-remount) | |
:render (gen-render-fn wrapped-render)}) | |
(possibly-set-lifecycle! proto "componentWillMount" will-mount) | |
(possibly-set-lifecycle! proto "componentDidMount" did-mount) | |
(possibly-set-lifecycle! proto "componentDidUpdate" did-update) | |
(possibly-set-lifecycle! proto "componentWillUnmount" will-unmount) | |
(when-not (empty? will-update) | |
(gobj/set proto "componentWillUpdate" (gen-will-component-update-fn will-update))) | |
(when-not (empty? should-update) | |
(gen-should-component-update-fn should-update)) | |
(when-not (empty? child-context) | |
(gobj/set proto "getChildContext" (gen-child-context-fn child-context))) | |
(when (some? class-props) | |
(when-some [cp (clj->js (apply merge class-props))] | |
(gobj/extend proto cp))) | |
constr))) | |
;; [.........] | |
(def | |
^{:private true | |
:jsdoc ["@type {goog.structs.Set}"]} | |
render-queue (Set.)) | |
(defn force-if-mounted | |
[comp] | |
;; React 15.5 doesn't expose isMounted anymore, but they just map it to updater.isMounted. | |
;; Let's see how long this goes well :) | |
(when ^boolean (.isMounted (.-updater comp) comp) | |
(.forceUpdate comp)) | |
nil) | |
(defn- render-all [queue] | |
(garr/forEach queue force-if-mounted)) | |
(defn- render [] | |
;; getValues copies so we're safe from mutation: | |
(let [rq (.getValues render-queue)] | |
(.clear render-queue) | |
(batch render-all rq))) | |
(defn request-render | |
"Schedules react component to be rendered on next animation frame" | |
[component] | |
(when (.isEmpty render-queue) | |
(rAF render)) | |
(.add render-queue component)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment