Fork me on GitHub
#fulcro
<
2018-08-26
>
tony.kay01:08:10

Fulcro-spec 2.2.1 is on clojars. The README has been updated to better reflect “why” and has some better usage examples. The new version of the library includes clojure.spec checking version of the mocking utilities provided! and when-mocking!. https://github.com/fulcrologic/fulcro-spec

8
levitanong04:08:45

I could’ve sworn there was an end-of-life notice on the readme a few days ago 😛

tony.kay06:08:21

There was…I changed my mind. I thought workspaces was going to be an adequate replacement for the rendering, but I’ve since re-evaluated. I’m personally dissatisfied with the general state of cljs testing support, and I prefer the rendering of diffs and such for both the clj and cljs renderers of fulcro-spec. So, I plan to continue to use it, even though I’d prefer to have fewer things to maintain 🙂

levitanong06:08:16

Awesome! I thoroughly enjoyed working with fulcro-spec. Glad I don’t have to port things anymore.

eoliphant15:08:04

anyone used the fulcro server as a datomic ion?

oscar02:08:50

I host my Fulcro backend using an Ion, but I opted to use Pathom as my parser instead of the included parser.

pvillegas1216:08:57

Can someone explain how does

(defquery-root :my-friends
  "Queries for friends and returns them to the client"
  (value [{:keys [query]} params]
         (get-people :friend query)))
work? Specifically, the part I don’t understand is the expression with value as a function

cjmurphy16:08:58

It is like a function call on the server when the client issued a load, The first parameter is 'env' and is a map with lots of useful things you might have injected, or are always there. So 'query' is always there, but 'your-database' might be something you injected. params is just the parameters the client sends across.

pvillegas1216:08:46

Where does value come from @cjmurphy?

cjmurphy16:08:04

value is like the name of the function call. Comes from the Om Next stuff that is underlying all this, where a map is returned with a :value key?? Really it could be called anything.

pvillegas1216:08:42

Looking at the macro definition of the multimethod, I guess my question is a design question, why make a DSL instead of a regular anonymous function?

pvillegas1216:08:36

As you said, it is essentially returning {:value (do ~@value-body)}

cjmurphy16:08:11

Just a bit shorter. You can use without. With some forms examples you can see without.

pvillegas1216:08:41

Using defquery-root is is speccing it with

pvillegas1216:08:43

(s/def ::root-value (s/cat
                      :value-name (fn [sym] (= sym 'value))
                      :value-args (fn [a] (and (vector? a) (= 2 (count a))))
                      :value-body (s/+ (constantly true))))

pvillegas1216:08:55

looks like it has to be a (value ...) type of expression

pvillegas1217:08:15

What confused me is why choose (value [...] ( ... )) over (fn [...] ( ... ))?

cjmurphy17:08:35

Oh I see, good point.

pvillegas1217:08:02

(it might be a silly question, I’ve just started playing around with clojure :))

cjmurphy17:08:03

Perhaps there are other parts to defquery-root, in which case fn would not be suitable.

pvillegas1217:08:50

Interested in the design decisions around this, looks like it is meant as part of a DSL

cjmurphy17:08:46

On the server, if I want the request to be injected into env, is there a particular handler that will do this?

cjmurphy17:08:48

I see how to do it now, in wrap-api.

tony.kay18:08:16

@pvillegas12 The defquery macros are just meant to mimic the look and feel of the defmutation macros. Technically, the underlying parser requires you write a function that returns a map which contains certain keys (e.g. :value). Much older versions essentially had you do that. The problem was that people would forget to return a map, and the difference between how you handled a query that started with an ident (e.g. for an entity) and a keyword (root-like) also had some boilerplate that was repeated over and over. The macro gives you a DRY way to specify the parts, along with some syntax checking and boilerplate elimination. The name value was chosen because that is the name of the key in the original map…so it’s kind of a lost legacy meaning that made sense at the time. These days, apps of any size should consider using something like pathom for their server parser, which is making the query macros obsolete.

tony.kay18:08:07

You can still do it the old way, and use raw multimethods (or functions for that matter) in the parser…the query macros give an easier entry into basic apps where issuing load on a keyword or ident directly corresponds to an “endpoint” you can write on the server.

tony.kay18:08:23

They’re also a nice way to factor apart bits of your queries to different “sub-parsers”…e.g. you could define a pathom parser for one query starting point, and use something else (e.g. walkable) for a different one.

thheller19:08:20

whats the general best-practice for side-effects in transactions? sending a WS message in my case?

thheller19:08:19

in CLJ it would be a bad idea to do it in the action given that the swap! on the app-state could be retried

thheller19:08:30

but I guess its fine given that that never happens in JS?

tony.kay19:08:06

@thheller So, I wouldn’t side-effect in the swap, but the action is run once and only once

tony.kay19:08:26

the two are separate…Fulcro is not running a swap to run the action

tony.kay19:08:05

mutation -> one call of action -> action side effects (including running swaps)

thheller19:08:06

ah yeah right

tony.kay19:08:37

I haven’t said it very well in the docs, but you can also side effect outside of fulcro in any way that you see fit (though that can lead to bad design…but that’s up to you). For example, if you want to do some Xhr stuff and transact in a callback (or even swap against app state), you’re free to do it.

thheller19:08:00

I optimized the swap! call away since I got tired of the boilerplate arround it. totally forgot the fact that the swap! isn't actually required 😛

thheller19:08:57

(fl/add-mutation tx/select-build
  (fn [{::keys [active-build subscriptions] :as state} {:keys [ref] :as env} {:keys [build-id] :as params}]
    (let [screen-ident [:PAGE/build-overview build-id]]

      (when-not (contains? subscriptions build-id)
        (ws/send env {::api-ws/op ::api-ws/subscribe
                      ::api-ws/topic [::api-ws/worker-output build-id]}))

      (js/console.log ::tx-select-build active-build build-id env)

      (-> state
          (update ::subscriptions conj build-id)
          (assoc ::active-build build-id)
          ;; FIXME: this doesn't feel right
          (update-in screen-ident merge {::build [::m/build-by-id build-id]
                                         :router/id build-id
                                         :router/page :PAGE/build-overview})
          (froute/set-route* ::router screen-ident)))))

thheller19:08:29

it that how the router is supposed to be used?

thheller19:08:28

the ident of the router is very confusing 😛

tony.kay19:08:50

So, there are several combined concerns in “routing”

tony.kay19:08:11

The defrouter is about giving you a way to both optimize your query and switch among things

tony.kay19:08:41

really what you want is a union query component, and a parent to control what that thing points to…which is all defrouter does (it makes two components)

thheller19:08:03

yes I have that correct I think

thheller19:08:19

but I had to do the update-in screen-ident myself since set-route* doesn't seem to do that

tony.kay19:08:41

I’m confused about your screen-ident bit

thheller19:08:45

if I don't set the :router/id :router/page there it just said unknown screen

thheller19:08:07

basically I want one "page" per build

thheller19:08:31

so [:PAGE/build-overview :my-build] is the router ident for that

thheller19:08:51

If I don't do the merge I just get unknown screen

thheller19:08:11

its a bit of a mess right now

tony.kay19:08:30

so I think you might be using it differently than intended. You should not need to manipulate the internals like that

tony.kay19:08:00

So, one page per build: does each page have it’s own “table”, or are they all the same “kind” of thing with different IDs?

tony.kay19:08:18

i.e. does the first or second element of the ident change?

thheller19:08:33

(defsc BuildOverview [this {:keys [router/id router/page] :as props}]
  {:ident (fn [] [page id])

   :query
   [:router/id
    :router/page
    {::build (fp/get-query BuildItem)}]

   :initial-state
   {:router/id 1
    :router/page :PAGE/build-overview}}

  (ui-build-item (::build props)))

(defsc Dashboard [this {:keys [router/id router/page] :as props}]
  {:ident (fn [] [page id])

   :query
   [:router/id
    :router/page]

   :initial-state
   {:router/id 1
    :router/page :PAGE/dashboard}}

  (html/div "dashboard"))

(defrouter RootRouter ::router
  ; OR (fn [t p] [(:router/page p) (:db/id p)])
  [:router/page :router/id]
  :PAGE/dashboard Dashboard
  :PAGE/build-overview BuildOverview)

thheller19:08:59

its basically your router example

tony.kay19:08:13

ok…that looks ok so far.

tony.kay19:08:29

and you compose initial state into the parent of this router?

thheller19:08:50

(defsc Root [this {::keys [main-nav router] :as props}]
  {:initial-state
   (fn [p] {::router (fp/get-initial-state RootRouter {})
            ::main-nav (fp/get-initial-state MainNav {})
            ::env/ws-connected false})

   :query
   [:ui/react-key
    ::env/ws-connected
    {::router (fp/get-query RootRouter)}
    {::main-nav (fp/get-query MainNav)}
    ]}

tony.kay19:08:04

don’t need react-key

tony.kay19:08:12

went away a while ago 🙂

thheller19:08:19

copied from generated code 😉

tony.kay19:08:26

my bad, but FYI

tony.kay20:08:31

ok, so that looks ok. So, all that has to happen for a route to show, is for you to set the ident that ends up located in app state at [:fr/routers-by-id your-router-id ::ft/current-route] to the ident of the thing that should show. That’s all set-route* does

thheller20:08:13

yes I called (froute/set-route* state ::router [:PAGE/build-overview :my-build])

thheller20:08:22

but that just got me to "unknown screen"

tony.kay20:08:24

your initial state should have gotten the screen contents at start

tony.kay20:08:47

does your app state have those two screens at start?

thheller20:08:58

dashboard is shown on start

tony.kay20:08:26

when you look in Inspect, though…is the state for the other there?

thheller20:08:56

well I have :router/id 1

thheller20:08:01

so those both exist yes

thheller20:08:10

but not one for each build

tony.kay20:08:46

no, something at [:PAGE/build-overview build-id]

tony.kay20:08:56

(you started it out with one with id 1)

tony.kay20:08:51

oh, I see, so you’re populating the build as you go…yes, that does look right then

tony.kay20:08:04

you do have to put state for the router to point to, of course

tony.kay20:08:39

I would write it using merge-component instead, though

tony.kay20:08:29

(-> state
  (prim/merge-component BuildOverview tree-of-build-data)
  (fr/set-route* ...))

tony.kay20:08:51

and I’d probably make the build data using some kind of factory function, so I didn’t have to remember the keys for the map

thheller20:08:07

but I have no build data. or rather its in another key that I don't want to look up?

tony.kay20:08:16

merge-component will use the ident function of the component to figure out where to put it, and will normalize all of the child stuff as well. Typically, you’d be loading that from the server, so it’d be more like

(load this :build BuildOverview {:post-mutation `show-build :post-mutation-params {:build-id id}})

thheller20:08:31

its already loaded. I just want to select it.

tony.kay20:08:00

well, in that case only the set-route* should be necessary, because all of that data should already be normalized

tony.kay20:08:43

the fact that you’re having to write some of the important data tells me your load isn’t loading everything the component needs

tony.kay20:08:27

What’s in your state from the load? Is there a :router/id and :router/page coming from the server? If not, then you might want to consider what you are doing and change it a bit

tony.kay20:08:53

Does a build have a natural ID? If so, why aren’t you using that? Why did you invent :router/id?

thheller20:08:31

because the docs said "The ident generator for the components and router must all work the same. "

thheller20:08:46

the other "pages" of the router don't take a build id

tony.kay20:08:26

they must work in a consistent manner might be a better way of saying it. Sorry if the docs aren’t great 😕 OK, so tell me about a BuildOverview…what natural data does it have that we can use to figure out what it is (by looking at it)?

tony.kay20:08:50

e.g. does it have a :build/id prop?

tony.kay20:08:15

seems to me BuildItem might really be the thing of interest

thheller20:08:36

(defsc BuildItem [this {::m/keys [build-id build-config-raw worker-active] :as props}]
  {:initial-state (fn [p] p)
   :ident [::m/build-by-id ::m/build-id]
   :query [::m/build-config-raw
           ::m/build-id
           ::m/worker-active]}

  (s/build-item {}
    (s/build-title (name build-id))

    (s/build-section "Actions")
    (s/build-toolbar
      (if worker-active
        (s/build-actions
          (s/build-action {:onClick #(fp/transact! this [(tx/build-watch-compile {:build-id build-id})])} "force-compile")
          (s/build-action {:onClick #(fp/transact! this [(tx/build-watch-stop {:build-id build-id})])} "stop watch"))

        (s/build-actions
          (s/build-action {:onClick #(fp/transact! this [(tx/build-watch-start {:build-id build-id})])} "start watch")
          (s/build-action {:onClick #(fp/transact! this [(tx/build-compile {:build-id build-id})])} "compile")
          (s/build-action {:onClick #(fp/transact! this [(tx/build-release {:build-id build-id})])} "release")
          )))

    (s/build-section "Log")
    (s/build-log
      "yo")

    (s/build-section "Config")
    (s/build-config
      (html/pre
        (with-out-str
          (pprint build-config-raw))))))

thheller20:08:47

I dumped everything in there but that will ultimately be split into multiple components

tony.kay20:08:05

great: that is a thing I can easily route to

tony.kay20:08:40

So, I can look at that and generate a consistent ident just by looking at the data…so defrouter’s ident function could do the same:

(defrouter R ::r
  (fn [this props]  (if (contains? props ::m/build-id) [::m/build-by-id ::m/build-id] ...))
  ::m/build-by-id BuildItem
  ...)

tony.kay20:08:09

the ident function on the router has to generate idents that point to the thing you want to show, which means it may have to compute it

tony.kay20:08:12

that’s what I mean by consisrtent…the router has to output an ident that matches the ident of the component..however you do it is up to you

thheller20:08:25

ah. its always a vector in the examples. didn't know I could pass a fn

thheller20:08:32

entirely too much macro magic everywhere 😛

tony.kay20:08:57

sorry…one of my YouTube videos shows how to do this by hand…the routing namespace just makes it much less to type

tony.kay20:08:14

unions have a lot of repetition, and it’s easy to get wrong

tony.kay20:08:47

to be fair…the router you pasted to me shows an ident fn in the comment 🙂

thheller20:08:13

haha. I never noticed that. even as I'm writing the fn

tony.kay20:08:17

and my bad: takes two args

tony.kay20:08:29

this and props

tony.kay20:08:51

So, the keys in the router are just the left member of the resulting ident you “route to”…that picks the query and UI. The ID part ends up picking the “right row” in the table to hydrate the UI (via the query)

tony.kay20:08:57

It also doesn’t care what you call the ident function….`fn`, ident, yo-momma…I recommend fn so IDEs like it better

thheller20:08:06

I'll revisit this tomorrow. my brain is too fried to work out the other way now

thheller20:08:12

but I think I get it. thanks.

thheller20:08:41

btw I implemented the transactions-without-quoting thing as seen above

thheller20:08:45

I'll see how it plays out

thheller20:08:58

the quoting really really bothered my and I didn't want to look at it 🙂

pvillegas1222:08:57

@thheller can you show an example implementation of tx/build-...?