Fork me on GitHub
#fulcro
<
2023-05-16
>
danielgrosse04:05:36

Hello, I’m very interested in Fulcro. Is it possible to create a custom renderer which uses htmx and its dynamic templates?

danielgrosse15:05:41

Is there some documentation or example on this topic?

tony.kay20:05:30

I’m not sure what the advantage would be. Are you talking about transpiling https://htmx.org/ this kind of stuff and trying to use Fulcro with it?

tony.kay20:05:58

The whole purpose of Fulcro is to eliminate that kind of entanglement. If you like what htmx does, then use htmx. The answer to your question of “can I make a custom renderer to do X”? The answer is almost certainly yes, but the follow-on question is “Should I?“, to which, in this case, I would probably answer no. As far as examples: There are youtube videos showing custom rendering with Reagent/Reframe. There are docstrings in the code. There is the code itself. The rendering of Fulcro is somewhat tied to react, but you can use it in a headless mode as well. So, if you just want data management you could use it headless, and add a listener to the atom or a hook in the transaction processing to trigger whatever visualization refresh you want.

danielgrosse03:05:44

Thanks for the indepth explanation. I know you are made Fulcro for non trivial web apps. The idea of the EQL queries and the state machine is really interesting so I want to make use of these in general. But sometimes I have a view which is rather simple and another which is more complex. For the simple one I don't want to have to send and evaluate lots of code on the client.

az16:05:59

Hello all. I'm trying to understand routing in Fulcro. I'm at a point I need to phone a friend. Using fulcro inspect, I see that the db has the data for :all-recipes as well as the normalized values. Issue is I can't seem to figure out how to get the all-recipes key to pull the data from the db and display the list. Any ideas? Here's the overview of what's going on: https://www.loom.com/share/d50483ce66cb40509cdf9fcb2eb750af The code:

tony.kay20:05:14

Routers in Fulcro are dynamic components that have to be told what to do. The “default” view of the initial frame is a pure rendering of state. The routing system hasn’t been started until you actually start it (by routing to a leaf target). In general I recommend setting up the app to have an initial screen that shows “loading” while you initialize things, and then once you’re running issue a routing instruction to go to the place of interest. That action is what triggers things like will-enter.

az20:05:20

Thanks @U0CKQ19AQ - I have the call to (dr/change-route! main-app ["main"]) So I see the route load, I even see the loading state then the component load. I see the data in the db, but I think I'm not yet fully understanding how query, ident, and initial-state work. Somehow I see all the normalized data and top level root data all-recipes but it doesn't render. Going back through all the videos. Thanks for the response.

tony.kay20:05:52

your target

tony.kay20:05:10

you want to target a field, not a component

tony.kay20:05:24

[:component/id ::main :all-recipes]

tony.kay20:05:36

you’re trying to overwrite the entire component 😛

tony.kay20:05:03

and target is a top-level parameter for load, not a post-mutation-params

tony.kay20:05:39

I see. You’re telling the router what that component is ready

tony.kay20:05:45

sorry, small screen crossed eyes

tony.kay20:05:59

but you do need to ADD :target as a top-level parameter to your load

tony.kay20:05:31

:target [:component/id ::main :all-recipes] otherwise load doesn’t know where in the graph to put the loaded data, and just defaults to putting it in app root

tony.kay20:05:12

the beauty of having normalized data is that targeting is always either at the root, or at some field in a normalized entity (so either you don’t need to say it, or it is a 3-tuple of ident + field)

az20:05:26

so:

#(df/load app
                                              :all-recipes Recipe
                                              :target [:component/id ::main :all-recipes]
                                              :post-mutation `dr/target-ready
                                              :post-mutation-params
                                              {:target [:component/id ::main]})

tony.kay20:05:44

looks right

tony.kay20:05:33

does that fix it?

az20:05:18

Unfortunately no, getting some other errors:

[com.fulcrologic.fulcro.routing.dynamic-routing:137] - route-deferred  was invoked with the ident  [:component/id :com.sajb.ui/main]  which doesn't seem to match the ident of the wrapping component (class  function (js_props){
var this$ = this;

az20:05:44

(defsc Main [this {:as props
                   :keys [all-recipes]}]
  {:ident         :all-recipes
   :query [{:all-recipes (comp/get-query Recipe)}]
   :initial-state {:all-recipes [{:recipes/id 1
                                  :recipes/name "Blueberry Jam"}]}
   :route-segment ["main"]
   :will-enter (fn [app route-params]
                 (dr/route-deferred [:component/id ::main]
                                    #(df/load app
                                              :all-recipes Recipe
                                              :target [:component/id ::main :all-recipes]
                                              :post-mutation `dr/target-ready
                                              :post-mutation-params
                                              {:target [:component/id ::main]})))}
  (tap> {:all-recipes all-recipes})

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

az20:05:52

that worked!

az20:05:02

I'm sorry I had a missing brace in the args

az20:05:09

I got a lot of reading to do :rolling_on_the_floor_laughing: I feel like I understand the high-level but really don't have a good grasp of the specifics

tony.kay20:05:14

That is the main hurdle to understanding I/O in Fulcro. Loads (and mutation return values) have to be targeted unless all you care about is the normalized data.

tony.kay20:05:03

When you load, you’ll always get the normalized data, as long as your resolver returns it with the right shape. But, if you want to join that loaded data into your UI graph you have to tell it “where”

tony.kay20:05:53

which is targeting. That’s really all there is to it

tony.kay20:05:17

so I could write a full-stack mutation like “gen-recipe” that returns a Recipe. I could then write a mutation with:

(defmutation gen-recipe [_]
  (remote [env] (m/returning env Recipe)))
and if I transact that (and the server actually returns a Recipe), then that recipe is now in my db. BUT it is not on the UI anywhere

az20:05:20

I see, I think the thing I'm struggling with is that I look at the local db, and I was thinking - that is the ultimate source of rendered truth, that somehow when that updates, all queries will always rerender. Am I misunderstanding though?

tony.kay20:05:52

yes, but the queries are relative to the component, not the root

tony.kay20:05:04

only the root component reads root keys

tony.kay20:05:19

you can include a link query if you want to read root keys, but I don’t recommend that in a case like this

az20:05:22

I see, so it's because this is a root key, we need to add the target

tony.kay20:05:44

It’s because your component’s key is NOT a root key

tony.kay20:05:50

it is a property of THAT component

tony.kay20:05:00

just because you named it the same doesn’t make magic happen

tony.kay20:05:04

Fulcro does almost no magic

tony.kay20:05:30

Open up component/id ::main in there

az20:05:31

that :all-recipes I thought this was a root key

tony.kay20:05:37

you’ll see there is an all-recipes in there as well now

az20:05:00

so this component is not actually using the :all-recipes data?

tony.kay20:05:03

df/load defaults to putting things in root, but componetns do NOT read from there

tony.kay20:05:18

I fyou reload your app

tony.kay20:05:24

you won’t see that root key anymore

tony.kay20:05:37

you’re seeing a prior load before you added targeting

tony.kay20:05:27

You can also do this: https://book.fulcrologic.com/#_link_queries but be sure to read the warnings (and use sparingly…this isn’t how it is intended to be done most of the time)

az20:05:35

I see it's gone now yes. So is this the idiomatic way of loading a master list like this?

az20:05:02

Should I try to read from :root/all-recipes instead?

tony.kay20:05:17

If you have some master list that should be shared all over the place, then putting it in root and using a link query is what you should do

az20:05:43

otherwise if I have a top level nav. Recipes | Settings | Account and recipes first displays a master list of all recipes, and then selecting one opens details. In this flow, would top level :root/all-recipes be the goal? Or targeting is better?

tony.kay20:05:45

:query [{[:root/all '_] (comp/get-query Recipe)}] but again be sure to read the warnings..you must have initial state or that won’t work

tony.kay20:05:10

the answer is “it depends”, of course 😄

az20:05:46

haha ok. I'm going to dig back into the docs and vids. Really appreciate the lesson Tony. Thanks again for the amazing work

tony.kay20:05:09

I personally would localize the data to a component. That would let me do things like play with pagination and incremetal loadings, etc.

az20:05:41

Excellent, this opened many questions, but I have a better sense now of what's happening

tony.kay20:05:07

The primitives are just that: primitive 🙂

az22:05:07

@U0522TWDA This is an excellent resource. Thank you for this.

🙏 2
tony.kay20:05:00

The general idea in Fulcro is that rendering is as close to a pure function of state as possible. I/O is always triggered by some kind of action (user event, explicit call on startup, timeout, etc.)

tony.kay20:05:35

I realize that because the router renders the route that this may be confusing: but rendering is just showing what you initialized the app to be.