> Imagine a form. The ident for a new thing has a tempid. The ident for the saved version does not. Now you have a moving target. @tony.kay that is what state machines do right? before/after
You’re making me realize that I need to double-check that I’ve implemented the event queue properly for statecharts integration so that the data in delayed events supports tempid rewriting. Otherwise things are broken with respect to tempids.
I don’t mind the idents in the state database, event queues, etc. from updating with a tempid rewrite. That makes sense in the overall data model. What I don’t love is the identity of a particular session changing over time. The identity of some entity changed. The session of the statechart is still what it was. I don’t love coupling the convenience with something that breaks with semantics.
I can probably argue myself into it…but that’s my general resistence. I’m in library writer mode, and you have to be very picky so as not to confuse people/thing/internals
tempids have a particular problem that you have to cope with, that generally doesn’t bite you, but can: closures. If you close over a tempid in a programming sense, no tempid rewriting can help you. So, if I’ve closed over the session id (setTimeout (fn [] …send to sessionid that has tempid…) 1000), which anyone might code, now the session ID being a moving target is a bad thing.
the semantics of a session id are that it will be stable. The semantics of a “new entity” are that it’s ID is inherently unstable. YOu can still get into trouble, but I feel less sorry for you in the latter case 🙂
yeah I'm with you, I guess I would rather have random session-ids if the utility functions around statecharts are convenient enough. what I really don't like is hardcoded session-ids that are known at compile time, because that means they are a singleton and only in rare cases (eg a FileDownloadManager) do I want that. I already had a case of a delayed event affecting a new instance of a statechart on a different entity due to a hardcoded sessionid
OK, back to that. Why do you think the session IDs are constant? Read ro/initialize and ro/initial-props.
hold on…let me refresh my own memory
I’m lost on which thing we are talking about. The UI Routing statechart has a constant, well-know session ID. The istate routes allows you to specify the child session ID…and since only one instance of that particular child can be active at a time, why is that a problem?
I mean one can provide a constant one, which is what we do because oftentime we havent figured out a way to talk to the statechart otherwise
autoforward, but that’s messy IMO
So, what exactly are you proposing? I need a session ID before I can start the invocation. If the invocation is going to do work that sets up the target, I don’t know the ident yet. Chicken-and-egg. An abstract (but constant) ID you can set just solves the problem (from an external actor standpoint).
But autoforward can actually be done well, now that I think about it.
If you properly namespace your events to the child chart, then just autoforward and send them to the routing chart 😄
so without use-statechart there's:
• current-invocation-configuration
• send-to-self
current-invocation-local-data would be nice
but then don't you need to manually add the configuration and the local-data of the statechart to the query?
Given the actor model, the point is that any component-centric data would be placed on the component state and query, and aliased into the chart. The internals of the statechart should be irrelevant to the UI
But yeah, for convenience it’s not terribly hard to supply those, but yes, it you want them to update properly, you have to add somethign to the query.
makes sense, what's the point of local-data then?
implementation details
e.g. in RAD reports it keeps cached info about the current loaded data
none of that is used by the UI
that would save the verbose aliasing
the point of actor/aliasing is two things: • Chart re-use. • Separation of implementation detail from UI-visible information
Remember that a chart can have many actors. An alias just gives you a shorthand for a combo of a particlar actor fact.
otherwise your API would be “assign :ui/flag? on actor X”
yeah but for colocated statecharts that have :actor/component automatically added and are invoked by a parent istate
but they can add additional actors…that component might have children that are part of the story
right
The convenience is I supply one for you 😄
ok I think I get the idea
but shouldn't we still have the statechart config in the query when relying on current-invocation-configuration result in the component?
yes
as shown in the demo code
https://github.com/fulcrologic/fulcro-with-statecharts/blob/simple-invocation-demo/src/main/com/example/client.cljs#L21 but you knew the id at compile time
I generally tell people that a link query is probably OK. It may cause a little over-rendering, but in practice it should be fine.
yeah that's what I was worried about 😄
E.g. [::sc/session-id '_]
you'd cause a rerender on any statechart change right?
yes
but I don’t bother optimizing that unless it actually proves to be a problem. This stuff is surpisingly fast in most cases.
if you systematically use statecharts in all of your components you'd be re-rendering the whole screen on every keystroke?
Remember that the thing changing is changing because it is already on-screen 😄
and there usually aren’t a lot of different components on screen at once
so the probability that you’re re-rendering “the right thing” is very very high
using the !! variants of prop changing focuses render to the local component…so there’s one possibly optimization for the cases that are forms (form field change fast local render).
If you have some weird UI where you have a statechart hammering timed events at a high frequency, it would be a problem…but otherwise I think you’re pre-optimizing for no good reason.
I'm not trying to pre-optimize just understanding what corner I'm potentially putting myself into 😄
yeah, I’m saying that if you need the configuration of a chart, putting a table link in the query probably isn’t going to hurt anything in 99% of cases
I can also not use the statechart config and reflect the relevant states as a ui prop
eg if I have a loading state assign :ui/loading? true
yes…there’s something to be said for not “denormalizing” things. I would not make states for things the chart isn’t in control of. For example, “dirty/clean” is represented in form-state, and should not have two states in the chart.
loading? could make sense as states, but you are right that manipulating a UI representation of that in the on-entry bits of the chart probably makes more sense, since there may be a number of states related to “being busy” that all should manifest the same way in the UI. E.g you could have a complex chart where loading include retries and such. Splitting out things that you want to display as logic in the chart makes the UI much simpler…
and much simpler UI with less logic is exactly what I’m going for.
I’m leaning very much in the direction of “coupling the UI to the internal structure of a chart is a poor design”
Instead: • Clearly state (via aliases) the things you need to know to properly render the UI • Write a statechart that produces that data • In cases where the data in the fulcro state database already represents some “state”, don’t re-model that in the chart (e.g. form dirty is a fact derived from data, not structure)
yeah I agree, and I think to make adoption easier having guidelines like this help
My time is limited. Documentation will eventually pop out, but I’m still refining my own idea of what the guidelines are…you’re helping 😄
My intuition tells me that this will be a great way to build large systems…but since “this way” doesn’t exist yet, I’m not sure how that is going to pan out.
> you’re helping 😄 thanks yeah I'm trying 😄
My intuition tells me that this will be a great way to build large systemsyeah I agree, in that regard I think use-statechart was a great hook to get things rolling on our side, but I get the feelings that it's not right
it encourages the use of local-state and config in the UI which I now feel is wrong
it only lets you use the colocated chart and its not even the same as the uir option
So, the good things I like is that a complete system modeled with the routes in a single chart give you the ability to reason about your routing globally. This is great for URL integrations, and things like the use of the statechart history node. I’m hoping that the invocation composition helps with teasing out minutiae so that the top-level chart doesn’t get too insane. Every route is going to need a top-level transition (generated by the wrapper function), but visualizing that means a LOT of edges into every possible target node.
I like that it gives a much better model around I/O and side-effects as well for the complexities of moving around in an application. The current dynamic routing system decouples all of the elements in your system where it is harder to reason about global concerns. DR also isn’t even written quite right and couples some concerns to the actual on-screen render…which is horrible 🙈
With DR you can kind-of get the “history node” thing, but it’s a bit of a mess to implement (e.g. I switch from Inventory to Sales, but Sales has 3 sub-routes…can I remember which one I was on last time I was there)
yeah in the exemple you have everything in the same chart:
(def application-chart
(statechart {}
(uir/routing-regions
(uir/routes {:id :region/routes
:routing/root Root}
(uir/rstate {:route/target `RouteA1})
(uir/rstate {:route/target `RouteA2}
(uir/istate {:route/target `RouteA21
:exit-target ::RouteA1
:child-session-id ::route-a21})
(uir/rstate {:route/target `RouteA22}))
(uir/rstate {:parallel? true
:route/target `RouteA3}
(uir/rstate {:route/target `RouteA31})
(uir/rstate {:route/target `RouteA32}))))))
in my use case at the moment I'm integrating it isolation within a modal to navigate the wizard. I actually broke down the substates into their own components and statechartsrouting to the “same component” but “different identity” is also a case that bites people
Yes, I am right now working on the ideas of composition. My goal is that you can re-use the routes in nested contexts. The routing-regions has the top-level route denial handling at the moment, so it’s meant to be a “root only” node.
this was the UI before:
; NOTE: some steps are intentionally here twice, the idea is to mirror the tree of views based on the
;; statechart states
(cond
(::submitting config) (dom/div {:className "w-full h-64"} (cl/loader))
(::error config) (error-step)
(:state/lti-mode config)
(cond
(:state/lti-choosing-step config)
(lti-choosing-step props send!)
(:state/lti-mode-new config)
(cond
(:state/subject-area-step config)
(subject-area-step props send! 1)
(:state/interface-language-step config)
(interface-language-step props language-course? send! 2)
(:state/level-step config)
(level-step props language-course? send! 3)
(:state/set-a-timeframe-step config)
(create-a-new-timeframe-step props send!))
(:state/lti-mode-existing config)
(cond
(:state/course-choice-step config)
(course-choice-step-ui course-choice-step
{:selected-course-id (:ui/selected-course-id props)
:onSelect #(send! :event/course-selected {:course-id %})
:send! send!})
(:state/set-a-course-timeframe-step config)
(cond
(:state/create-a-new-timeframe-step config)
(create-a-new-timeframe-step props send!)
(:state/choose-an-existing-timeframe-step config)
(timeframe-choice-step-ui timeframe-choice-step
{:selected-timeframe-id (:ui/selected-timeframe-id props)
:onSelect #(send! :event/timeframe-selected {:timeframe-id %})
:send! send!}))))
(:state/regular-mode config)
(cond
(:state/name-step config)
(name-step props send! 1)
(:state/subject-area-step config)
(subject-area-step props send! 2)
(:state/interface-language-step config)
(interface-language-step props language-course? send! 3)
(:state/level-step config)
(level-step props language-course? send! 4)))
with a giant chart alongside it, I'm trying to turn that into routesoh yeah, what a mess
I’m still trying to bend my brain into how I can sub-route across an invocation boundary…
I think it’s just a matter of having event data that I can propagate into the data model of the invoked chart…
sub-route across an invocation boundary you mean being able to navigate between different routes when they are not in the same statechart?
being able to restore a route that requires invocations, possibly nested
so yes
right now I'm looking at how to navigate between my routes
main routing chart has a state for “Sales”, but then the subroutes are in an invoked chart.
uir/route-to wouldn't work since I'm not using the global uir/session-id chart
now imagine a user loading a bookmark
Are you using the uir/routes?
yes
That embeds direct transitions for every possible target
just send an event with the name (route-to-event-name Taget)
The problem is, depending on your overall chart nesting, those direct transitions may be embedded at a level where they are not visible.
I guess I need to split these things apart for proper composition to work.
You should be able to use the (direct-transitions) as a node you can put at the top of your master chart. The istate needs logic to “continue” routing cross invocation boundaries.
I'm thinking in the context of a wizard I may not need uir/routes, just handling a :next event, when all the steps of a chart are done it's done, the parent moves to the next
yeah
I probably want that level of control on what the transitions should be anyway
exactly. in a general routing system, all the labeled routes are meant to be directly reachable from a bookmark
in a wizard, this is not true
and what about uir/routing-regions?
That embeds the logic for “route denied and override” handling
I can't render (uir/ui-current-subroute this comp/factory) without uir/routes though apparently
it's because it also sets route/idents
They are currently written to be paired
that’s why I said I probably need to factor them apart
I think I have a better idea for how to do route denials. Statechart semantics, if I remember right, are that “the deepest transition that matches wins”. It’s actually more complicated and well-defined than that. There’s document order and such. But, if there’s a top-level direct trasition for all things, some arbitrary sub-state can always add a
(transition {:event :route-to.*
:cond I-am-busy?} ...)
that captures attempts to route away. Then no global handling is needed at allPutting a wrapper around that where you could even partially allow local routing based on transition order means that is any substate taht wants to block routing you’d just end up with a bank of transitions, with your local routes listed first with their targets, and a catch-all target-less transition (with possibly a trigger of a toast or whatever) that conditionally captures other routes.
The down-side of that is that if you use it a lot it makes your chart considerably bigger…but the up-side is that it gives you unlimited localized control of how to handle being busy.
but given that I’ll have 1000 forms that all want to do it the same way, having the global as a default is probably wise…but for a wizaard, having local control of allowing partial routes is a great add-on.
send-to-self also only works with uir/session-id
I can't find a way to send-to-self
> I can't find a way to send-to-self
@tony.kay colocated statecharts aren't really linked in any way to the UI component they are colocated with. send-to-self works around that by knowing the top level parent session-id and walking down from there.
I think that was the biggest blocker for my teammates as well for using Invoke
Basically if you Invoke your collocated statechart from a parent you can't send to the child statechart from the Child component if you don't provide a hardcoded session-id.
I'm trying to list potential solutions to this problem here:
without uir:
• use a generated session-id known to the parent and pass it as a computed prop to the child (or even pass a send! function like use-statechart
with uir
• could actually do the same so as not to rely on uir/session-id statechart for isolated routing?
Ah, yeah, that is a problem…
well, but is it? This is a react hook…you’re using it locally…which are we talking about? Invocations or use-statechart?
I'm talking about invokations yes, not use-statechart
OK, so a few things: 1. UIR is not API stable. I’m about to change it significantly. 2. The use of co-located statecharts for uir/istate invocation states PRESUMES the use of UIR right now…that’s why it is in that ns. It is a UI routing invocation state.
so, in that context the routing system resolves the issue for you, but you could use the same pattern to resolve it for yourself if you want the same functionality in your own supporting code
Here’s what I realized as I was working through things. A given “routable state” almost certainly will need side-effects at times. In js these will be async. The child states MAY depend upon data established by parent states. That’s one of the issues with the current composition in Fulcro: there’s no way to be explicit about data dependencies in the routing structure that are resolved async. You can sort-of do it (route-deferred lets you defer the UI portion of the routing), but say you have this:
Event (selected by the user, contains subroutes)
Dashboard
Edit venue
Edit ticket price
...
The sub-routes all assume that you’re already on some “event”. In Fulcro we can model this pretty easily and even pass the event details through computed props. But if some side-effect of the “active” sub-route needs that data while routing, then there’s a timing issue.
Worse, side-ui concerns that might be mounted at root (e.g. a modal) won’t have the benefit of the “selected event” in the tree, leading to us creating a “selected event” data point in the database. But there’s no good place to put logic that ensures we’ve actually set selected event, or that such a process is in progress.
This is actually one of the use-cases that has been driving me to move to this better model.A statechart will ensure on-entry/on-exit, etc. BUT, the problem I didn’t consider properly was the async nature of the I/O. It isn’t a simple on-entry per route…it’s an on-entry that has to have other states to process the potentially async interactions before continuing a routing decision.
in other words I can’t apply a path “bookmark” in one shot. It has to be “consumed” by the chart and cascaded through the child states/invocations, etc.
This formalism fixes my problem, but now I’m wondering “at what cost” in terms of complexity 😄
I’m betting I can make wrappers that make it less tedious to code, but I’m not sure a visualization is goign to be very fun. So, I continue to experiment.
not sure if that is exactly the same problem you are describing but in our app we have deeplinks via urls that can go to specific entities but since they are nested in the UI eg a question is in a course in the content tab, in a particular lecture and topic (breadcrumps) so there is lots of extra attributes to grab before displaying. so what I've done is a statemachine that handles all of the general and specific concerns in terms of loading / setting up before displaying a particular entity
I assume your system intends to do that in a more generic/composable way
> so, in that context the routing system resolves the issue for you, but you could use the same pattern to resolve it for yourself if you want the same functionality in your own supporting code
the pattern in uir is just to rely on a hardcoded top-level session-id AND walk down from there to find the invoked statechart related to your component right?
I was hoping to figure out a more trivial solution.
eg imagine i have a parent component and I want the statechart to launch a modal on some event.
I would do that with an invokation, but now how woud the modal get the session-id of its own statechart if I used a generated session-id for the parent?
right now I can only think of the solution I mentioned above where the session-id or send would be passed as a computed-prop to the modal
Given I don’t have a general solution to routing yet, I don’t have an answer for you. Ultimately it’s going to be “store the ID somewhere well-known, or use a constant”
An invocation NODE in a statechart can have an ID, and if supplied will be the child session id. You can also use :idlocation to have it generate a session ID and plop that into the data model of your chart. The data model IS part of app state, so you could look it up in there.
you could also base it on idents, etc.
I don’t ahve a more general solution for you with such wide-open constraints and a lack of knowledge of your own local solution…not that I’m asking to solve your larger application programming problem 😉 Other than trying (and currently failing 😄 ) to write library code for routing
I think my overall idea/approach is flawed. I actually think the right thing to do is simply provide some utilities for managing the URL from whatever statechart states and structure you want to define. Anything else makes an explosion….but I’m still trying
> Given I don’t have a general solution to routing yet, I don’t have an answer for you. Ultimately it’s going to be “store the ID somewhere well-known, or use a constant”
storing the ID somewhere well-known could be a solution
I think this is an important pattern to support with utilities, and maybe how uir should do send-to-self rather than the current approach if it is robus
it would be good for adoption, projects can't switch their whole routing overnight, but being able to use invoke on colocated statecharts of child ui components without relying on constant session-ids turned out to be a frequent need of ours and I think many SPAs can't work with singletons (eg dahsboard and whiteboard like apps)
> you could also base it on idents, etc. as you mentioned that wouldn't work if the component doesn't have a stable ident like a form
@tony.kay I'm really with you on: > I think my overall idea/approach is flawed. I actually think the right thing to do is simply provide some utilities for managing the URL from whatever statechart states and structure you want to define. Anything else makes an explosion….but I’m still trying when I started with Fulcro RAD, I was really battling the nested form/report URL requirements because I was obsessed with having a specific format of URL with a single id in it and as little segments as possible. any form of default URL scheme seem to bring on complexity in the library and complexity in doing anything slightly different than what's expected in the implementation From you design doc > • Some level of URL integration should be possible for browser usage, but sane limitations should apply to reduce complexity. > • Complete arbitrary composition of statecharts leads to scenarios where URL representation is quite difficult or even insane. > ◦ IS possible to "fix" this problem with the user coding explcit top-level transitions that resolve conflicts. E.g. auto-detect when there is ambiguity that is not resolved by a manual top-level transition. with the right utilities and exemples even the current dynamic routing schemes should be trivial to implement by developpers to me the things I expect the most that would help do my own routing is utilities to deal with dynamic queries and connecting statecharts (at the risk of sounding like a broken clock, I'm still having issues finding how a child component can talk to its colocated statechart without relying on a singleton/known session-id)
which version of co-located charts are you talking about? use-statechart , invocations, or something else?
One way to do what you want well is to probably use use-generated-id with use-statechart in the component. Then each chart has a unique ID IF the lifetime of the chart is that of the component’s on-screen time. Otherwise, something else in your app has to be responsible for the lifecycle. Making some kind of wrapper logic around that (e.g. a central statechart that does the routing as invocations and registers them somehow). IF you have a statechart like that, then the state id (of the invocation) IS a global fact, and could be used for the session id. By definition only one can be active at a time.
but use-statechart returns a send! function….so there’s that
and it looks like my istate stores the session id so you can find it:
:idlocation [:invocation/id target-key]
where target-key is the fully-qualified classname of the target that the invocation is running
but the solutions you are describing imply that if I want to send to the colocated statechart from within the child, I have to write code that is aware of that global fact.
I'm thinking that when invoking the colocated statechart of a component, if the component has a statechart, a invoking util such as istate should store the statechart ident under the key :ui/statechart at the component ident.
that way even if the component changes ident like a form that saves for the first time the :ui/statechart will still be there.
that way the child can send events to it's colocated statechart without knowing which global statechart it depends on.
So, ARE you using ui routing or not? If you ARE, then you already have the solution written for you in uir/send-to-self! , but I’m likely to deprecate the entire namespace in light of developments. If you want to make your own istate that works with dynamic queries on a component to support some level of “routing” and want to co-locate the session ID on the state of the component, that sounds perfectly sane. I’m quite busy recently, so I don’t have time to do that for you, nor review it for including in the library at the moment, but all of the hard work is done, and you have examples of how to write any of those elements in the existing code.
The statecharts library was specifically designed to make such extensions trivial to write, as you’ve seen, with the intention that you’ll invent what works best for you anyway
I've experimented with the ui routing locally but send-to-self! wasn't working for me since I didn't use the global statechart. But yeah I have a good idea on how to do it, I'm in no hurry ATM as I still use the dynamic routing and use-statechart.
I’m realizing now, after all of this design work, that the overall model is too simple to support what I want as a final result. So, the entire overall design needs to change. I’ll be updating the design doc more as I put together a prototype of the new approach.
I've been doing some hammock time when I laid out an actual fully functional asynchronous solution, it was rather complex. I'm starting to feel like the basic primitive I should give is something that allows you to easily change the dynamic query of a component, and to do an invocation around that, and for your component to handle everything else from there. The problem with compositional routing is that the asynchronous behavior of a parent route may want to block a child route. In other words, you may have data dependencies from the parent that need to resolve before you continue on to the child's State. State charts don't have this mechanism built into States themselves, you have to build a chart that describes these interactions. Interactions. So, generalizing that makes kind of a mess because each subrout gets its own complete combination of States and entry and exit handlers and events and transitions etc
Url integration is a major pain, unless you just assign particular URLs to a particular final state, and then the synchronization system at least from inside out can easily work. Re-establishing, a particular State on an incoming URL is kind of the big difficult problem
Because transitioning to some extremely nested state where there were i o dependencies that were asynchronous in nature, and where you probably had to invocations and such, means such transitions are rather complex.
I'm starting to lean towards making a state chart-based version of dynamic routing, and keeping the compositional hooks exactly the same, but just cleaning up the internal logic using state charts. This would separate the entire routing issue into its own concern, which now that I've looked at it, was probably the right design choice to begin with. I could also just try to fix up the existing issues and dynamic routing, and leave it in UI State machines. The only reason to rewrite it, would be to then get the visualization support that is already partially extant in my state charts support in inspect.
honestly I like the id of "just assign particular URLs to a particular final state"
Yeah, but there's no clean way to transition to that Target State. Unless you provide the input URL transition yourself
> no clean way to transition to that Target State. Unless you provide the input URL, transition yourself I don't get this
Imagine a parent state that needs to load the current context before it can allow a transition to a substate. So, if a URL is placed on that substate, how does the routing system get through the upper States that need to run and transition and do all their handling to do the loading?
The dynamic routing system as it currently exists gives you a UI State machine for each router. And so cascading through the different routers to each subtarget allows i o control at each layer
State charts don't have asynchronous transitions. Neither do uI state machines. You represent asynchrony through events and various states that model the exact sequence, error handling, etc
I would expect that the URL contains enough parameters to reach the state where it is defined. that is actually how I do it in my current-course state machine right now the url to a question only contains a question-id so the state machine gathers additional params to reach the full routing state before routing, like the course-id, topic-id etc... from the API essentially letting pathom figure out how to get them
Yes, in the trivial case that's fine. But if you're trying to design a general reusable system for arbitrary systems, then you have to consider that there might be asynchronous dependencies. Your simplified scheme simply doesn't support that. You'd have to spell out the rest of the chart, and then an external system. Trying to enforce an incoming URL would not be able to do it in a single synchronous step, other than to send you an event saying hey, this URL needs to be displayed
At which point, the routing system itself isn't a solution. See what I mean?
this week end I was thinking about making an abstraction for our pattern of having tabs in modals, but then I realized that it falls once again under the umbrella of a routing solution, opening a modal is just a parallel state, each tab is a substate.