This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2015-12-28
Channels
- # admin-announcements (72)
- # aws (23)
- # beginners (43)
- # boot (140)
- # cider (11)
- # cljs-dev (4)
- # cljsrn (82)
- # clojars (2)
- # clojure (215)
- # clojure-nl (2)
- # clojure-russia (149)
- # clojurecup (4)
- # clojurescript (159)
- # cursive (19)
- # datomic (47)
- # editors (1)
- # emacs (27)
- # hoplon (32)
- # jobs (11)
- # ldnclj (3)
- # mount (33)
- # off-topic (1)
- # om (380)
- # onyx (1)
- # re-frame (2)
- # reagent (54)
- # yada (63)
@robert-stuttaford: I would encourage you to look at https://github.com/swannodette/om-next-demo . It might be outdated but when you work around it, you will find some useful functions to test and work along the way.
I'm having trouble understanding the reconciler's :send
callback. I've got a parser with remote reads that asynchronously merges in novelty with the send callback. It works great when the page loads: when the XHR finishes the send callback is called and everything updates. However I also have a mutation that updates state. When I query that mutation along with a bunch of other keys I want to read (which trigger the same XHR), everything ALMOST works. The state gets updated, the novelty merged in (asynchronously, after the XHR returns), but the read queries don't seem to fire after the state gets the novelty from the send callback, so nothing gets re-rendered. My workaround for now is to swap a dummy value into the app atom every so often to force a re-render, which is obviously not ideal. Re-rendering also happens if I update some component local state.
I know this is a vague description, but I'm hoping somebody will have had experience and can give a pointer or two. If not I'll boil it down to a simpler case to share.
@maackle: Have you checked this one? https://github.com/omcljs/om/wiki/Om-Next-FAQ#why-is-my-component-not-rerendered-after-transact
so, i’m probably tackling too many things at once, here, but here goes. i’ve got a homogenous, nested record structure (name, id, children->[(name, id, children),…]). i have it rendering this tree in a recursive manner just fine, but i’m struggling with mutating it. the tree structure is loaded via XHR, and, as best as i can tell, the data is not normalised by om.next when the remote merges it in. is there a way to get om.next to normalise the merged-in data as well, and is there a way to see whether it’s doing this or not?
@robert-stuttaford If you use merge!
, the incoming data will be normalized. You may have to pass your own :merge-tree
to the reconciler to make merging novelty work (to avoid overwriting properties set only in the client for instance).
If you print @reconciler
in a browser REPL, that'll show you what the data looks like after normalization.
ok. the data is currently coming in via the automatic merging that om does with :remote true
i’ll do the @reconciler thing. i was printing state
from inside a mutate impl but that showed me the nested maps
Are your records lists, like your post suggests or are they nested maps? If they are, it might be a good idea to transform them to maps (e.g. {:name ..., :id ..., ...}
. One way to look at Om queries is as statements that (recursively) select keys (properties) from a nested map (your app state). Om is actually more flexible than that but your read
implementation will be simpler the closer your apo state resembles a nested map.
they’re nested maps
@robert-stuttaford Look for default-merge-tree
in next.cljs
. That's the default merge algorithm. It's expected that most people will have to overload it when constructing the reconciler because it's very, very simple.
i note that normalised data uses idents. how do idents get their names? :person/by-name
Idents are just two-element vectors. I'm not even sure the first element needs to be a keyword. The naming is up to you.
gotcha. ok. data isn’t denormalised. going to look into :merge-tree now
so, reading https://github.com/omcljs/om/wiki/Components%2C-Identity-%26-Normalization, it seems to suggest that om came up with the :person/by-name
key all by itself
In the normalized data, this will lead to two map entries: :person {19 {:id 19, :name John ...}}
(a lookup table essentially) and :people [[:person 19]]
(a vector of links, where :people
is an entry in your "unnormalized" data that you query for and that originally contained a vector of maps - the original persons records).
ohhhh. it’s in the om/Ident declarations
things are snapping into focus a little
looks like i shouldn’t give om a tree structure and rather just give it pre-denormalised data with idents in place
Om essentially looks at the root query and the denormalised state data side by side. The query is annotated with the origin components in its meta data. So when normalization looks at a subquery that corresponds to a component and a record in the data, it passes the record to the component's om/Ident, replaces the record with the resulting link and puts the actual record data into the lookup table using update-in @state link merge data
(well, simplified but roughly like that). That's how the normalized data is constructed.
so, how do i model recursion in om/IQuery decls?
i totally get doing this in the non-recursive case, now. i’m trying to reach a bit further
No, you don't have to put idents in place. But your records need to have a structure that your om/Ident implementations can turn into idents. If the ident is computed from an :id
property, your records will have to have the structure {:id ...}
.
yes, i have this. i’m now working on how to relate the recursive component’s query to the root component’s query
Recursion is done via {:somekey (om/get-query SomeComponent)}
or {:children '...}
if the children will be rendered using the same component.
ok. then i’m on the right path. i have the first case. but when i pr-str the om/get-props value in the recursive component, i see the idents instead of the actual maps
it feels like i’m really close
Ah. In your read
you'll have to translate back from normalized data to the real data (to expand the ident links again for instance).
aha, like this?
https://github.com/omcljs/om/wiki/Components%2C-Identity-%26-Normalization#adding-reads
@jannis, can i put a gist together for you to review?
what method can I use to set the component initial state?
found the answer, use the initLocalState
lifecycle method
curious: what are you doing with om.next, @jannis?
@robert-stuttaford: I'm just playing with it, toying with different ideas involving Git, Clojure and Om web apps to eventually be comfortable looking for related work. My main pet projects at the moment are Gitalin (a Git-based data store with a Datalog-like query interface), Custard (a requirements/architectore/work planning tool for hacker-friendly, no-nonsense software architecture) and a somewhat secret project with @thosmos
@jannis would you recommend using it now to build a production app? I have to rewrite an web admin dashboard and turn it into a SPA. We use Clojure for our backend and I would like to use it for our frontend to. I would hate to have to go back to JS
@jplaza: Sure, technically I don't see a reason not to use it. Personally, I wouldn't want to go back to anything else at this point, having worked with Om Next for a few months. There may be other factors in the decision for your company, like how much training your team will need to adopt it, how much time pressure there is etc.
@jplaza: The two main areas where the story is still a little rough perhaps and where more experiments will be needed to come up with elegant solutions are routing and animations.
I can deal with the experimentation .. we are a Startup so that’s our “daily bread”. But something like secretary would help with routing, right? At least until a more suitable solution comes up
the only reason I’m spending any time on it is because people keep asking the same question over and over and over again
if all you need a custom routing thing - there’s enough there to sort this out yourself
however due to popular demand - I am interested in a standard places to hook in whatever routing thing you like
That’s a good point, but why do people keep asking about routing? Seems to me that using something like secretary would do the job
Ok fair enough… I’ll continue working in my new project and will let you guys know how things went.
The only reason I care about routing is to allowing visitors/users to re-enter a particular UI state from a URL.
@jplaza: FWIW, check out aemette, a lein template I put together which comes with client side routing (https://github.com/anmonteiro/aemette)
This implies a two-way translation between URLs and state. I'm not even sure I'd call that routing. 😉
But I agree with @dnolen, the pieces are already there so you can achieve routing already. Except it may not be the most convenient way, hence the thinking about standard hooks.
Thanks @anmonteiro will check that out
@dnolen: on routing, all fair points. if you’re interested, i can definitely share a couple strong user-centric cases for routing
i’m super glad that you’ve pushed it to the edges; that’s where it belongs!
@robert-stuttaford: I remain completely unconvinced in the value of routing
yeah. it’s a web thing. it allows us to provide users with good continuity - leave SPA to go to some other page - hit back, SPA renders the way it was when you left it
that’s one. the other is sharing state with others. e.g., providing a deep link to some record’s view
i hear you Om is generic to the factor in which it runs - native, appletv, web, etc
it’s the one we fall back on mostly because it’s the one we’ve always used, i guess. and browsers know how to do it. just like http caching
i’m curious; what other approaches do you see to solving this problem that don’t involve browser history?
(assuming a good case can be made for needing it)
i agree
@dnolen: i’m finally taking a stab at om.next. busy doing a recursive tree (since doing that with the previous om proved to be so useful for learning). i’ve run into a snag. i’ve pm’d you gist url. i’d greatly appreciate it if you can give me some idea of why i’m getting the included stacktrace at the linked line, please, when you have a moment?
@robert-stuttaford: I don’t take PMs about Om questions
ok. i’ll sanitise the data and link it in here
you’re right. my bad
huh, i don’t know what i was worried about. it’s fine: https://gist.github.com/robert-stuttaford/e7ef1e151c157f4a4d24#file-4-parser-cljs-L29
using [org.omcljs/om "1.0.0-alpha28”]
. i’m going to keep digging, of course. just sharing as i might be experiencing a bug or a common but undocumented gotcha
@robert-stuttaford: you are loop-sending to the remote
at some point you're gonna have to return local data
when it gets merged to your state
i only see one XHR call, though
the error happens when it’s attempting to do that merge
what if you replace {:value (om/db->tree query v st) :remote true}
with {:value (om/db->tree query v st)}
in the line you highlighted?
or do i misunderstand?
same thing happens
let me take a closer look
i’m pretty sure it’s not looping the remote anyway, because before i got to doing db->tree, it was also only doing it once, but attempting to render the ident values directly
that it’s blowing the stack tells me that it’s looping infinitely somehow. i’ve checked and the data doesn’t have any …. humm. i might have an infinite loop in my data 😞
@anmonteiro: thanks for rubber ducking. i have an attack vector!
we could well have two classes of entity with identical slug values
@robert-stuttaford: am I wrong or is there something wrong with your normalized data anyway?
children
is not correct in all cases
e.g. "loose-suites"
thank you, i’ll double check that it’s consistent
@robert-stuttaford: it also seems that there is a list in the app state while Om only supports maps & vectors
In the place I pointed out before
Not sure if this could be it
It's just what caught my eye
@anmonteiro: the lists not being vectors thing was the issue. turns out i had no duplicate slugs. sharp eye, thank you!
happy to help
Looking for advice: I'm getting started with Om Next and finding it a little confusing (not unexpected since it's still alpha). My situation is that I have Datomic->DataScript streaming sync working (sending the entire DB and then every transaction because I have a very small dataset) so all reads are local and I don't think any of Om Next's normalization stuff is relevant. All (or virtually all) mutations are remote; I don't care about optimistic updates for this project. The changes caused by mutations are transacted into DataScript via the same streaming mechanism as all other changes, so my remote mutations don't need any special handling for merging. Is Om Next worth it for this project given the above conditions and a tight deadline? Would I be better off using something like Rum (with a small custom mixin for DataScript) or will Om Next be faster to develop with after I get up-to-speed?
@robert-stuttaford: Ah, lists vs vectors. I've hit that before.
It feels odd to me that each read
invocation is responsible for deref'ing the app state. Doesn't that mean that a single query could read from multiple versions of the app state, one for each key?
there are still a few remaining significant gaps that would keep me from using it on any production project with a timeline in the very near future
Oh, I see, you could decide to give parse
a map instead of an atom as the :state
, then?
Well, okay, you say that, but as someone who's still learning, there are definitely somethings in Om where it matters what you do.
@peeja: sure but just pointing out that you are just dealing with whatever defaults I could come up with
Really what I'm trying to understand is: where is it sensible to "do whatever I want", and where would I be abusing something that happens to work at the moment
@dnolen: By tight timeline I mean several months and January isn't far off. Assuming OmNext is complete, what does it provide if you don't need normalization, remote reads, or optimistic updates? Sorry if this is a foolish question. I've watched all the talks and read all the docs and I'm still not sure.
since the details of parsers between clients and servers may not necessarily be the same at all
Yeah, that makes sense. I had it in my head that there would always be something reasonable to call :state
. (And maybe there almost always is, but it doesn't mean the parser has to care.)
Yeah, I was imagining you'd have to call that your :state
, but I suppose there's also no reason you'd have to have only one, right?
quite an elegant design
but :state
can easily be a DataScript DB so I haven’t seen a real need to make it more flexible.
Hi all. Complete noob question here, I’ve got a bit of experience with Clojure but trying to pick up Om. Can’t make the jump from the basic getting started to an actual SPA with pages, URL params etc. Can’t even seem to work out how best to get to setting params on components. Could someone point me to some examples of this?
@heeton I suggest you follow the Om.next tutorials at the wiki: https://github.com/omcljs/om/wiki
@wilkerlucio: I have done the wiki ones, I’ve browsed through the others listed in the footnotes but can’t seem to see anything that deals with this
@heeton: people are still figuring out ways for routing, no standard at this point, you may wanna go easy at first, since you have done the wiki pages check the om-tutorial from tony kay, it can give you more experience with om.next: https://awkay.github.io/om-tutorial/
Wow, a lot of activity in the last 8 hours! @artemyarulin, responding to your response from 8 hours ago about re-rendering after transact!
...I did take that bit of wisdom into account, and it doesn't help in that case. I'm also transact!
ing on the reconciler itself, not some child component, so I would think that would render the whole shebang. I have a feeling the problem is in the asynchronous nature of the mutation, so the transaction completes before the novelty is merged in.
Here's a fresh question for everyone: When doing a mutation involving an async XHR, do I have to ensure that the transaction finishes after the novelty is merged in? If so, how? If not, why else could it be in my case that the novelty-merging callback does not trigger a re-render?
My transact! call looks like this:
clj
(om/transact! core/reconciler `[(people/add ~verified-person)
;; these reads are basically all the queries for the entire app
{:widget/person-list [{:list/people [:natal-chart]}]}
{:widget/aspect-table [{:list/people [:natal-chart]}]}])
@wilkerlucio: thanks. Could you suggest some current options? Are people plugging in existing routing libs, or rolling their own?
@maackle: You should not, in general, transact on the reconciler. There is no "whole shebang" re-render. The keywords (and you need only supply keywords) mentioned after a transact will cause re-render of any component that has those in their query.
OK, even transacting on the local component (`this` in this case) leads to the same behavior
My send is written pretty much like the tutorial here https://github.com/omcljs/om/wiki/Remote-Synchronization-Tutorial
it pushes data and callbacks onto a channel, which gets taken by a go-loop that makes XHRs and calls the callback on the novelty
So here is the sequence of events: 1. (people/add) will cause an invocation of mutate for local (target = nil). If you return an action thunk, it will run right then against local app state 2. (people/add) will cause an invocation of mutate for each remote (target = remote). IF you return :remote true, then that will get queued 3. The local reads will happen 4. Send will run
Ahh I didn't think about the remote invocation. But I also haven't seen any examples of that so far.
Does the mutate function need to return {:value ... :remote-key foo}
similar to reads? And if so, what is foo
here? another ast?
But wait. I'm not actually doing a remote mutation. I just want the mutation to trigger a remote fetch from my :read parsing
@heeton: I saw people talking about routing but I haven't tried to do tackle it myself, sorry but I can't remember a link with a reference now
@tony.kay: I've read a few, not sure which you're referring to. You mean on the github wiki?
@wilkerlucio: No worries, thanks anyway
can't say I fully understand everything, but at least I think my use case hasn't shown up there
@heeton @wilkerlucio: I've put together a lein template that I've linked before (https://github.com/anmonteiro/aemette) which brings a basic web app with routing included. It couples together some ideas that I've been using in my own apps
@maackle: OK, so general rules for transact!: 1. Transact on a component. The one that "owns" the thing being changed...if a child needs to trigger the action, then send a callback to do so from the owner using om/computed 2. Follow mutations with keyword property names (not queries). (the keywords will get transformed into appropriate UI queries)
Your read/mutate infrastructure will then have to decide what to do. If you want remote reads, then your code will have to say so. If you want local stuff, then they have to do it.
Thanks for all this. I'll take it and run with it. I think the issue might be that I'm expecting too much of a particular remote read. It sounds like I have to abstract out the XHR stuff from that read and put it directly into this mutate, rather than trying to trigger the query that causes the remote read after the mutation
yes they are side-effect free, but my read causes a remote query like [(:action {:params ...})]
which gets picked up by send and does XHR stuff
so I think I need to make sure that happens directly with the mutation, i.e. can't rely on the read getting triggered again after the mutation
(even though it does get triggered and does the XHR and state update, just doesn't cause rerender)
By putting the keywords in there, you tell Om to queue re-renders of all components that query for those keywords. That happens during the default merge (which you trigger via thecallback in send)
that's the confusing part: the callback definitely does merge in the proper state. but rerendering doesn't happen.
I tried console.logging the app-atom every second with js/setInterval, and it definitely updates after the callback runs. just no rerender. if I cause a rerender through some other interaction, all the new stuff shows up
that means, it would seem to me, any top-level (sub)item that came back with data (and all children). Result looks like {:x {...} :y {...}}
would queue render for :x
and :y
I'm glad you got me looking at the source too, I think I may have to trace the internals to see what's really going on for my case
and transact!
does this for rerender list: (p/queue! r (into q (remove symbol?) (keys v)))
there must be something slightly different going on here. There is another case where the same callback gets called on some novelty and the rerender does happen after. but with this mutation, no
Yeah, trace it down. Them's the specifics I know, and I feel like I just understood them better myself
the two queues are identical...they both take the keys from the response (top level) and put them in for a re-render
it also queues the component that ran transact, as well as the ident of that component, if it has one
well that's the difference then: in the case of transact! it queues these things immediately, in the case of read it queues after the callback merges novelty, right?
transact immediately causes re-render, and queues any sends that your read/mutate functions say are remote
when your network code chooses to call the callback (which is merge), then another re-render happens based on the novelty
OK so it could be that my novelty does not reference any actual components, only ident tables...
so, I'd probably consider that a bug, unless David has some reason for not re-rendering something parameterized, an I could not imagine that
but if we're talking about the queue! after merge, it takes the state delta, not a query
and the fact that the params are there are probably hanlded at the rendering stage, now that I think of it...it is still a key
I need to head out. hopefully that gets you closer. Inspect what you are making, it should easier to figure out now
@maackle: FYI: It does not appear that parameterized top-level queries will re-render properly.
Is there an architectural reason why read
and mutate
are different functions? Given one receives keyword key
s and the other receives symbol key
s, it seems like they could be a single function.
@anmonteiro: Thanks!
@peeja: those functions are provided…they have same signature so you technically could but they have different expectations in what is returned. I would not conflate the two. Typical approach is to utilize multimethods for each.
When not passing an atom to om/reconciler
(as state), should I be doing something similar to the defonce
on the atom that I used to pass (when I wasn’t using the indexer)?
The reason I’m asking is that the entire app state is refreshing when I make a trivial update to a component. The state is reset too when I check @reconciler.
(def reconciler
(om/reconciler
{:state app-state
:parser (om/parser {:read read
:mutate mutate})}))
(om/add-root! reconciler
sentence-view/SentenceView (gdom/getElement "app”))
That’s how I’m setting up my root and reconciler. I used to defonce
my app-state
before I started using the indexer. But, I don’t do that any more.
Didn’t think it was necessary, since it is passed to the reconciler as a plain old map.
@mudphone: not quite sure what your problem is
@mudphone: are you using figwheel?
So the problem is in reloading, not in the reconciler (not sure what you want to mean by indexer, I think you're mistakingly referring to it as reconciler)
sorry, I mean, I switched to passing a map to the reconciler as state (instead of an atom), in order to get it to index my tables?
Indexer exists
But I don't think your problem is related to that switch
@mudphone: several good projects out there. Even the quickstarts in the om.next documentation set you up with figwheel.
@mudphone: Anyhow, have you read through figwheel's section on writing reloadable code?
@adammiller: yes, I started there… I’m just wondering if I’m missing something (again), so looking for more examples… thanks for the tip though… I think maybe I should probably retrace my steps, starting there might help.
Cool, I'd start by defonce
ing the reconciler
@anmonteiro: that’s simple enough
good, more complete client side project: https://github.com/Jannis/om-next-kanban-demo
@adammiller: thanks, just downloaded it a minute ago, but haven’t taken a look yet… also going to dive into the guts of @tony.kay ’s devcards tutorial
@mudphone: also not sure if something should be done about add-root!
@anmonteiro: still resets the app state… I must be doing something basic wrong… I think I need to reduce this to a minimal example, I’ve been throwing things together as I learn them
@mudphone @adammiller - not that the kanban demo uses boot
instead of lein/figwheel I believe.
@mudphone: I'd try wrapping the add-root!
call in a function and call it on demand
(Not on figwheel reload)
@anmonteiro: doing that now
@anmonteiro: figwheel hangs, I’m guessing because I don’t get a chance to add-root
The last message from figwheel is "Prompt will show when Figwheel connects to your application"
Hmm, I wonder... does the fact that so many seem to find the kanban demo useful even without any source comments or documentation prove that Om's goal of leading to predictable, universally understandable program code is being met? 😉
@anmonteiro: looks like I can’t put the add-root in an fn, because figwheel needs it to run on startup
@jannis Om’s goal along with the way you wrote and structured the code…it is very easy to read and understand so thanks for that!
@anmonteiro: but, I can put it in a defonce… and I still have my original problem of the state refreshing
@anmonteiro: oh wait, I mis-spoke, I can get figwheel to connect before running a run fn with add-root in it
@anmonteiro: going to try putting my core/run fn into the figwheel { :on-jsload "example.core/reload-hook" }
call the set-query for component B throwed "No queries exist for component path (front-end. core/A front-end/B)"
I was having issues comprehending how tempid migration works so I created a very basic app in case anyone else might be interested that shows how it works https://github.com/akmiller78/tut-omnext-tempids
@a.espolov: What does the query of the component look like afterwards?
@jannis: full example above, complete example above, next query equal first query use paging params 0 5
@a.espolov: You're aware that you're not parameterizing the query in B
correctly, yeah?
I suppose since you're taking it apart in A
, parameterization may not be what you're after.
But you're setting the query of A
and try to update the query params for B
? That's never going to work.
{:message "No queries exist for component path (front-end.core/A front-end.core/B)", :data {:type :om.next/no-queries}}
If you want to parameterize the query of B
, one thing you'll have to look into is the parameterization syntax. Embedded in a component query that's [... (<query expr> <param map>) ...]
. The way you're doing it in your example is simply broken. You're also picking the query of B
apart in A
which is wrong.
@jannis: (om/get-query B) returned structure data which does not understand backend parser
I found the other way to do so is to sub-query
Therefore, this code appears ' [( (first q-inspections) (last q-inspections))]
I'd be surprised if that worked. Certainly setting :params
in A
where they are originally defined in B
seems wrong.
I would try to get parameterization work in a single component first and if that works, move it down into the sub-component. Picking the sub-query apart with first
and last
like you do is not valid AFAIK, so don't do it. Include the full sub-query as-is.