Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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