Fork me on GitHub
#fulcro
<
2023-05-17
>
Norman Kabir08:05:14

Has anyone compared Fulcro to Electric? https://github.com/hyperfiddle/electric Both take novel approaches to web development. Both are different enough from traditional approaches that comparing their engineering trade-offs is challenging.

Jakub Holý (HolyJak)16:05:38

I plan to rewrite a small Fulcro app in Electric but it will take me many weeks to get to it 😭 My impressions is > To me, the most interesting part is that Phoenix, similarly to Electric Clojure and HTMX, moves state and logic to the backend (BE) and handles efficiently communicating changes to the (mostly?) static frontend (FE). i.e. both F. and E. handle server<>client communication but Fulcro is for SPAs and other frontend-code-heavy apps, while Electric makes it possible to have most logic and state management on the backend. But I might be wrong…

👍 6
az22:05:41

After going through the troubleshooting blog post, I feel I'm somewhere in the realm of 3: Data in DB OK but props in the UI are nil, but I'm not seeing where I'm going wrong. https://www.loom.com/share/74badf7a675d428a83b6eb0782cadc8d

tony.kay00:05:09

Your load target is still wrong

tony.kay00:05:35

target a FIELD, not a COMPONENT. The ident itself will merge components to their correct locations. You want load to place an ident in the field.

tony.kay00:05:22

actually, it’s a little worse than that

tony.kay00:05:44

So, you mean for RecipeDetail to be another view of a given recipe. The ident of that component should therefore be a recipe’s ident (not some singleton component). Normalization will always put a thing at it’s ident, but you’re asking load to normalize a recipe using a thing that won’t have a recipe ident.

az00:05:35

Yes, I'm trying to get this RecipeList -> Recipe Detail flow. Partial load on the list query, then full record load on detail.

tony.kay00:05:44

So, what you want is a recipe list, which should a summary of recipes. Then a recipe detail that shows that same info expanded.

tony.kay00:05:34

so:

(defsc RecipeDetail [this props]
 {:ident :recipe/id
  :query [:recipe/id :recipe/description :recipe/name]
  ; no initial state...this is something you'll load, so won't be on first render frame anyway
  :route-segment ["recipe-detail" :id]
  :will-enter  (fn [app {:keys [id]}]
                 (let [id (coerce id)] ; you do coercion
                 (dr/route-deferred [:recipe/id id]
                    #(df/load app [:recipe/id id] RecipeDetail)))
 }
  ...)

tony.kay00:05:59

(defsc RecipeRow [this props]
 {:ident :recipe/id
  :query [:recipe/id :recipe/name]
  ; no initial state...this is something you'll load, so won't be on first render frame anyway
 }
  ...)

tony.kay00:05:17

ident is about the real identity of the thing globally full-stack

tony.kay00:05:37

you only make up singleton idents for UI-only components that have no individual identity otherwise.

tony.kay00:05:32

Now, your “report” of all recipes is just what you have in Main (which I would call RecipeList)

tony.kay00:05:09

when a user click on a particular recipe, then you want to issue a route to a RecipeDetail (which would go in the router as a target) with an id.

tony.kay00:05:30

so add RecipeDetail to your router-targets

tony.kay01:05:44

but use a target on that guy of ["recipe-detail" :id]

tony.kay01:05:58

so you can pass the recipe ID as :id: in the route parameters

tony.kay01:05:39

now your will-enter will have that parameter (as a string…you’ll have to convert it back to the correct type), and you can issue your load. Alternatively, you could issue the load, and as a post-action issue your route change

tony.kay01:05:46

see the RecipeDetail above (edited) for an approximation

az01:05:22

I think I am following up to ["recipe-detail" :id]. This is what I have so far. Please excuse the strange naming :recipes/id instead of :recipe/id the postgres db has pluralized tables.

(ns com.sajb.ui
  (:require
   [com.sajb.mutations :as mut]
   [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
   [com.fulcrologic.fulcro.routing.dynamic-routing :as dr :refer [defrouter]]
   [com.fulcrologic.fulcro.algorithms.merge :as merge]
   [com.fulcrologic.fulcro.algorithms.tempid :as tempid]
   [com.fulcrologic.fulcro.algorithms.data-targeting :as targeting]
   [com.fulcrologic.fulcro.algorithms.normalized-state :as norm]
   [com.fulcrologic.fulcro.components :as comp :refer [defsc transact!]]
   [com.fulcrologic.fulcro.ui-state-machines :as uism]
   [com.sajb.application :refer [main-app]]
   [com.fulcrologic.fulcro.raw.components :as rc]
   [com.fulcrologic.fulcro.data-fetch :as df]
   [com.fulcrologic.fulcro.dom :as d]
   [reitit.frontend.easy :as rfe]))

(declare RecipeDetails)

(defmutation load-recipe [{:recipes/keys [id]}]
  (action [{:keys [app]}]
          (df/load! app [:recipes/id id] RecipeDetails)))

(defmutation use-recipe [{:recipes/keys [id]}]
  (action [{:keys [app state]}]
          (swap! state assoc-in [:component/id ::recipe-details] [:recipes/id id])
          (dr/target-ready! app [:component/id ::recipe-details])))


(defsc RecipeDetails [this {:as props
                            :recipes/keys [id name description]}]
  {:ident :recipes/id
   :query [:recipes/id :recipes/name :recipes/description]
   :route-segment ["recipes" :recipes/id]
   :will-enter (fn [app {:recipes/keys [id]}]
                 (comp/transact! app [(use-recipe {:recipes/id id})])
                 (dr/route-deferred [:component/id ::recipe-details]
                                    #(comp/transact! app [(load-recipe {:recipes/id id})])))}
  (d/div
   (d/h2 "Recipe Details")
   (d/p (str "ID: " id))
   (d/p (str "Name: " name))
   (d/p (str "Description: " description))))

(defsc Recipe [this {:recipes/keys [id name]}]
  {:ident :recipes/id
   :query [:recipes/id :recipes/name]
   :initial-state (fn [_] {:recipes/id 1
                           :recipes/name "Blueberry Jam"})}
  (d/a {:href (rfe/href :recipe-details {:id id})}
       (d/p (str "id: " id))
       (d/p (str "name: " name))))

(def ui-recipe (comp/factory Recipe {:keyfn :recipes/id}))

(defsc Main [this {:as props
                   :keys [all-recipes]}]
  {:ident         (fn [_] [:component/id ::main])
   :query [{:all-recipes (comp/get-query Recipe)}]
   :initial-state (fn [_] {:all-recipes [(comp/get-initial-state Recipe)]})
   :route-segment ["main"]}

  (d/div "All Recipes"
         (when all-recipes
           (d/ol
            (mapv ui-recipe all-recipes)))))

(defrouter TopRouter [this {:keys [current-state pending-path-segment]}]
  {:router-targets [Main RecipeDetails]}
  (case current-state
    :pending (d/div "Loading...")
    :failed (d/div "Loading seems to have failed. Try another route.")
    (d/div "Unknown route")))

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

(defsc Root [this {:root/keys [router]}]
  {:query         [{:root/router (comp/get-query TopRouter)}]
   :initial-state {:root/router {}}}
  (let [top-router-state (or (uism/get-active-state this ::TopRouter) :initial)]
    (if (= :initial top-router-state)
      (d/div :.loading "Loading...")
      (d/div
       (d/a {:href (rfe/href :main)}
            #_{:onClick #(dr/change-route this ["main"])} "All Recipes")
       #_(d/button {:onClick #(dr/change-route this ["recipes" 1])} "Recipe Details")
       (ui-top-router router)))))

tony.kay01:05:04

don’t use namespaced keywords in route segment

az01:05:06

how do we come up with the string ["recipe-detail" :id] is that a target on the load?

tony.kay01:05:15

it is meant to be compatible with URLs, which use / for something else 🙂

az01:05:23

got it, you were talking about route-segments. So now I have: ["recipes" :id]

az01:05:41

that's where you were saying to have ["recipe-details" :id]

tony.kay01:05:44

(dr/change-route! this ["recipes" (:recipes/id row)])

tony.kay01:05:03

where row is the props from an item

tony.kay01:05:03

(dom/a {:onClick (fn [] (dr/change-route! this ["recipes" (:recipes/id props)]))})

tony.kay01:05:22

unless you installed HTML5 routing and route-history there IS NO integration with the browser

tony.kay01:05:31

dynamic routing is UI agnostic (could be react native)

tony.kay01:05:55

and in either case, you should call change-route!, not mess with URIs

az01:05:09

I did, I have reitit set up, it's working all the routing is working, data is loading

tony.kay01:05:25

I still do not recommend using URIs in your code

tony.kay01:05:52

actually, what I do recommend is you use RAD’s utilities, which let you change route directly to a target, and it figures it out for you

tony.kay01:05:19

(it makes the assumption that a leaf doesn’t have more than one possible path, which is not a constraint dr imposes)

tony.kay01:05:07

but for now, using the core underlaying system is a good learning experience

tony.kay01:05:42

when you installed reitit did you hook it up to dr/change-route! I assume?

tony.kay01:05:02

I guess that’s ok. The beauty of using the RAD way is that you can “jump” to a target source from the code…e.g. (rroute/route-to! this ReceipeDetails {:id id}) causes it to figure out the path for you, and also makes it so you can use your IDE nav to jump there

az01:05:32

Yes, on route change I fire

(dr/change-route main-app (convert-params-to-path new-route))

tony.kay01:05:42

the other issue with using URIs and core dr is that it doesn’t refactor well. If you move the UI araound (e.g. nest what you have a level deeper) then all the paths need to change. With the RAD way it auto-heals. If you want old routes to continue to work (historic bookmarks) that is a simple startup map from old URL to new target.

az01:05:38

I totally can see that. I will definitely switch to the RAD solution

az01:05:22

I feel I'm so close to making a breakthrough with Fulcro, once I have this flow working, I can really think deeply about the structure and I think many things will click

tony.kay01:05:01

your only problem at this point is that ident I think

tony.kay01:05:25

your destructuring in will-enter is using the namespace as well. I guess it technically works to use a qualified keyword there…I didn’t remember that