diff --git a/examples/react-context/src/example/core.cljs b/examples/react-context/src/example/core.cljs index 2a26a639..ddfcadda 100644 --- a/examples/react-context/src/example/core.cljs +++ b/examples/react-context/src/example/core.cljs @@ -1,11 +1,11 @@ (ns example.core - (:require [reagent.core :as r] + (:require [reagent.context :as r.context] + [reagent.core :as r] [reagent.dom :as rdom] - [react :as react] [goog.object :as gobj])) -(defonce my-context (react/createContext "default")) +(defonce my-context (r.context/make-context "my-context" "default")) (def Provider (.-Provider my-context)) (def Consumer (.-Consumer my-context)) @@ -16,6 +16,18 @@ (r/as-element [:div "Context: " (pr-str v)]))]) (defn root [] + ;; When using the pure Clojure wrappers, the value is passed as is. + [:div + [r.context/provider {:value {:foo :bar} ;; The value here can be anything, does not need to be a map + :context my-context} + [r.context/consumer {:context my-context} + (fn [{:keys [foo]}] + [:div ":foo is a keyword: " (pr-str foo)])] + + ;; The `with-context` macro cuts away some boilerplate from the above + (r.context/with-context [{:keys [foo]} my-context] + [:div "From the macro: " (pr-str foo)])]] + ;; Provider takes props with single property, value ;; :< or adapt-react-class converts the Cljs properties ;; map to JS object for the Provider component. diff --git a/src/reagent/context.clj b/src/reagent/context.clj new file mode 100644 index 00000000..479853f8 --- /dev/null +++ b/src/reagent/context.clj @@ -0,0 +1,17 @@ +(ns reagent.context) + +(defmacro with-context + "Defines a context consumer component. + First argument should be a binding as in when-let, with the left value being a context identifier. + Remaining arguments will be the render function of the component + + (reagent.context/with-context [{:keys [foo bar]} react-context-id] + [:div ...])" + [bindings & body] + (assert (and (vector? bindings) + (= 2 (count bindings))) + "First argument must be a binding vector with 2 elements") + (let [[left right] bindings] + `[consumer {:context ~right} + (fn [~left] + ~@body)])) diff --git a/src/reagent/context.cljs b/src/reagent/context.cljs new file mode 100644 index 00000000..23669f53 --- /dev/null +++ b/src/reagent/context.cljs @@ -0,0 +1,42 @@ +(ns reagent.context + (:require-macros [reagent.core] + [reagent.context]) + (:require [react :as react] + [reagent.core :as r])) + +(defn make-context + "Creates a context with the given name and optional default value. + The default value will be used when trying to retrieve the context value with no provider present. + Attempting to retrieve a context value that has no provider, and no default value will result in a crash." + ([name] + (let [context (react/createContext)] + (set! (.-displayName context) name) + context)) + ([name default-value] + (let [context (react/createContext default-value)] + (set! (.-displayName context) name) + context))) + +(defn provider + "Provides the value for the given context to descendant components." + [{:keys [context value]} & contents] + (into [:r> (.-Provider context) #js{:value value}] + contents)) + +(defn consumer + "Retrieves the value for the given context. + render-f must be a reagent render function that will be called with the value. + If there's no provider, will return the default value if it is set, or throw otherwise." + [{:keys [context]} render-f] + ;; Use with-let to maintain a stable render function, otherwise the child will + ;; remount every time a new prop comes into the parent. + (r/with-let [wrapper-comp + ;; Passes through context to component using meta data. See: + ;; https://github.com/reagent-project/reagent/blob/ce80585e9aebe0a6df09bda1530773aa512f6103/doc/ReactFeatures.md#context + ^{:context-type context} + (fn [render-f] + (let [value (.-context (r/current-component))] + (when (undefined? value) + (throw (js/Error. (str "Missing provider for " (.-displayName context))))) + (render-f value)))] + [wrapper-comp render-f]))