diff --git a/pom.xml b/pom.xml index ac81bb58..12747a82 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.fulcrologic fulcro jar - 3.6.11-SNAPSHOT + 3.7.0-SNAPSHOT UTF-8 1.8 diff --git a/src/main/com/fulcrologic/fulcro/components.cljc b/src/main/com/fulcrologic/fulcro/components.cljc index 370971a4..40702417 100644 --- a/src/main/com/fulcrologic/fulcro/components.cljc +++ b/src/main/com/fulcrologic/fulcro/components.cljc @@ -15,6 +15,7 @@ [com.fulcrologic.fulcro.algorithms.do-not-use :as util] [com.fulcrologic.fulcro.algorithms.denormalize :as fdn] [com.fulcrologic.fulcro.algorithms.lookup :as ah] + [com.fulcrologic.fulcro.mutations :as m] [com.fulcrologic.fulcro.raw.components :as rc] [com.fulcrologic.guardrails.core :refer [>def]] [clojure.set :as set]) diff --git a/src/main/com/fulcrologic/fulcro/raw/components.cljc b/src/main/com/fulcrologic/fulcro/raw/components.cljc index 17e4d135..afa1e855 100644 --- a/src/main/com/fulcrologic/fulcro/raw/components.cljc +++ b/src/main/com/fulcrologic/fulcro/raw/components.cljc @@ -21,6 +21,12 @@ (defonce ^:private component-registry (atom {})) +(defn legal-registry-lookup-key? + "Returns true if `k` is a legal thing to with `registry-key->class`. The registry contains keywords, but that helper + function accepts (and converts) strings or symbols as well." + [k] + (or (qualified-keyword? k) (qualified-symbol? k) (and (string? k) (str/includes? k "/")))) + ;; Used internally by get-query for resolving dynamic queries (was created to prevent the need for external API change in 3.x) (def ^:dynamic *query-state* nil) @@ -124,9 +130,12 @@ "Look up the given component in Fulcro's global component registry. Will only be able to find components that have been (transitively) required by your application. - `classname` can be a fully-qualified keyword or symbol." + `classname` can be a fully-qualified keyword, string, symbol, or component class. In the latter + case this function just acts as `identity`. This allows this function to act as a coercion + that ensures you have a class." [classname] (cond + (component-class? classname) classname (keyword? classname) (get @component-registry classname) (symbol? classname) (let [k (keyword (namespace classname) (name classname))] (get @component-registry k)) @@ -703,9 +712,9 @@ "props" {"fulcro$queryid" :anonymous}}) {:query-id :anonymous}) (not ident) (assoc :ident - (fn [this props] - (when-let [k (union-keys props)] - [k (get props k)]))) + (fn [this props] + (when-let [k (union-keys props)] + [k (get props k)]))) componentName (assoc :componentName componentName)))] (assoc original-node :component component)) (let [real-id-key (ast-id-key children) @@ -956,21 +965,3 @@ o# (cond-> o# ident# (assoc :ident ident#))] (def ~sym (nc ~query o#)))))) - -(comment - (def Person (entity->component - {:person/id 1 - :ui/checked? true - :person/name "Bob" - :person/addresses [{:ui/autocomplete "" - :address/id 11 - :address/street "111 Main St"} - {:ui/autocomplete "" - :address/id 12 - :address/street "222 Main St"}]} - {:componentName ::MyThing})) - - (def Address (get-subquery-component Person [:person/addresses])) - - (get-ident Address {:address/id 99}) - ) diff --git a/src/main/com/fulcrologic/fulcro/routing/dynamic_routing.cljc b/src/main/com/fulcrologic/fulcro/routing/dynamic_routing.cljc index 272a4c56..25c7a7b3 100644 --- a/src/main/com/fulcrologic/fulcro/routing/dynamic_routing.cljc +++ b/src/main/com/fulcrologic/fulcro/routing/dynamic_routing.cljc @@ -10,19 +10,23 @@ be easy to integrate with HTML5 history and URL control." #?(:cljs (:require-macros [com.fulcrologic.fulcro.routing.dynamic-routing])) (:require - #?(:cljs [goog.object :as gobj]) + #?@(:clj [[cljs.analyzer :as ana]] + :cljs [[cljs.loader :as loader] + [goog.object :as gobj]]) + [clojure.spec.alpha :as s] [clojure.zip :as zip] - [com.fulcrologic.guardrails.core :refer [>fdef => ?]] - [com.fulcrologic.fulcro.ui-state-machines :as uism :refer [defstatemachine]] - [com.fulcrologic.fulcro.components :as comp] - [com.fulcrologic.fulcro.raw.components :as rc] + [com.fulcrologic.fulcro.algorithms.indexing :as indexing] + [com.fulcrologic.fulcro.algorithms.lookup :as ah] + [com.fulcrologic.fulcro.algorithms.merge :as merge] [com.fulcrologic.fulcro.application :as app] + [com.fulcrologic.fulcro.components :as comp] [com.fulcrologic.fulcro.mutations :refer [defmutation]] + [com.fulcrologic.fulcro.raw.components :as rc] + [com.fulcrologic.fulcro.ui-state-machines :as uism :refer [defstatemachine]] + [com.fulcrologic.guardrails.core :refer [>fdef => ?]] [edn-query-language.core :as eql] - [taoensso.timbre :as log] - [clojure.spec.alpha :as s] - #?(:clj [cljs.analyzer :as ana]) - [com.fulcrologic.fulcro.algorithms.indexing :as indexing])) + [taoensso.encore :as enc] + [taoensso.timbre :as log])) (def ^:dynamic *target-class* "INTERNAL USE ONLY. Not guaranteed to be available at runtime in production builds. This is used to aid in giving @@ -120,9 +124,30 @@ (defn route-lifecycle? [component] (boolean (rc/component-options component :will-leave))) (defn get-targets - "Returns a set of classes to which this router routes." - [router] - (set (rc/component-options router :router-targets))) + "Returns a set of classes to which this router routes, including dynamic ones if possible. + + `router` - A router instance, class, or registry key, or ident. + `state-map` - The current app state + + If `router` is a class or registry key you'll get the static list + from component options unless you also supply the state-map (it will attempt to use rc/*query-state* if it is bound). " + ([router] + (let [sm (if (rc/component-instance? router) + (app/current-state router) + (or rc/*query-state* {}))] + (get-targets router sm))) + ([router state-map] + (enc/when-let [[router router-ident] (cond + (rc/component-class? router) [router (rc/get-ident router {})] + (rc/component-instance? router) [router (rc/get-ident router)] + (eql/ident? router) [(rc/registry-key->class (second router)) router] + (rc/legal-registry-lookup-key? router) (enc/when-let [cls (some-> router (rc/registry-key->class))] + [cls (rc/get-ident cls {})])) + static-router-targets (set (rc/component-options router :router-targets)) + router-targets (into static-router-targets + (keep rc/registry-key->class) + (get-in state-map (conj router-ident ::dynamic-router-targets)))] + router-targets))) (defn- ident-matches-expectation? [[expected-table maybe-expected-id] [table id]] ;; NOTE: If the `id` of the ident is hardcoded then maybe-expected-id will be set, @@ -264,7 +289,7 @@ (defn route-target "Given a router class and a path segment, returns the class of *that router's* target that accepts the given URI path, - which is a vector of (string) URI components. + which is a vector of (string) URI components. `state-map` is required if you want it to work with dynamic targets. Returns nil if there is no target that accepts the path, or a map containing: @@ -277,29 +302,32 @@ NOTE: If more than one target matches, then the target with the longest match will be returned. A warning will be printed if more than one match of equal length is found. " - [router-class path] - (when (and router-class (router? router-class)) - (let [targets (get-targets router-class) - matches (->> (reduce (fn [result target-class] - (let [prefix (and target-class (route-target? target-class) - (some-> target-class (route-segment) (matching-prefix path)))] - (if (and prefix (seq prefix)) - (conj result {:length (count prefix) - :matching-prefix prefix - :target target-class}) - result))) [] targets) - (sort-by :length) - reverse) - max-length (some-> matches first :length) - match (filter #(= max-length (:length %)) matches)] - (when (second match) - (log/warn "More than one route target matches" path "See https://book.fulcrologic.com/#warn-routing-multiple-target-matches")) - (first match)))) + ([router-class path] (route-target router-class path rc/*query-state*)) + ([router-class path state-map] + (when (and router-class (router? router-class)) + (let [targets (get-targets router-class state-map) + matches (->> (reduce (fn [result target-class] + (let [prefix (and target-class (route-target? target-class) + (some-> target-class (route-segment) (matching-prefix path)))] + (if (and prefix (seq prefix)) + (conj result {:length (count prefix) + :matching-prefix prefix + :target target-class}) + result))) [] targets) + (sort-by :length) + reverse) + max-length (some-> matches first :length) + match (filter #(= max-length (:length %)) matches)] + (when (second match) + (log/warn "More than one route target matches" path "See https://book.fulcrologic.com/#warn-routing-multiple-target-matches")) + (first match))))) (defn accepts-route? - "Returns true if the given component is a router that manages a route target that will accept the given path." - [component path] - (boolean (route-target component path))) + "Returns true if the given component is a router that manages a route target that will accept the given path. + Requires `state-map` to work on dynamically-added routes." + ([component path] (accepts-route? component path rc/*query-state*)) + ([component path state-map] + (boolean (route-target component path state-map)))) (defn ast-node-for-route "Returns the AST node for a query that represents the router that has a target that can accept the given path. This is a breadth-first @@ -307,13 +335,16 @@ ast - A query AST node path - A vector of the current URI segments. + state-map - Application state map, required for support of dynamically-added routes. Returns an AST node or nil if none is found." - [{:keys [component children] :as ast-node} path] - (or - (and (accepts-route? component path) ast-node) - (some #(and (accepts-route? (:component %) path) %) children) - (some #(ast-node-for-route % path) children))) + ([{:keys [component children] :as ast-node} path] + (ast-node-for-route ast-node path rc/*query-state*)) + ([{:keys [component children] :as ast-node} path state-map] + (or + (and (accepts-route? component path state-map) ast-node) + (some #(and (accepts-route? (:component %) path state-map) %) children) + (some #(ast-node-for-route % path state-map) children)))) (defn ast-node-for-live-router "Returns the AST node for a query that represents the closest \"live\" (on-screen) router @@ -426,11 +457,11 @@ state-map (app/current-state app) root-query (rc/get-query relative-class-or-instance state-map) ast (eql/query->ast root-query) - root (ast-node-for-route ast new-route) + root (ast-node-for-route ast new-route state-map) result (atom [])] (loop [{:keys [component]} root path new-route] (when (and component (router? component)) - (let [{:keys [target matching-prefix]} (route-target component path) + (let [{:keys [target matching-prefix]} (route-target component path state-map) target-ast (some-> target (rc/get-query state-map) eql/query->ast) prefix-length (count matching-prefix) remaining-path (vec (drop prefix-length path)) @@ -448,7 +479,7 @@ (when (vector? target-ident) (swap! result conj (vary-meta target-ident assoc :component target :params params))) (when (seq remaining-path) - (recur (ast-node-for-route target-ast remaining-path) remaining-path))))) + (recur (ast-node-for-route target-ast remaining-path state-map) remaining-path))))) @result))) (defn signal-router-leaving @@ -701,13 +732,13 @@ router relative-class-or-instance root-query (rc/get-query router state-map) ast (eql/query->ast root-query) - root (ast-node-for-route ast new-route) + root (ast-node-for-route ast new-route state-map) routing-actions (atom (list)) pessimistic-txn (atom []) delayed-targets (atom [])] (loop [{:keys [component]} root path new-route] (when (and component (router? component)) - (let [{:keys [target matching-prefix]} (route-target component path) + (let [{:keys [target matching-prefix]} (route-target component path state-map) target-ast (some-> target (rc/get-query state-map) eql/query->ast) prefix-length (count matching-prefix) remaining-path (vec (drop prefix-length path)) @@ -755,7 +786,7 @@ (binding [rc/*after-render* true] (completing-action)))) (when (seq remaining-path) - (recur (ast-node-for-route target-ast remaining-path) remaining-path))))) + (recur (ast-node-for-route target-ast remaining-path state-map) remaining-path))))) ;; Normal route instructions are sent depth first to prevent flicker (doseq [action @routing-actions] (action)) @@ -820,19 +851,21 @@ (defn validate-route-targets "Run a runtime validation on route targets to verify that they at least declare a route-segment that is a vector." [router-instance] - (doseq [t (get-targets router-instance) - :let [segment (route-segment t) - valid? (and - (vector? segment) - (not (empty? segment)) - (every? #(or (keyword? %) (string? %)) segment))]] - (when-not valid? - (log/error "Route target " - (rc/component-name t) - "of router" - (rc/component-name router-instance) - "does not declare a valid :route-segment. Route segments must be non-empty vector that contain only strings" - "and keywords. See https://book.fulcrologic.com/#err-dr-target-lacks-r-segment")))) + (when #?(:cljs goog.DEBUG :clj true) + (let [state-map (app/current-state router-instance)] + (doseq [t (get-targets router-instance state-map) + :let [segment (route-segment t) + valid? (and + (vector? segment) + (not (empty? segment)) + (every? #(or (keyword? %) (string? %)) segment))]] + (when-not valid? + (log/error "Route target " + (rc/component-name t) + "of router" + (rc/component-name router-instance) + "does not declare a valid :route-segment. Route segments must be non-empty vector that contain only strings" + "and keywords. See https://book.fulcrologic.com/#err-dr-target-lacks-r-segment")))))) #?(:clj (defn defrouter* [env router-ns router-sym arglist options body] @@ -844,13 +877,14 @@ (compile-error env options (str "defrouter options are invalid: " (s/explain-str ::defrouter-options options)))) (let [{:keys [router-targets]} options _ (when (empty? router-targets) - (compile-error env options "defrouter requires at least one router-target")) + (compile-error env options "defrouter requires a vector of :router-targets with at least one target")) id (keyword router-ns (name router-sym)) getq (fn [s] `(or (rc/get-query ~s) (throw (ex-info (str "Route target has no query! " (rc/component-name ~s)) {})))) query (into [::id [::uism/asm-id id] + ::dynamic-router-targets {::current-route (getq (first router-targets))}] (map-indexed (fn [idx s] @@ -1051,31 +1085,36 @@ (partition-all 2 segments)))))) (defn resolve-path-components - [StartingClass RouteTarget] - (if (rc/component-options RouteTarget :route-segment) - (let [query (rc/get-query StartingClass) - root-node (eql/query->ast query) - zipper (zip/zipper #(contains? % :children) :children (fn [n children] (assoc n :children children)) root-node) - node (->> zipper - (iterate zip/next) - (drop-while (fn [n] - (let [{:keys [component]} (zip/node n)] - (and - (not= component RouteTarget) - (not (zip/end? n)))))) - first) - found? (= RouteTarget (some-> node zip/node :component))] - (when found? - (conj (->> node zip/path (map :component) vec) RouteTarget))) - nil)) + ([StartingClass RouteTarget] + (resolve-path-components StartingClass RouteTarget [])) + ([StartingClass RouteTarget base-path] + (if (= StartingClass RouteTarget) + (let [parent (last base-path) + final-path (conj base-path RouteTarget)] + (when (router? parent) final-path)) + (let [path (conj base-path StartingClass)] + (if (router? StartingClass) + (let [targets (get-targets StartingClass rc/*query-state*)] + (->> targets + (keep #(resolve-path-components % RouteTarget path)) + (first))) + (let [candidates (->> (comp/get-query StartingClass) + (eql/query->ast) + :children + (keep :component))] + (->> candidates + (keep #(resolve-path-components % RouteTarget path)) + (first)))))))) (defn resolve-path "Attempts to resolve a path from StartingClass to the given RouteTarget. Can also be passed `resolved-components`, which - is the output of `resolve-path-components`. + is the output of `resolve-path-components`. + + NOTE: This function works against static queries UNLESS you bind `rc/*query-state*` to `app/current-state`. Returns a vector of route segments. Any keywords in the result will be replaced by the values from `params`, if present. - Returns nil if no path can be found." + Returns nil if no path can be found. Be sure rc/*query-state* is bound to current app state if you want to include dynamic queries." ([resolved-components params] (when (seq resolved-components) (let [base-path (into [] @@ -1086,7 +1125,9 @@ (str (get params ele)) ele)) base-path)))) ([StartingClass RouteTarget params] - (resolve-path (resolve-path-components StartingClass RouteTarget) params))) + (if (:route-segment (comp/component-options RouteTarget)) + (resolve-path (resolve-path-components StartingClass RouteTarget) params) + (log/warn "Attempt to resolve the path to a component that has no route-segment")))) (defn resolve-target "Given a new-route path (vector of strings): resolves the target (class) that is the ultimate target of that path." @@ -1094,15 +1135,15 @@ (let [state-map (app/current-state app) root-query (rc/get-query (app/root-class app) state-map) ast (eql/query->ast root-query) - root (ast-node-for-route ast new-route)] + root (ast-node-for-route ast new-route state-map)] (loop [{:keys [component]} root path new-route] (when (and component (router? component)) - (let [{:keys [target matching-prefix]} (route-target component path) + (let [{:keys [target matching-prefix]} (route-target component path state-map) target-ast (some-> target (rc/get-query state-map) eql/query->ast) prefix-length (count matching-prefix) remaining-path (vec (drop prefix-length path))] (if (seq remaining-path) - (recur (ast-node-for-route target-ast remaining-path) remaining-path) + (recur (ast-node-for-route target-ast remaining-path state-map) remaining-path) target)))))) (letfn [(active-routes* [state-map {:keys [path] :as result} parent-component ast-nodes] @@ -1158,3 +1199,239 @@ query (comp/get-query starting-from state-map) {:keys [children]} (eql/query->ast query)] (set (active-routes* state-map {:path []} starting-from children)))))) + +(defn dynamic-router + "The functional version of `defrouter`. Generates a router (particularly useful at runtime for use with dynamically + generated components) with the given Fulcro registry-key and list of router-targets. The options map can contain: + + * `:render` - A (fn [this props] ...) that needs to function as described in `defrouter`. + * Any other options that `defrouter` supports in the component options map. + " + ([registry-key targets] + (dynamic-router registry-key targets {})) + ([router-registry-key router-targets {:keys [render always-render-body?] :as options}] + (let [main-target (first router-targets) + alt-targets (rest router-targets) + static-query (into + [::id + [:uism/asm-id router-registry-key] + ::dynamic-router-targets + {::current-route (or + (rc/get-query main-target) + (throw (ex-info (str "Route target has no query! " (rc/component-name main-target)) {})))}] + (map-indexed (fn [idx c] {(keyword "alt" idx) (rc/get-query c)})) + alt-targets) + addl-options (dissoc options :render) + user-render (fn [this router-props route-factory current-route-target-props] + (when render + (let [current-state (uism/get-active-state this router-registry-key) + state-map (comp/component->state-map this) + sm-env (uism/state-machine-env state-map nil router-registry-key :fake {}) + pending-path-segment (when (uism/asm-active? this router-registry-key) + (uism/retrieve sm-env :pending-path-segment)) + render-props {:pending-path-segment pending-path-segment + :route-props current-route-target-props + :route-factory route-factory + :current-state current-state + :router-state (get-in router-props [[::uism/asm-id router-registry-key] ::uism/local-storage])}] + (render this render-props)))) + render-target-only (fn [this route-target-props route-factory] + (when route-factory + (route-factory route-target-props (rc/get-computed this))))] + (comp/sc router-registry-key + (merge + addl-options + {:preserve-dynamic-query? true, + :router-targets router-targets + :ident (fn [_ _] [::id router-registry-key]), + :componentDidMount (fn [this] (validate-route-targets this)), + :initial-state (fn [params] + (into + {::id router-registry-key + ::current-route (rc/get-initial-state (first router-targets) params)} + (map-indexed (fn [idx c] [(keyword (str "alt" idx)) (rc/get-initial-state c {})])) + (rest router-targets))) + :query (fn [_] static-query)}) + (fn [this {::keys [id current-route] :as props}] + (let [TargetClass (current-route-class this) + route-factory (some-> TargetClass (comp/computed-factory))] + (if always-render-body? + (user-render this props route-factory current-route) + (let [TargetClass (current-route-class this) + current-state (uism/get-active-state this router-registry-key) + states-to-render-route (if render #{:routed :deferred} (constantly true))] + (if (states-to-render-route current-state) + (render-target-only this current-route route-factory) + (user-render this props route-factory current-route)))))))))) + +(defn add-route-target* + "Mutation helper. Add a target to a router dynamically. + + `router` - A class or registry key + `target` - A class or registry key + `initial-state-params` - Parameters to pass to `get-initial-state` when merging the state of `target` (which is only + done if that component has a stable ident). + " + [state-map {:keys [router target initial-state-params]}] + (let [Router (rc/registry-key->class router) + Target (rc/registry-key->class target) + stable-ident? (and Target (some? (second (comp/get-ident Target {})))) + router-ident (rc/get-ident Router {}) + target-registry-key (rc/class->registry-key Target)] + (cond + (nil? Router) + (do + (log/error "Cannot add route target. Router class not found for" router) + state-map) + + (nil? Target) + (do + (log/error "Cannot add route target. Target class not found for" target) + state-map) + + (not (vector? (route-segment Target))) + (do + (log/error "Cannot add route target. Target class has a missing or invalid :route-segment: " target) + state-map) + + :else + (cond-> (update-in state-map (conj router-ident ::dynamic-router-targets) (fnil conj #{}) target-registry-key) + stable-ident? (merge/merge-component Target (rc/get-initial-state Target (or initial-state-params {}))))))) + +(defmutation add-route-target + "Mutation. Add a target to a router dynamically. + + params: + * router - A router class or registry key + * target - A target class or registry key (must have :route-segment) + * initial-state-params - Parameters for the initial state for merging the target into state (if it has a stable ident) + + See also `add-route-target!` and `add-route-target*`. + " + [{:keys [router target] :as params}] + (action [{:keys [state]}] + (swap! state add-route-target* router target))) + +(defn add-route-target! + "Add a target to an existing router. + + app-ish - An app or component + options - A map: + * router - A router class or registry key + * target - A target class or registry key (must have :route-segment) + * initial-state-params - Parameters for the initial state for merging the target into state (if it has a stable ident) + " + [app-ish options] + (comp/transact! app-ish [(add-route-target options)])) + +(defn add-route-target!! + "Add a target to an existing router synchronously. This will NOT show in Fulcro Inspect as a transaction. + + app-ish - An app or component + options - A map: + * router - A router class or registry key + * target - A target class or registry key (must have :route-segment) + * initial-state-params - Parameters for the initial state for merging the target into state (if it has a stable ident) + " + [app-ish options] + ;; We go straight to the app state atom, which is safe because the targets are never rendered, and it ensures + ;; absolute synchronous change. + (let [state-atom (::app/state-atom (comp/any->app app-ish))] + (swap! state-atom add-route-target* options))) + +(defn absolute-path + "Get the absolute path for the given route target. + + NOTE: Using this on a route target that is on multiple paths of your application + can lead to ambiguity and failure of general routing, since this will then return an unpredictable result." + [app-ish RouteTarget route-params] + (let [app (comp/any->app app-ish) + app-root (app/root-class app) + state-map (app/current-state app)] + (binding [rc/*query-state* state-map] + (resolve-path app-root RouteTarget route-params)))) + +(defn- loaded? [k] #?(:cljs (or (nil? k) (enc/catching (loader/loaded? k))) :clj true)) + +(defn route-to! + "Route to a specific `target` of the given `Router`. This is different from `change-route!` in that it makes the + code a bit more navigable (though a bit less easily refactored), and supports some additional dynamic features: + + * Dynamically adding the target to the router if it isn't there + * Loading a module that contains the router (dynamic code load through cljs.loader) and adding it to the router + + `app-ish` - An app or component instance + + The `options` map can contain: + + * `router` (OPTIONAL/REQUIRED) - A router class or registry key for that router. Required if you want auto-add or loading to work. + * `target` (REQUIRED) - A target class or registry key. + * `:route-params` - A map from keywords to values for any of the route parameters expected for the given target. + * `:auto-add?` - Default false. Automatically add the target to the router if it isn't already there. + * `:load-from ` - Default nil. Check to see if is loaded. If not, load it, IMPLIES `auto-add? true`. + * `:initial-state-params` - Parameters to use for the merge with get-initial-state if the component is added, and has a stable ident. + * `after-load (fn [app] ...)` - IF dynamically loaded, this function will be called before attempting to add the target, allowing + you to dynamically generate the component from the loaded code if necessary. Such generation MUST be synchronous. + * `before-change (fn [app {:keys [target path route-params]}] ...)` - If the routing is possible and is not denied, + this will be called just before the route is put into effect. + " + [app-ish {Router :router + :keys [target + route-params + auto-add? + after-load + before-change + initial-state-params + load-from] :as options}] + (let [app (comp/any->app app-ish) + state-map (app/current-state app) + auto-add? (or auto-add? (boolean load-from)) + Router (rc/registry-key->class Router) + target-key (if (rc/legal-registry-lookup-key? target) + (keyword target) + (rc/class->registry-key target)) + RouteTarget (rc/registry-key->class target-key) + existing-targets (and Router (into #{} (map rc/class->registry-key) (get-targets Router state-map))) + present? (or (nil? Router) (contains? existing-targets target-key)) + loaded? (loaded? load-from)] + (cond + (and Router auto-add? loaded? (not present?)) + (do + (add-route-target!! app {:router Router + :initial-state-params initial-state-params + :target RouteTarget}) + (route-to! app {:router Router + :target RouteTarget + :route-params route-params + :auto-add? false})) + + (and Router (not loaded?)) + #?(:clj nil + :cljs (loader/load load-from (fn [] + (when (fn? after-load) (after-load app)) + (add-route-target!! app {:router Router + :initial-state-params initial-state-params + :target target-key}) + (route-to! app {:router Router + :target target-key + :route-params route-params + :auto-add? false})))) + + (and present? RouteTarget) + ;; TODO: We can disambiguate sibling collisions using Router, if supplied. + (if-let [path (absolute-path app RouteTarget route-params)] + (do + (when-not (every? string? path) + (log/warn "Insufficient route parameters passed. Resulting route is probably invalid." + (comp/component-name RouteTarget) route-params)) + (when (and (can-change-route? app path) (fn? before-change)) + (before-change app {:target RouteTarget + :path path + :route-params route-params})) + (change-route! app path route-params)) + (log/error "Routing failed. Unable to construct route path from given arguments" {:router Router :target target-key})) + + (and (not present?) (not auto-add?)) + (do + (log/error "Cannot route to target because the router does not have that target (perhaps it failed to load?, or auto-add? was false)." + {:router Router :target target}))))) diff --git a/src/test/com/fulcrologic/fulcro/routing/dynamic_routing_test.cljc b/src/test/com/fulcrologic/fulcro/routing/dynamic_routing_test.cljc index 6eee3db3..6b2a08b4 100644 --- a/src/test/com/fulcrologic/fulcro/routing/dynamic_routing_test.cljc +++ b/src/test/com/fulcrologic/fulcro/routing/dynamic_routing_test.cljc @@ -148,7 +148,7 @@ "Can replace parameters via a map at the end" (dr/path-to A B C {:a/param "hello" :c/param "there"}) => ["a" "hello" "b" "c" "there"])) -(specification "resolve-path" +(specification "resolve-path" :focus (assertions "resolves paths from some relative starting point" (dr/resolve-path Root2 Settings {}) => ["settings"]