Skip to content

Instantly share code, notes, and snippets.

@domkm
Created April 4, 2017 23:07
Show Gist options
  • Save domkm/09374eaa08d25b9f08300e11e0f76b54 to your computer and use it in GitHub Desktop.
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.
(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)))))
(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