Fork me on GitHub
#fulcro
<
2020-03-23
>
stuartrexking01:03:10

Is there an idiomatic way of formatting data from a resolver before it's updated in a specific instance of a UI element? Say my resolver returns a decimal 0.1, and I want to use this format in one instance of a defsc, but in a different instance of that same defsc I want to format it to be a percentage, say 10%. Is there a reasonable way to this or do I need different components? I could wrap the decimal defsc in a percentage defsc and do the formatting before passing it through. Thoughts?

tony.kay02:03:38

@stuartrexking I highly recommend that your data model be the real data. If it is a number, keep it a number in the db. Formatting should be in your UI code (i.e. a formatting function, like cl-format ). I see no need for a separate component per-se, unless you ahve some other reason for wrapping a single data point in a component. They seem orthogonal concerns to me. The defsc is about making a component. The query is about pulling raw data. The code in the render body is concerned with visualization.

stuartrexking02:03:45

@tony.kay Thanks for the response. Based on what you are saying, I either need to have logic in the defsc that determines the formatting (showing a % sign or not), or I need to do that on the server in the resolver.

stuartrexking02:03:18

And based on those two options, the former seems more appropriate.

tony.kay02:03:34

yeah, I would never put formatting in a resolver

tony.kay02:03:59

I think of attributes as talking about a particular fact in a native data format. For example, I use transit bigdecimals in RAD so that you have full accuracy on front and back end. Formatting is done at ui layer as late as possible. That way the fact is never “aliased”…you always know what you have.

tony.kay02:03:45

(in RAD I have a decimal ns that makes a numeric type that is implemented with big.js in cljs, so that bigdecimals work isomorphically in front/back-end)…numeric mismatches between front/back are particularly annoying.

stuartrexking02:03:19

Ok great. Thanks.

Chris O’Donnell03:03:48

I started a series of blog posts documenting how I've approached solving different problems when building an SPA with fulcro and pathom. Right now there's just an introductory post and a followup on client-side auth. I'm sharing them here in the hopes that they're helpful to others and not just future me. I haven't blogged before and wouldn't consider myself a fulcro expert; I'm open to any constructive feedback. You can find the posts at https://chrisodonnell.dev/.

👍 28
stuartrexking04:03:58

Great. Will keep an eye on it.

vinnyataide07:03:43

emacs users using clj-kondo. how you teach it not to freak out with fulcros macros like defmutation?

tony.kay16:03:47

NOTE: The macro just generates a defmethod. I can’t really change it for compatibility reasons, and it exists to help with navigation and easy use, but there is nothing saying you couldn’t generate a similar macro or just write the defmethod yourself. Each section just becomes and output entry in a map with a key that matches the section name. I’ve thought of various tricks, but Cursive added support for the macro years ago, so it’s always been smooth sailing for those users (like me). You could make a macro that still helps some, but that looks more like a defn returning a map.

vinnyataide07:03:11

I tried looking at kondo's config.edn but found nothing about fulcro

borkdude07:03:51

@vinnyataide take a look at the config docs at the clj-kondo repo. You can use :lint-as or configure the :unresolved-symbol linter

vinnyataide07:03:54

@borkdude thanks, I'm looking at it now

vinnyataide07:03:02

I think in this case since fulcro's defmutation has no close candidate to lint as I'll simply put on unresolved

vinnyataide07:03:30

I would like to contribute if it could've possible to make clj-kondo understand fulcro's macros so it could help people catch errors quickly

borkdude07:03:46

@vinnyataide Sure. You can make an issue for it first. I could give you some pointers where to start. There is built in support for compojure for example

vinnyataide07:03:03

@borkdude that's great to know

vinnyataide07:03:15

I'll take a look at compojure's

borkdude07:03:17

Note that you can also use def-catch-all

vinnyataide08:03:38

thats cool too

vinnyataide08:03:11

it worked better with def-catch-all since all mutation applications were ignored too

stuartrexking11:03:31

I have a key at the root of the database :a. I use initial-state in my root container to set the value of this key to a comp/get-initial-state. I call load! to retrieve the remote data. When the remote data is returned the ident value is passed into the sub component [:a/id 1], not the realised value {:a/id 1 a:/name "Stu} What am I missing here.

Chris O’Donnell11:03:17

If you're seeing the ident in your app state, that is the expected behavior. When you ask for data from that entity in a component's query, fulcro will denormalize that data and pass the entity data (not the ident) to your component.

stuartrexking11:03:36

It's passing the ident though.

Chris O’Donnell11:03:51

What do your component queries and idents look like?

stuartrexking11:03:29

Let me grab it.

stuartrexking11:03:28

(ns ventures.fierce.c19.ui
  (:require
    [cognitect.transit :as ct]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.algorithms.react-interop :as interop]
    ["victory" :refer [VictoryChart VictoryAxis VictoryTheme VictoryLine]]))

(defsc SelectContainer [_ {:select-container/keys [label selected values]}]
  {:ident :select-container/id
   :query [:select-container/id
           :select-container/label
           :select-container/selected
           :select-container/values]
   :initial-state
          {:select-container/id       :params/id
           :select-container/label    :params/label
           :select-container/selected :params/selected
           :select-container/values   :params/values}}
  (dom/div :.select-container
           (dom/label label)
           (dom/div :.select
                    (dom/div :.selected selected)
                    (dom/div :.dropdown
                             (dom/ul
                               (map #(dom/li {:key %} (dom/a %)) values))))))

(def ui-select-container (comp/factory SelectContainer {:keyfn :select-container/id}))

(defsc Header [_ {:keys [countries]}]
  {:query [:countries (comp/get-query SelectContainer)]
   :initial-state
          (fn [_]
            {:countries (comp/get-initial-state SelectContainer {:id 1 :label "Country" :selected "Loading..." :values [""]})})}
  (dom/header
    (dom/div :.title
             (dom/h1 "HELLO")
             (dom/h2 (dom/a {:href "#countries"})))         ;selected-country)))
    (dom/nav
      (println "countries: " countries)
      (ui-select-container countries))))

(def ui-header (comp/factory Header))

(defsc Root [this props]
  :initial-state (fn [_] (comp/get-initial-state Header))
  (dom/div
    (ui-header props)))

stuartrexking11:03:33

That's the entire UI.

stuartrexking11:03:22

countries in Header is [:select-container 1]

stuartrexking11:03:59

There is only 1 header but could be multiple select-containers (at the root level)

Chris O’Donnell11:03:00

Your Header query doesn't look quite right. When you want to compose a child component's query, you should use a join like so: [{:countries (comp/get-query SelectContainer)}].

Jakub Holý (HolyJak)17:03:09

BTW you should get an error for this query in the latest Fulcro

Chris O’Donnell12:03:54

Yeah it wouldn't even compile for me. Nice sanity check to have! 🙂

stuartrexking11:03:05

Changing that query makes no difference.

stuartrexking11:03:14

The only other thing I'm doing is calling

(df/load! app :countries ui/SelectContainer)

stuartrexking11:03:19

From my app init function.

Chris O’Donnell11:03:52

Your root component doesn't have initial state; it needs to be in a map like {:initial-state (fn [_] (comp/get-initial-state Header)}. Also, your root component needs a query. Queries and initial state are always composed from the root, so if your root component doesn't have initial state or a query, neither will work for child components. I updated your root component to look like:

(defsc Root [this {:root/keys [header]}]
  {:initial-state {:root/header {}}
   :query [{:root/header (comp/get-query Header)}]}
  (dom/div
    (ui-header header)))
(And also your SelectComponent's initial state values should have the namespace param rather than params.) With those two changes I can see application state being initialized as I would expect.

Chris O’Donnell11:03:43

Are you using fulcro inspect? If not, I would highly recommend it.

Chris O’Donnell11:03:55

Makes it much easier to see what's going on with your application state.

stuartrexking11:03:44

I am using it, and it was working fine. I refactored some stuff and seems to have broken everything. I'll add in your suggestions now. I also noticed that my version of Root didn't have :initial-query in a map.

Chris O’Donnell12:03:30

Edited my root component above to be correct; the initial state was messed up.

stuartrexking14:03:18

What's the best way to exclude initial-state values from a load! query? Say I'm setting a label value in my initial-state, I don't want to have to query the server for that label key. I can use :without in load! config. Is that the best way to do it?

Chris O’Donnell15:03:39

Any keywords with the namespace ui (eg. :ui/label) won't be fetched from your remote, as well. That's what I usually do.

tony.kay16:03:52

@stuartrexking I generally also make a “blacklist” of keys that should never go on the network, and then add a global EQL transform. See this code in the RAD library: https://github.com/fulcrologic/fulcro-rad/blob/develop/src/main/com/fulcrologic/rad/application.cljc#L90

❤️ 4
tony.kay16:03:27

so, you can invent your own naming pattern and make that easier in some ways, but the blacklist is really easy to update as you go (and see bad things hit the net)

tony.kay16:03:49

The naming convention of “has a ui namespace” is fine for component-local concerns, but it can also be nice to add a rule like “elide it if it contains .local. in the namespace of the key” or something.

cjsauer21:03:10

Is it common in Fulcro to pass an ident to a server resolver simply for it to be passed back to ease normalization? Say for example I have a component called SearchResult. It doesn’t really have a natural :ident, so I invent one like [:ui/id :root/search], but now I’m realizing that my load! calls are having trouble normalizing. Perhaps a post mutation would be better here…

cjsauer21:03:42

More generally, what is the idiomatic way to handle components that don’t have a readily available :ident? I’ve seen examples of passing params into comp/get-initial-state so that the parent can give the component a unique name, and that seems good. I’m struggling to understand how to get that setup working with load!

currentoor21:03:17

@cjsauer nothing wrong with using static idents like so

(comp/defsc CurrentOrder [_ _]
  {:ident       :order/id
   :query       [:order/id
                 {:order/line-items (comp/get-query LineItem)}
                 {:order/payments (comp/get-query Payment)}
                 fs/form-config-join]
   :form-fields #{:order/line-items :order/payments}})
(comp/defsc CurrentSale [_ _]
  {:ident (fn [] [:COMPONENT/by-id ::current-sale])
   :query [{:current-order (comp/get-query CurrentOrder)} :payment-in-progress?]})
where CurrentSale is a client only component, and CurrentOrder is the thing that gets loaded from the server

currentoor21:03:54

you should never be sending queries to the server that the server isn’t actually supposed to fulfill

currentoor21:03:55

so we only call load! with CurrentOrder

currentoor21:03:37

now if CurrentOrder has keys that don’t make sense to the server we can remove them in `

:global-eql-transform

cjsauer21:03:37

@currentoor ty for the reply. Above makes sense, but what if instead of CurrentOrder we’re trying to load something that doesn’t have a natural ident? Say the :ident is computed from initial state parameters. In this case, the load doesn’t have the proper information to denormalize. I’m thinking now that pre-merge might be useful in this case, because I can merge the newly loaded data with the computed ident…otherwise it seems to get clobbered.

currentoor21:03:18

yeah that seems reasonable

cjsauer21:03:47

The only example I can think of right now is something like SearchResult. It doesn’t really have an ID, but “load” feels semantically correct.

currentoor21:03:06

i try to avoid that situation but it does come up from time to time

currentoor21:03:39

i think tony recently implemented a searchable auto-complete input in RAD

currentoor21:03:43

and it had the same problem

cjsauer21:03:29

Ah interesting. Initially I was trying to :target the load to the proper component, but that leads to clobbering. I wonder if something like (targeting/merge [:component/by-id :search-results]) would be useful?

currentoor21:03:25

not sure, been a while since i’ve messed with that, you’d have to read the docs and try it

currentoor21:03:06

hmm i can’t find the source for the searchable input @tony.kay built recently, maybe he can chime in when he’s got a moment

currentoor21:03:02

check that out

cjsauer21:03:57

targeting/merge would be something new, I don’t think it exists. Ah cool, I’ll take a look, ty

tony.kay21:03:59

right, that…but that is a generalized complex component, which may be hard to understand

tony.kay21:03:44

That one requires the new floating root stuff

tony.kay21:03:41

The idea with that one is the following: • I have an arbitrary number of form fields on a form, each one is an autocomplete (needs to load/normalize some data for that) • BUT, each form field is stateless, so there is noplace to load the (many) different autocomplete lists

tony.kay21:03:32

There are several ways to tackle that 1. Make a top-level table, like :autocomplete-options, and assign each autocomplete list some kind of ID, and target that to that table. Then include the table in the form’s query as a link query. I do that with the picker in RAD. In this case the form fields are not stateful defsc with queries…you’re just making a normalized cache in a top-level table. 2. Give each field a lifecycle outside of React…i.e. make a state machine that, when the form comes on-screen, creates in-app IDs and state for each of these “stateful” things, so that each field becomes a stateful component with a query/ident. 3. Do something fancy like the floating roots thing…not sure if I love it yet 😜 Just invented that one two days ago 4. Do something really nasty and run an http get and put the results in component-local state. While ugly, this is the “normal” way in stock js/react. Latest Fulcro releases support hooks, so perhaps you can make it a bit prettier with that 🙂

cjsauer21:03:40

Aha okay. I see with option 1 you’re using a post-mutation and closing over the id of the autocomplete field. That’s what I was missing. https://github.com/fulcrologic/fulcro-rad/blob/51b6b5e16d557c3618a4e69c380450ce48a5506d/src/main/com/fulcrologic/rad/rendering/semantic_ui/autocomplete.cljc#L50-L52

tony.kay21:03:00

that’s option 3…which is the complex floating root one

tony.kay21:03:20

but yes, some kind of ID is going to be needed to track which autocomplete values you want

tony.kay21:03:40

could be as simple as the keyword name of the field in question

tony.kay21:03:21

approach (1) is probably the easiest to do:

(defsc AutocompleteOptions [_ _] {:query [:text :value]})

(defsc Form [this props]
  {:query [:local/thing [:autocomplete-table '_]]
   ...}
  (let [cache-key (something-that-generates-stable-cache-key)]
    (div
      (input {:onChange #(df/load! this :some-autocomplete-key AutocompleteOptions {:params {} :target [:autocomplete-table cache-key})})

tony.kay21:03:23

something like that

cjsauer22:03:08

Cool, ty. Let me try to wire this up using a post-mutation. Curious tho, would it work to :target the component that has a computed ident, and then use a :pre-merge hook on that component so that the incoming data gets merged rather than clobbered? Not sure if that question makes sense, I’ve never used pre-merge before.

cjsauer22:03:38

Suppose I can just try it 😛

cjsauer22:03:07

Got it working! Thank you both for the help 🙏

👍 4