Created
April 4, 2017 23:07
-
-
Save domkm/09374eaa08d25b9f08300e11e0f76b54 to your computer and use it in GitHub Desktop.
This is an incredibly hacky ClojureScript wrapper for Relay 0. While we used this in production at one point, I would recommend against doing so. I'm only uploading it for posterity. Relay 1, which decouples the React wrapper from the GraphQL client, will enable us to write a good CLJS wrapper, as opposed to this abomination.
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 broadbrim.react-relay | |
(:require | |
[cljs.core :refer [specify! this-as js-arguments js-obj]] | |
[clojure.java.io :as io] | |
[clojure.string :as str] | |
[clojure.tools.macro :as macro] | |
[me.raynes.conch :as conch] | |
[potemkin] | |
[sablono.core :as sablono] | |
[taoensso.timbre :as log])) | |
(defn ^:private js-class [ctor-name super forms] | |
`(let [ctor# (fn ~(with-meta ctor-name {:jsdoc ["@constructor"]}) [~'& args#] | |
(this-as this# | |
(.apply (js/Object.getPrototypeOf ~super) | |
this# | |
(into-array (map cljs.core/clj->js args#))) | |
;; hack to initialize React component state | |
(when-let [f# (goog.object/get this# "get-initial-state")] | |
(->> this# (.call f#) to-js (goog.object/set this# "state"))) | |
this#)) | |
props# ~(reduce | |
(fn [props [name [this & args] & body :as form]] | |
(let [name (str name) | |
field? (str/starts-with? name "-") | |
name (if field? (subs name 1) name) | |
func `(fn ~(symbol name) ~(vec args) | |
(this-as ~this | |
~@body)) | |
desc (if field? | |
{:get func} | |
{:value func | |
:writable true}) | |
desc (merge desc {:configurable true :enumerable true}) | |
prop-type (if (-> form meta :static) :static :proto)] | |
(assoc-in props [prop-type name] desc))) | |
{} | |
forms) | |
static-props# (to-js (:static props# {})) | |
proto-props# (to-js (:proto props# {}))] | |
(inherit! ctor# ~super) | |
(js/Object.defineProperties ctor# static-props#) | |
(js/Object.defineProperties (.-prototype ctor#) proto-props#) | |
ctor#)) | |
(defmacro defclass | |
"TODO: Document" | |
{:style/indent [2 :form :form [1]]} | |
[ctor-name superclass & body] | |
(let [[ctor-name body] (macro/name-with-attributes ctor-name body)] | |
`(def ~(with-meta ctor-name {:jsdoc ["@constructor"] :export true}) | |
~(js-class ctor-name superclass body)))) | |
(potemkin/import-macro sablono/html dom) | |
(defn ^:private wrap-properties [props replace-map] | |
(map (fn [[name :as prop]] | |
(if-let [sym (get replace-map name)] | |
(with-meta (concat (butlast prop) | |
`[(~sym ~(last prop))]) | |
(meta prop)) | |
prop)) | |
props)) | |
(defmacro defcomponent | |
"TODO: Document" | |
{:style/indent [1 :form [1]]} | |
[ctor-name & body] | |
(let [[ctor-name body] (macro/name-with-attributes ctor-name body) | |
body (wrap-properties body {'render `dom}) | |
display-name (-> &env :ns :name (str "/" ctor-name)) | |
superclass `Component] | |
`(def ~(with-meta ctor-name {:jsdoc ["@constructor"] :export true}) | |
(let [class# ~(js-class ctor-name superclass body)] | |
(set! (.. class# -displayName) ~display-name) | |
(if (goog.object/get class# "fragments") | |
(js/Relay.createContainer | |
class# | |
(js-obj | |
"fragments" (to-js (goog.object/get class# "fragments")) | |
"initialVariables" (to-js (goog.object/get class# "initial-variables")) | |
"prepareVariables" (to-js (goog.object/get class# "prepare-variables")))) | |
class#))))) | |
(defmacro defmutation | |
"TODO: Document" | |
{:style/indent [1 nil [1]]} | |
[ctor-name & body] | |
(let [[ctor-name body] (macro/name-with-attributes ctor-name body) | |
body (wrap-properties body {'fragments `to-js}) | |
superclass `Mutation] | |
`(def ~(with-meta ctor-name {:jsdoc ["@constructor"] :export true}) | |
~(js-class ctor-name superclass body)))) | |
(def ^:private schema-path "./app/graph/relay_schema.json") | |
(def ^:private cache | |
(atom {:ql {} | |
:schema nil})) | |
(defn ^:private update-cache [cache script] | |
(let [schema (slurp schema-path) | |
cache (if (not= (:schema cache) schema) | |
{:ql {} :schema schema} | |
cache)] | |
(->> (conch/execute "node" "--print" script) | |
(or (get-in cache [:ql script])) | |
(assoc-in cache [:ql script])))) | |
(defn ^:private compile-ql! [script] | |
(-> cache | |
(swap! update-cache script) | |
(get-in [:ql script]))) | |
(defmacro ql [^String query] | |
(let [filename (-> &env :ns :name) | |
code (str "Relay.QL`" query "`") | |
script (str "var schema = require('" schema-path "');" | |
"var schemaData = schema.data;" | |
"var getBabelRelayPlugin = require('babel-relay-plugin');" | |
"var babel = require('babel-core');" | |
"var code = " (pr-str code) ";" | |
"var options = {};" | |
"var plugin = [getBabelRelayPlugin(schemaData), {enforceSchema: true}];" | |
"options.plugins = [plugin];" | |
"options.filename = '" filename "';" | |
"options.compact = true;" | |
"options.comments = false;" | |
"options.ast = false;" | |
"options.babelrc = false;" | |
"babel.transform(code, options).code;")] | |
(try | |
`(js/eval ~(compile-ql! script)) | |
(catch clojure.lang.ExceptionInfo e | |
(-> e ex-data :stderr Exception. throw))))) |
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 broadbrim.react-relay | |
(:require | |
[cljsjs.react] | |
[cljsjs.react.dom] | |
[cljsjs.react-relay] | |
[clojure.string :as str] | |
[goog.object :as gobj] | |
[sablono.core :as sablono]) | |
(:require-macros | |
[broadbrim.react-relay :as r])) | |
(->> #js {:credentials "same-origin"} ; send cookies | |
(new js/Relay.DefaultNetworkLayer "/graphql") | |
js/Relay.injectNetworkLayer) | |
(def ^:private no-op (constantly nil)) | |
(defn ^:private to-clj [x] | |
(js->clj x :keywordize-keys true)) | |
(def ^:private to-js clj->js) | |
(defn ^:private inherit! [subclass superclass] | |
(->> #js {"value" subclass | |
"enumerable" false | |
"writable" true | |
"configurable" true} | |
(js-obj "constructor") | |
(js/Object.create (.-prototype superclass)) | |
(set! (.-prototype subclass))) | |
(if (exists? js/Object.setPrototypeOf) | |
(js/Object.setPrototypeOf subclass superclass) | |
(set! (.-__proto__ subclass) superclass))) | |
(r/defclass ^:private Component js/React.Component | |
(componentWillMount [this] | |
(when-let [f (gobj/get this "will-mount")] | |
(.call f this))) | |
(componentDidMount [this] | |
(when-let [f (gobj/get this "did-mount")] | |
(.call f this))) | |
(componentWillReceiveProps [this next-props] | |
(when-let [f (gobj/get this "will-receive-props")] | |
(.call f this (to-clj next-props)))) | |
(componentWillUpdate [this next-props next-state] | |
(when-let [f (gobj/get this "will-update")] | |
(.call f this (to-clj next-props) (to-clj next-state)))) | |
(componentDidUpdate [this prev-props prev-state] | |
(when-let [f (gobj/get this "did-update")] | |
(.call f this (to-clj prev-props) (to-clj prev-state)))) | |
(componentWillUnmount [this] | |
(when-let [f (gobj/get this "will-unmount")] | |
(.call f this))) | |
(shouldComponentUpdate [this next-props next-state] | |
(if-let [f (gobj/get this "should-update?")] | |
(.call f this (to-clj next-props) (to-clj next-state)) | |
true)) | |
^:static (-propTypes [this] | |
(to-js (gobj/get this "prop-types"))) | |
^:static (-defaultProps [this] | |
(to-js (gobj/get this "default-props")))) | |
(r/defclass ^:private Mutation js/Relay.Mutation | |
(getConfigs [this] | |
(-> this (gobj/get "get-configs") (.call this) to-js)) | |
(getFatQuery [this] | |
(-> this (gobj/get "get-fat-query") (.call this) to-js)) | |
(getMutation [this] | |
(-> this (gobj/get "get-mutation") (.call this) to-js)) | |
(getVariables [this] | |
(-> this (gobj/get "get-variables") (.call this) to-js)) | |
(getCollisionKey [this] | |
(when-let [f (gobj/get this "get-collision-key")] | |
(-> f (.call this) to-js))) | |
(getFiles [this] | |
(when-let [f (gobj/get this "get-files")] | |
(-> f (.call this) to-js))) | |
(getOptimisticConfigs [this] | |
(when-let [f (gobj/get this "get-optimistic-configs")] | |
(-> f (.call this) to-js))) | |
(getOptimisticResponse [this] | |
(when-let [f (gobj/get this "get-optimistic-response")] | |
(-> f (.call this) to-js))) | |
^:static (-initialVariables [this] | |
(to-js (gobj/get this "initial-variables"))) | |
^:static (prepareVariables [this prev-vars route] | |
(when-let [f (gobj/get this "prepareVariables")] | |
(-> f (.call (to-clj prev-vars) (to-clj route)) to-js)))) | |
(doseq [obj [Component | |
(.-prototype Component) | |
Mutation | |
(.-prototype Mutation)]] | |
(specify! obj | |
ILookup | |
(-lookup | |
([obj k] | |
(-lookup obj k nil)) | |
([obj k not-found] | |
(assert (or (string? k) (keyword? k)) | |
(str "Object key must be a string or a keyword. Got: " k)) | |
(let [k (if (keyword? k) | |
(if-let [ns (namespace k)] | |
(str ns "/" (name k)) | |
(name k)) | |
k) | |
v (if (gobj/containsKey obj k) | |
(gobj/get obj k) | |
not-found)] | |
(if (fn? v) | |
(.bind v obj) | |
(to-clj v))))))) | |
(defn dom [x] | |
(r/dom x)) | |
(defn element [component & [props & children]] | |
(let [[props children] (if (or (nil? props) (map? props)) | |
[props children] | |
[nil (cons props children)])] | |
(apply js/React.createElement component (to-js props) children))) | |
(defn element? [x] | |
(js/React.isValidElement x)) | |
(defn set-state! [element state-or-func] | |
(if (fn? state-or-func) | |
(.setState element (fn [prev-state current-props] | |
(to-js | |
(state-or-func (to-clj prev-state) | |
(to-clj current-props))))) | |
(.setState element (to-js state-or-func)))) | |
(defn force-update! [element] | |
(.forceUpdate element)) | |
(defn set-variables! | |
([element variables] | |
((-> element :props :relay :setVariables) | |
(to-js variables))) | |
([element variables on-ready-state-change] | |
((-> element :props :relay :setVariables) | |
(to-js variables) | |
#(-> % to-clj on-ready-state-change)))) | |
(defn force-fetch! | |
([element] | |
((-> element :props :relay :forceFetch))) | |
([element variables] | |
((-> element :props :relay :forceFetch) | |
(to-js variables))) | |
([element variables on-ready-state-change] | |
((-> element :props :relay :forceFetch) | |
(to-js variables) | |
#(-> % to-clj on-ready-state-change)))) | |
(defn optimistic-update? [element prop] | |
((-> element :props :relay :hasOptimisticUpdate) (to-js prop))) | |
(defn pending-transactions [element prop] | |
(to-clj | |
((-> element :props :relay :getPendingTransactions) (to-js prop)))) | |
(defn container? [x] | |
(js/Relay.isContainer x)) | |
(defn apply-update | |
([mutation] | |
(apply-update mutation no-op)) | |
([mutation callback] | |
(.call (gobj/get js/Relay.Store "applyUpdate") | |
js/Relay.Store | |
mutation | |
(js-obj "onSuccess" (fn success [resp] | |
(callback nil (to-clj resp))) | |
"onFailure" (fn failure [tx] | |
(callback tx nil)))))) | |
(defn commit-update! | |
([mutation] | |
(commit-update! mutation no-op)) | |
([mutation callback] | |
(let [tx (apply-update mutation callback)] | |
(.call (gobj/get tx "commit") tx)))) | |
(defn root | |
([component route] | |
(root component route {})) | |
([component route opts] | |
(element | |
js/Relay.RootContainer | |
(reduce-kv | |
(fn [m k v] | |
(assoc m k (if (fn? v) | |
(let [wrap (if (str/starts-with? (name k) "render") | |
dom | |
identity)] | |
(fn [& args] | |
(->> args | |
(map to-clj) | |
(apply v) | |
wrap))) | |
v))) | |
{:Component component | |
:route (-> route | |
(update :name (fnil identity (str (.-displayName component) " Route"))) | |
(update :params (fnil identity {})))} | |
opts)))) | |
(defn mount | |
([react-element dom-element] | |
(js/ReactDOM.render react-element dom-element)) | |
([react-element dom-element callback] | |
(js/ReactDOM.render react-element dom-element callback))) | |
(defn unmount [dom-element] | |
(js/ReactDOM.unmountComponentAtNode dom-element)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment