How to set up Dynamic UI Routing for a simple Fulcro app

439 Views Asked by At

I've crawled through some of the web and video documentation for routing. But I'm failing to get Dynamic UI Routing working for a simple set of pages.

root.cljs

(ns ui.root)
;; ...

(defsc Index [this props]
  {:query [:index]
   :ident (fn [] [:id :index])
   :route-segment ["index"]
   :initial-state {}}

  (h3 "Index"))

(defsc Landing [this props]
  {:query [:landing]
   :ident (fn [] [:id :landing])
   :route-segment ["landing"]
   :initial-state {}}

  (h3 "Landing"))

(defsc Settings [this props]
  {:query [:settings]
   :ident (fn [] [:id :settings])
   :route-segment ["settings"]
   :initial-state {}}

  (h3 "Setting"))


(dr/defrouter TopRouter [this {:keys [current-state] :as props}]
  {:router-targets [Game Settings Landing Index]
   :initial-state (fn [{:keys [current-state]}]
                    {:current-state current-state})}

  (case current-state
    :pending (dom/div "Loading...")
    :failed (dom/div "Failed!")
    (dom/div "No route selected.")))

(def ui-top-router (comp/factory TopRouter))


(defsc Root [this {:keys [router] :as props}]
  {:query [{:router (comp/get-query TopRouter)}]
   :ident (fn [] [:id :root])

   :initial-state (fn [_]
                    {:top-router (comp/get-initial-state TopRouter {:current-state :pending})
                     :index {:id 1}
                     :landing {:id 1}
                     :settings {:id 1}})

   :componentDidMount (fn [_] (log/info "Root Initial State /" (prim/get-initial-state Root {})))}

  (log/info "Root Props /" this props)
  (ui-top-router router {:current-state :pending}))

client.cljs

(ns client)
...

(app/mount! @app root/Root "app" {:initialize-state? true
                                  :foo :bar})

Q: Initial load gives this output. How do we pass props into the Root component? I expect to see at least {:foo :bar}.

INFO [ui.root:81] - Root Props / [object Object] {}
INFO [ui.root:53] - TopRouter Props / {:current-state nil, :route-factory #object[cljs.core.MetaFn], :pending-path-segment nil, :route-props nil}
INFO [ui.root:77] - Root Initial State / nil

Q: If this is my initial state, is the :query and :ident right? And do they (:query + :ident) correspond to the :route-segment ? Do they need to?

{:index {:id 1}
 :landing {:id 1}
 :settings {:id 1}}

Q: How do we kick off the initial route? Calling this fails with the below message.

(dr/change-route app ["index"])

INFO [com.fulcrologic.fulcro.rendering.ident-optimized-render:146] - Optimized render failed. Falling back to root render.

>> UPDATE <<

I was able to get a working Fulcro Root :initial-state, and :query and :ident on child components.

On initial load, the router fails with this.

INFO [beatthemarket.ui.root:61] - TopRouter Props / {:current-state nil, :route-factory #object[cljs.core.MetaFn], :pending-path-segment nil, :route-props {:index/id 1, :index/text "Index Text"}}

core.cljs:159 ERROR [com.fulcrologic.fulcro.routing.dynamic-routing:410] - will-enter for router target beatthemarket.ui.root/Index did not return a valid ident. Instead it returned:  [:index/id nil]
core.cljs:159 ERROR [com.fulcrologic.fulcro.routing.dynamic-routing:410] - will-enter for router target beatthemarket.ui.root/Index did not return a valid ident. Instead it returned:  [:index/id nil]

browser.cljs:25 shadow-cljs: WebSocket connected!
browser.cljs:25 shadow-cljs: REPL session start successful

core.cljs:159 INFO [com.fulcrologic.fulcro.algorithms.indexing:104] - component beatthemarket.ui.root/Index's ident ([:index/id nil]) has a `nil` second element. This warning can be safely ignored if that is intended.

So a command like (dr/change-route app (dr/path-to root/Index)) fails with this.

react_devtools_backend.js:6 ERROR [com.fulcrologic.fulcro.routing.dynamic-routing:410] - will-enter for router target beatthemarket.ui.root/Index did not return a valid ident. Instead it returned:  [:index/id nil]

These are my client.cljs and root.cljs look like this.

2

There are 2 best solutions below

0
On

I guess you first have to fix the Ident problem with your Index component.

Does routing work for the other components?

1
On

I think your Root initial state should be calling (comp/get-initial-state Index). You have an initial state set on Index but it's different than the initial state that Root gives.

Also, a big part of Fulcro (and React) is that you build a tree of components and a tree of data and they need to match.

The way you have it here, there's no connection between "Root" and "Index" because Root only renders (ui-top-router router). You're getting the data for Index by having a query for {:root/index (comp/get-query Index)} but you're not creating the connection between Root and Index by having Root call Index and pass in that data. You need a (ui-index index) inside Root.

And if you do that, then in that (ui-index index) call, index will take the value that you're setting with :initial-state. That's why you'll also need to update initial-state to call comp/get-initial-state so that you can get the :index/id 1 value that you're setting in the Index component's initial state.

(defsc Index [this {:index/keys [id text]}]
  {:query [:index/id :index/text]
   :ident [:index/id :index/id]
   :route-segment ["index"]
   :initial-state {:index/id 1
                   :index/text :param/text}}

  (h3 text))

(defsc Root [this {:root/keys [router index landing game settings]}]
  {:query [{:root/router (comp/get-query TopRouter)}
           {:root/index (comp/get-query Index)}
           {:root/landing (comp/get-query Landing)}
           {:root/game (comp/get-query Game)}
           {:root/settings (comp/get-query Settings)}]

   :initial-state {:root/router {}
                   :root/index {:text "Index Text"}
                   :root/landing {:text "Landing Text"}
                   :root/game {:text "Game Text"}
                   :root/settings {:text "Settings Text"}}}
  (when router
   (dom/div (ui-top-router router))))

After all of that is addressed, here's the next thing you might be interested in.

You probably won't always want to hard-code index/id 1. You might need to fetch data from a server in order to have anything to render.

That's where :will-enter and "deferred routing" come into play. See the example below and the docs at http://book.fulcrologic.com/#_router_rendering_of_a_deferred_ui.

(defsc Person [this {:ui/keys      [modified?]
                     :person/keys  [id name]
                     :address/keys [city state]
                     :as           props}]
  {:query               [:ui/modified? :person/id :person/name :address/city :address/state]
   :ident               :person/id
   :route-segment       ["person" :person/id]
   :route-cancelled     (fn [{:person/keys [id]}]
                          (log/info "Routing cancelled to user " id))
   :allow-route-change? (fn [this {:ui/keys [modified?]}]
                          (when modified?
                            #?(:cljs (js/alert "You cannot navigate until the user is not modified!")))
                          (not modified?))
   :will-enter          (fn [app {:person/keys [id] :as route-params}]
                          (log/info "Will enter user with route params " route-params)
                          ;; be sure to convert strings to int for this case
                          (let [id (if (string? id) (edn/read-string id) id)]
                            (dr/route-deferred [:person/id id]
                                               #(df/load app [:person/id id] Person
                                                         {:post-mutation        `dr/target-ready
                                                          :post-mutation-params {:target [:person/id id]}}))))}