Fork me on GitHub
#fulcro
<
2021-06-28
>
genekim16:06:51

@holyjak Wow! Thank you so much for writing these Fulcro tutorials and exercises. I found them to be incredibly illuminating, and marvel at how I managed to write anything at all without understanding fully these concepts. I think the documentation and tutorials are superb! https://github.com/fulcro-community/fulcro-exercises (Can’t wait to resume work on them this afternoon — I just finished Task 2. 🙂

😻 8
👍 11
Jakub Holý (HolyJak)16:06:15

Thank you very much! It makes me happy to see it helps people.

jlmr17:06:06

Ok so I am quite stuck with a project in Fulcro. Probably I’m trying something to ambitious for my current skill with Fulcro, but hopefully someone here can help me out. Earlier @holyjak gave me some pointers but unfortunately I haven’t gotten any further. I’ll elaborate more in a thread.

jlmr17:06:03

Basically the user can define nested widgets, which I store server side in the database (crux). Right now these widgets look like this in the database:

{:crux.db/id      "some-long-id-for-this-widget"
 :ent/type        :widget
 :widget/type     :container
 :widget/opts     {:direction :vertical}
 :widget/children ["some-other-id-for-that-widget" "and-another-id-for-widget"]}
I have a Pathom resolver that looks like this:
(defresolver widget-resolver
  [{:keys [db]} {:widget/keys [id]}]
  {::pc/input #{:widget/id}
   ::pc/output [:widget/type
                :widget/opts
                {:widget/children [:widget/id]}]}
  (ffirst (crux/q db
                  '{:find [(pull ?widget [:widget/type
                                          (:widget/opts {:default {}})
                                          {:widget/children [(:crux.db/id {:as :widget/id})]}])]
                    :in [id]
                    :where [[?widget :crux.db/id id]
                            [?widget :ent/type :widget]]}
                  id)))
This will get a widget by id and output it in the form:
{:widget/id "some-long-id-for-this-widget"
 :widget/type :container
 :widget/opts {:direction :vertical}
 :widget/children [{:widget/id "some-other-id-for-that-widget"
 					:widget/id "and-another-id-for-widget"}]
 }
I’m stuck with how I make a “master” Widget component in Fulcro that renders the widget based on the type. Ideally each type would have its own Component that can be supplied with an :initial-state fn. Widgets can be recursive, so the widget with id "and-another-id-for-widget" could have other :widget/children as well and so on. Should I change the way widgets are stored in the database? Do I have to change the resolver? Earlier attempts led to all kinds of errors. I know I have to use recursive queries, and possibly unions as well, but I’m unsure how to combine the two. Any help is appreciated!

dvingo18:06:05

Have you looked into union queries for components? https://book.fulcrologic.com/#Unions

dvingo18:06:17

seems like the :widget/type would be your branch property

jlmr18:06:49

Ive looked at those but haven’t gotten my head around it yet. It seems like those work with the presence of keys in maps. Not values of a single specific key such as :widget/type

Jakub Holý (HolyJak)18:06:39

If all widgets have the same props then I would only have a single Widget component. However you could also make UI-only components for each of the types and manually use the right one based on the type: (defsc Widget [this props] {:query.., :ident :widget/id..} (case type :widget-a (ui-widget-a props)...) with stateless (defsc WidgetA [..] {} some-dom...)

jlmr18:06:20

The different widgets have the same props, but I might need different initial state and “ephemereal” state in the client. Is that possible as well with this approach @holyjak ?

Jakub Holý (HolyJak)19:06:33

Pre-merge might be a good solution for the "initial state" need though it's difficult to say without knowing why you need it. What do you mean by ephemeral state / what do you need it for? (m/set-field!..) would work just fine?

jlmr20:06:48

I think I don’t know enough about Fulcro yet ;-). But I was thinking about where to store the value of a text input.

Jakub Holý (HolyJak)20:06:11

Set-field would be a good solution I think. Did you go through https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/ and the associated exercises?

lgessler22:06:59

I have an entity with an ident, and the entity has an attribute that has a hiccup-like nested vector structure. if I want to use a fulcro component to render the nested structure, I need to give them all idents. where's the best place to do this? in pre-merge somehow, perhaps?

lgessler22:06:37

some more detail: it's not feasible to give each subvector an ident just based on the data it has alone, so i'd need to add a uuid or something

tony.kay02:06:24

Not sure what you mean. An attribute is allowed to hold any arbitrary EDN as data. So, it is perfectly legal to just put your hiccup-like thing in state, and query for it as a single attribute. Does that help, or doyou mean something else?

tony.kay02:06:03

(defsc MyDynamicThing [this {:thing/keys [hiccup]}]
 {:query [:thing/id :thing/hiccup]
  :ident :thing/id
  :initial-state {:thing/id 1 :thing/hiccup [:div "Hello"]}}
 (sablono.core/html hiccup))

tony.kay02:06:35

If you're instead trying to build an HTML-like model where each DOM component node is represented as a Fulcro component, then that is a bit more involved. You could generate something like this:

(declare ui-html-node)
(defmulti render-node :node/type)

(defmethod render-node :div [node]
  (dom/div
    (mapv ui-html-node (:node/children node))))

(defsc HTMLNode [this props]
  {:query (fn [] [:node/id :node/type :node/text {:node/children ...}])
   :ident :node/id}
  (render-node props))

(def ui-html-node (comp/factory HTMLNode {:keyfn :node/id}))
or something like that...basically the multi-method does the detail rendering for each node, but using the (single recursive) HTMLNode during recursion.

tony.kay02:06:41

I don't recommend this latter method, actually. I worked on a project that used that approach, and it became quite complex tracing down the data model, reloading the mess, etc. The RAD approach for dynamism is much more amenable I think. You can generate such components on-the-fly and then only customize the data model definition, instead of then entire UI.

lgessler04:06:54

ah sorry I wrote that sloppily--I meant the latter, where I make some TreeNode that will recursively call itself for the hiccup-y structure, pretty much what you have there, the only issue being that the nodes don't have :node/id after they've been freshly loaded from the server, and there's no easy way to construct one from just the data in the nodes

lgessler04:06:53

I guess my question is, if I used an approach like the one you outline, where the right place would be to assign a random ID (like a UUID) to every node. Perhaps a pre-merge function in HTMLNode that assigns a :node/id if it's not present

lgessler04:06:26

good to know RAD handles this more gracefully though--more reason for me to sit down and learn it

tony.kay19:06:44

RAD may not handle it more gracefully depending on how you define your problem. What I was trying to get across is that your problem definition may be the problem. Storing UI as a first-class concern in a data model is sometimes necessary (esp when you get marketers involved 😉 ), but it is always way more complicated, and is very often overkill. Having a well-described data model that you can add metadata to (such as rendering hints) is way more effective and less complex. For example in RAD the form layout is very limited in the built-in UI plugin...you can just say:

[[:address/street]
 [:address/city :address/state :address/zip]]
which makes street appear on line one, and the other 3 fields appear on line two of the form. It's super-easy to encode, but not very powerful....BUT, it is powerful enough for many many use-cases. This, compared to reifying all the diffs and CSS to accomplish the same responsive layout.

👍 2
lgessler20:07:11

Thanks for the patience and answers as always Tony, think I'll need to keep letting it stew what's best for my particular problem though it'll be easier now with your perspective