This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2018-08-26
Channels
- # announcements (5)
- # architecture (1)
- # bangalore-clj (4)
- # beginners (45)
- # boot (4)
- # cider (19)
- # clojure (56)
- # clojure-austin (1)
- # clojure-canada (1)
- # clojure-finland (1)
- # clojure-russia (67)
- # clojure-uk (2)
- # clojurescript (57)
- # clojutre (1)
- # cursive (15)
- # datomic (3)
- # emacs (2)
- # figwheel-main (71)
- # fulcro (117)
- # hoplon (33)
- # java (5)
- # off-topic (52)
- # pedestal (7)
- # remote-jobs (1)
- # shadow-cljs (134)
- # slack-help (9)
- # specter (1)
- # tools-deps (17)
- # vim (2)
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
I could’ve sworn there was an end-of-life notice on the readme a few days ago 😛
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 🙂
Awesome! I thoroughly enjoyed working with fulcro-spec. Glad I don’t have to port things anymore.
I host my Fulcro backend using an Ion, but I opted to use Pathom as my parser instead of the included parser.
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 functionIt 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.
Where does value
come from @cjmurphy?
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 ?? Really it could be called anything.:value
key
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?
As you said, it is essentially returning {:value (do ~@value-body)}
Just a bit shorter. You can use without. With some forms examples you can see without.
Using defquery-root
is is speccing it with
(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))))
looks like it has to be a (value ...)
type of expression
What confused me is why choose (value [...] ( ... ))
over (fn [...] ( ... ))
?
(it might be a silly question, I’ve just started playing around with clojure :))
Perhaps there are other parts to defquery-root
, in which case fn
would not be suitable.
Interested in the design decisions around this, looks like it is meant as part of a DSL
On the server, if I want the request to be injected into env
, is there a particular handler that will do this?
@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.
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.
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.
whats the general best-practice for side-effects in transactions? sending a WS message in my case?
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
@thheller So, I wouldn’t side-effect in the swap, but the action is run once and only once
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.
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 😛
(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)))))
The defrouter
is about giving you a way to both optimize your query and switch among things
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)
but I had to do the update-in screen-ident
myself since set-route*
doesn't seem to do that
so I think you might be using it differently than intended. You should not need to manipulate the internals like that
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?
(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)
(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)}
]}
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
(-> state
(prim/merge-component BuildOverview tree-of-build-data)
(fr/set-route* ...))
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
but I have no build data. or rather its in another key that I don't want to look up?
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}})
well, in that case only the set-route*
should be necessary, because all of that data should already be normalized
the fact that you’re having to write some of the important data tells me your load isn’t loading everything the component needs
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
Does a build have a natural ID? If so, why aren’t you using that? Why did you invent :router/id
?
because the docs said "The ident generator for the components and router must all work the same. "
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)?
(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))))))
I dumped everything in there but that will ultimately be split into multiple components
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
...)
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
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
sorry…one of my YouTube videos shows how to do this by hand…the routing namespace just makes it much less to type
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)
It also doesn’t care what you call the ident function….`fn`, ident
, yo-momma
…I recommend fn
so IDEs like it better
@thheller can you show an example implementation of tx/build-...
?