Skip to content

Instantly share code, notes, and snippets.

@gmp26
Created April 10, 2017 07:14
Show Gist options
  • Save gmp26/c7319698bde377e1dc145876c9fc1662 to your computer and use it in GitHub Desktop.
Save gmp26/c7319698bde377e1dc145876c9fc1662 to your computer and use it in GitHub Desktop.
(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