Fork me on GitHub
#fulcro
<
2019-12-11
>
tony.kay01:12:33

@fjolne.yngling I’ve pushed 3.0.13-SNAPSHOT, which should fix that bug

👍 4
tony.kay01:12:07

If you use websockets, you’ll also need to update to 3.0.5-SNAPSHOT

tony.kay01:12:13

testing…but will have formal release soon

defun02:12:13

Hi there! I use the version 3.0.12. There is a strange something about the state machine's handler, it seems that uism/assoc-aliased won't work before the dr/change-route. the codes like this: :event/goto-club-page {::uism/handler (fn [{::uism/keys [event-data] :as env}] (uism/assoc-aliased env :selected-club (:club/id event-data)) ;; it doesn't work. (dr/change-route SPA ["main" "currentClubPage"]) )} :event/goto-club-page {::uism/handler (fn [{::uism/keys [event-data] :as env}] (dr/change-route SPA ["main" "currentClubPage"]) (uism/assoc-aliased env :selected-club (:club/id event-data)) ;; it works. )} Have anyone know why is it?

jaihindhreddy05:12:41

Just a slack tip: You can enclose multi-line code snippets in triple backticks.

tony.kay03:12:00

You have to thread the env through, and return the new env

tony.kay03:12:04

there are no side effects in UISM functions

tony.kay03:12:45

in other words: You must return updated env

defun06:12:33

@tony.kay it's ok, thanks for your help!

fjolne08:12:37

@tony.kay thanks a lot, it’s working as expected now, pre-merge made the app’s logic much cleaner

Robin Jakobsson10:12:18

coming from React/ES6-land: is there any equivalent of PropTypes declarations in Fulcro (for requiring certain props)?

tony.kay12:12:54

@jakobssonrobin Clojure spec is your friend. As you learn more about Clojure and the general approach to name-spaced keywords (which stems from RDF) you’ll find that hard-core schema hurts you more than it helps. You’re also in a language where new “syntax” is just a macro away…so, the short answer is “not exactly”, and the longer answer is “sort-of, but a bit different”, and the more involved answer is “you can easily get something better” (for example the options map on components is an open map, which means you can put your own nsed declarations (e.g. about prop specs) and then a simple wrapper macro could add in devel-time checking.

Robin Jakobsson12:12:39

Thanks, I will have a look at spec 🙂 . And I’ll give nsed declarations a thought.

tony.kay12:12:58

Fulcro 3.0.13 and Fulcro Websockets 3.0.5 released. These fix some internal issues that were affecting correct operation of :pre-merge and load normalization.

👍 36
fjolne19:12:33

Is there a concise way to mark form fields as complete and then immediately check validity? Validation check requires the denormalized entity tree, and props aren’t updating that fast if I’m transacting fs/mark-complete! and then pass props to the validator. I’ve got 2 solutions: to manually denormalize required component from normalized state in mark-complete transaction and to use js/setTimeout. Both seem quirky...

tony.kay01:12:44

You mean within a mutation, right? I should really add a convenience version of the function for that. You can denormalize things with com.fulcrologic.fulcro.algorithms.denormalize/db->tree and use the existing function.

tony.kay01:12:50

or you could look at the implementation and write the helper that works on normalized state.

tony.kay01:12:21

Basically you define what it means for a field to be complete, and the ::fs/complete? entry in the form config tracks completeness…so you can always figure it out in a mutation from base data that you have easy access to. But if you’re trying to use a generated validator from make-validator, then using db->tree is probably easiest.

tony.kay01:12:36

There is a form-state helper function called update-forms that walks over your (potentially) nested for via normalized state. You could combine that with an atom in a helper function to build something that could directly walk normalized state and use your same predicate you gave to make-validator . I supplied update-forms as a helper for building whatever you might need that works across form sets.

fjolne05:12:53

Thank you for the ideas, update-forms sounds promising. But I’m using spec validation, and there’s another caveat: I must check validity before calling dirty-fields, because it is instrumented (?) and fails on my specs. So I can’t just pass the dirty diff to the mutation which would mark the form complete, check for validity and submit — spec fails before that. So my implementation with db->tree within a mutation failed and I went with js/setTimeout.

fjolne05:12:45

To the first question, I meant within a mutation + in the same transaction (in case we’re using fs/mark-complete!, so that mutations would execute in the right order, but now I come to think about and it seems to not really matter, as optimistic transactions’ actions are executed asap, right?)

fjolne06:12:31

That’s kinda funny how I ended up implementing a similar transactional mechanics in re-frame for past projects, before learning about Fulcro. Thought I was doing something wrong cuz I haven’t found anything like that for re-frame back then. Good to know it wasn’t such a misconception on my side. Better though to finally switch to a battle-tested implementation;)

tony.kay15:12:01

So, you are using instrumented specs to throw on validation? That seems very off to me. There’s nothing wrong with using specs, and there are API calls to evaluate those when/where you need to. Am I missing something?

fjolne16:12:46

No, I’m using specs only for fs/get-spec-validity, I’m not instrumenting anything. But Fulcro somehow throws on calls to fs/dirty-fields, when my fields aren’t valid.

thosmos20:12:41

I’m getting an error when I try to load an ident-backed router-target detail comp using a deferred load triggered from the :will-enter prop on the detail comp:

Storing :target -> [:sitevisit/id "17592186213031"] on :riverdb.ui.sitevisits-page/SVRouter
Doing merge and targeting steps:  {[:sitevisit/id "17592186213031"] {:sitevisit/id "17592186213031", :db/id "17592186213031"}} 
Doing post mutation  com.fulcrologic.fulcro.routing.dynamic-routing/target-ready
ERROR  -  dr/target-ready! was called but there was no router waiting for the target listed:  [:sitevisit/id "17592186213031"] This could mean you sent one ident, and indicated ready on another. 

tony.kay01:12:30

You used route-deferred ?

tony.kay01:12:45

any particular reason to use strings for IDs? Seems strange (but harmless as long as they match). Returning route-deferred on will-enter tells the router state machine to expect a future call to target-ready. The router then stores that, and when the call comes, it looks it up. If they don’t match it issues this error.

tony.kay01:12:31

If you used route-immediate or didn’t match the idents, then you’ll see the error…if you did the former it will appear to still work, since you already approved the route.

fjolne16:12:57

@U07KXN95L I’ve got the same issue, turned out you have to pass initialized props to the router (e.g. initialized via component’s :initial-state), otherwise the router won’t have an id.

thosmos16:12:58

@fjolne.yngling ah yes I wondered about that but I didn’t figure out how to do it. I’ll look into it. @tony.kay yes I wound up switching to a route-immediate instead. I use strings for keys because they’re Datomic IDs which are longs and which javascript can’t handle in the higher value range. This was my original will-enter and as you can see I’m literally using the same ident for the route call and the target-ready call (straight out of the example in the guide)

:will-enter    (fn [app {:keys [sv-id] :as params}]
                    (log/debug "WILL ENTER" sv-id params)
                    (dr/route-deferred [:sitevisit/id sv-id]
                      #(f/load app [:sitevisit/id sv-id] SiteVisitEdit 
                         {:post-mutation        `dr/target-ready
                          :post-mutation-params {:target [:sitevisit/id sv-id]}})))

tony.kay16:12:09

mmm….initial state is generated by defrouter

tony.kay16:12:54

@fjolne.yngling what did you do to work around?

tony.kay16:12:21

specifically…you passed your own :initial-state to defrouter? Or used initial state in the children (which is something you should do)

tony.kay16:12:14

If we can confirm a bug, I’ll look into it. I personally don’t use the deferred support much. Turned out to be less useful than I thought, since I usually would rather give the user the feedback of routing first, and then show a loader.

fjolne16:12:12

(defrouter YourComponentRouter [this props]
  {:router-targets [YourTarget1 YourTarget2]})

(def ui-your-component-router (comp/factory YourComponentRouter))

(defsc YourComponent [this {:your/keys [router]}]
  {:query         [{:your/router (comp/get-query YourComponentRouter)}]
   :ident         (fn [] [:component/id :your-component])
   :route-segment ["your" "segment"]
   :initial-state {:your/router {}}}
  (ui-your-component-router router))

fjolne16:12:42

So, I’ve just done what I should have done, initialized router’s props.

tony.kay16:12:24

Right. Exactly. But I guess there could be a bug for deferred routing to something you are loading (which won’t be in state yet).

thosmos16:12:41

so this is bringing up something which is that I guess I’ve been assuming that :initial-state is optional, but it seems necessary in several cases

thosmos16:12:05

but maybe that’s a bug

tony.kay16:12:06

So, initial state is required when you want that part of the graph to pre-exist in the db at app start…simple as that

thosmos16:12:21

I’ll see if I can replicate the error and will let you know

tony.kay16:12:26

for a static route like his example, you definitely want it

tony.kay16:12:42

for a dynamic example there’s nothing to be done

thosmos16:12:02

so the router will take care of initializing state even for all of its route targets?

tony.kay16:12:08

but I might have some kind of initialization logic error that won’t detect the route target if it has no initial state

thosmos16:12:08

I think I had initial-state in the route target component, but i didn’t initialize the router itself

tony.kay16:12:10

The macro will compose in initial state for all declared targets. Always

tony.kay16:12:21

no no, you can’t set initial state in router

tony.kay16:12:47

but if a declared target is dynamically loaded, there won’t be anything there

tony.kay16:12:13

the logic should be ok with that…but it could be a case I missed

thosmos16:12:29

right, like editing a new entity that doesn’t exit yet

tony.kay16:12:30

( I might have given an empty map for initial state in dyn child)

thosmos16:12:12

ok I just switched back to route-deferred and got the error again. I do have :initial-state on the route target, and this is even routing to something that already has data in the DB under its ident

tony.kay16:12:31

If you look in fulcro inspect db, and slide back in time while watching the state of the ::dr/asm-id of your router, perhaps that’ll give you a clue

thosmos16:12:42

and I am giving the router initial-state as @fjolne.yngling suggested

tony.kay16:12:01

It is an error to give the defrouter initial state…don’t do that

tony.kay16:12:21

or a query/ident…those should generate compile errors

tony.kay16:12:07

but I don’t have that logic in place…it will be ignored if you supply it

tony.kay16:12:12

the macro overwrites it

thosmos17:12:00

no it wasn’t given in the defrouter call, that’s just: `

(dr/defrouter SVRouter [this props]
  {:router-targets [SiteVisitList SiteVisitEdit]})

thosmos17:12:29

It was in the comp that is hosting the router

thosmos17:12:44

but with or without it’s the same

tony.kay17:12:47

which looks through the routers in state trying to find one that is waiting on the ident you specified

tony.kay17:12:00

I don’t see how initial state could break that…

tony.kay17:12:49

the ident is either there or it isn’t (and it gets put there when you do deferred return value)…and it either matches on = or it doesn’t…which is also up to the values you use

tony.kay17:12:56

so strings vs int matter

tony.kay17:12:06

you sure your ident really matches?

thosmos17:12:36

the ident matches because the route-immediate works using the same ident, and in the route-deferred I’m literally using exactly the same ident for the route and for the target-ready

thosmos17:12:02

I’ll look into that code for the router-for-pending-target

thosmos17:12:49

FWIW here’s the console log. There is a render of the page that hosts the router. would that make a difference?

DEBUG [com.fulcrologic.fulcro.ui-state-machines:123] -  Triggering :route! on :riverdb.ui.sitevisits-page/SVRouter with {:error-timeout 5000, :deferred-timeout 100, :path-segment ["edit" "17592186213031"], :router [:com.fulcrologic.fulcro.routing.dynamic-routing/id :riverdb.ui.sitevisits-page/SVRouter]meta, :target [:sitevisit/id "17592186213031"]meta}
DEBUG [com.fulcrologic.fulcro.ui-state-machines:?] -  Storing :pending-path-segment -> ["edit" "17592186213031"] on :riverdb.ui.sitevisits-page/SVRouter
DEBUG [com.fulcrologic.fulcro.ui-state-machines:?] -  Applying mutation helper to state of :riverdb.ui.sitevisits-page/SVRouter
DEBUG [com.fulcrologic.fulcro.ui-state-machines:?] -  Activating state  :deferred on :riverdb.ui.sitevisits-page/SVRouter
DEBUG [com.fulcrologic.fulcro.ui-state-machines:?] -  Storing :target -> [:sitevisit/id "17592186213031"]meta on :riverdb.ui.sitevisits-page/SVRouter
DEBUG [com.fulcrologic.fulcro.ui-state-machines:?] -  Setting timeout :error-timer on :riverdb.ui.sitevisits-page/SVRouter to send :timeout! in 5000 ms
DEBUG [com.fulcrologic.fulcro.ui-state-machines:?] -  Setting timeout :delay-timer on :riverdb.ui.sitevisits-page/SVRouter to send :waiting! in 100 ms
DEBUG [com.fulcrologic.fulcro.algorithms.indexing:58] -  (Re)indexing application query for prop->classes
INFO [com.fulcrologic.fulcro.networking.http-remote:321] -  merge-edn => [{[…] […]meta}]
DEBUG [com.fulcrologic.fulcro.ui-state-machines:?] -  TIMEOUT on :riverdb.ui.sitevisits-page/SVRouter due to timer :delay-timer after 100 ms
DEBUG [com.fulcrologic.fulcro.ui-state-machines:123] -  Triggering :waiting! on :riverdb.ui.sitevisits-page/SVRouter with {}
DEBUG [com.fulcrologic.fulcro.ui-state-machines:?] -  Activating state  :pending on :riverdb.ui.sitevisits-page/SVRouter
RENDER SiteVisitsPage [list]
XHR finished loading: POST "".
DEBUG [com.fulcrologic.fulcro.data-fetch:154] -  Doing merge and targeting steps:  {[:sitevisit/id "17592186213031"] {}} [{[…] […]meta}]
DEBUG [com.fulcrologic.fulcro.data-fetch:159] -  Doing post mutation  com.fulcrologic.fulcro.routing.dynamic-routing/target-ready
ERROR [com.fulcrologic.fulcro.routing.dynamic-routing:110] -  dr/target-ready! was called but there was no router waiting for the target listed:  [:sitevisit/id "17592186213031"] This could mean you sent one ident, and indicated ready on another.

tony.kay17:12:54

Hm…looks sane

tony.kay17:12:15

again, use Inspect to examine the router’s state history…that pending stuff is saved on the router itself

tony.kay17:12:01

That error indicates that no router had that target stored in a pending route state

thosmos17:12:14

would that pending router state survive if the comp that is hosting it rerenders?

thosmos17:12:03

time-traveling …

tony.kay17:12:46

render has nothing to do with modifying state

tony.kay17:12:00

this isn’t component-local state affected by mounts

thosmos17:12:51

the first frame where the router changed to :failed

thosmos17:12:03

hmm, something else is wrong though, there’s now no :db/id in that record …

tony.kay17:12:42

the router pending route has the ident you indicated, though.

tony.kay17:12:55

so not sure why it would not be finding it

thosmos20:12:45

OK, I’ve tried all of the different variations (and fixed the bugs I introduced since my original message that interfered with the investigation) and I can now definitively agree with @fjolne.yngling that getting the router’s initial state into the comp that hosts it is essential like so:

(defsc SiteVisitsPage [this {:keys [svrouter] :as props}]
  {:initial-state (fn [params]
                    {:svrouter        (om/initial-state SVRouter {})
I had it partially working by only doing `
:svrouter {}

tony.kay20:12:42

Ah, yes, you DO have to compose the router itself into state before using it

thosmos20:12:49

But by partially working I mean that I loaded my list page via a load! in :componentDidMount (whereas :will-enter did not work when I just tried it, even with a route-immediate) and therefore when I DID try to use :will-enter on the detail page, deferred routing failed.

tony.kay20:12:54

and if you didn’t use the fn version of initial state, you could have used an empty map

tony.kay20:12:52

:initial-state {:sv-router {}} === (fn [p] {:svrouter (comp/get-initial-state SVRouter {})})

tony.kay20:12:10

the macro does that magic using the query as a convenience

thosmos20:12:15

OK, so this is emphasizing to me the importance of always doing initial-state which I’d thought was optional

tony.kay20:12:37

there are no blanket statements about initial state

thosmos20:12:55

well I’m glad it’s all making sense now

tony.kay20:12:00

The things you use in the logic MUST be in state. :initial-state is just one way of getting them there (at app startup)

thosmos20:12:41

does the state from a secondary route get initialized at app start? If not, does it then get the class’s :initial-state immediately before loading it?

thosmos20:12:45

I guess I can answer my own question just by looking at inspect …

tony.kay20:12:09

You are responsible for populating state in all circumstances. The dynamic router will attempt to statically resolve all initial state inclusions for it’s immediate children (by composing them in), but you must compose the parts you control…if you don’t then you must add them to state during runtime

thosmos20:12:57

ok got it. thanks for the help!