This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2016-06-16
Channels
- # arachne (1)
- # beginners (27)
- # boot (17)
- # cider (10)
- # cljs-dev (5)
- # cljsrn (76)
- # clojure (59)
- # clojure-austin (2)
- # clojure-brasil (1)
- # clojure-greece (76)
- # clojure-mexico (1)
- # clojure-quebec (63)
- # clojure-russia (16)
- # clojure-spec (127)
- # clojure-uk (12)
- # clojurescript (72)
- # community-development (7)
- # core-async (3)
- # core-matrix (2)
- # cursive (13)
- # datomic (8)
- # emacs (4)
- # funcool (4)
- # hoplon (148)
- # immutant (5)
- # keechma (2)
- # lambdaisland (5)
- # lein-figwheel (15)
- # leiningen (20)
- # off-topic (23)
- # om (13)
- # om-next (19)
- # onyx (11)
- # planck (11)
- # re-frame (59)
- # reagent (14)
- # rum (34)
- # specter (30)
- # spirituality-ethics (16)
- # uncomplicate (5)
- # untangled (387)
- # yada (2)
Yeah, I'll give the union case a bit more thought. The problem is if I try to solve that then I have to WALK initial state, which is a pain. Ident does not have a leading I in Om...I think eliding it is ok
@tony.kay @jasonjckn my only hesitation with InitialState
is how general it is. devs might confuse it with initLocalState
, or with their preexisting understanding of React’s getInitialState
. Maybe something like InitialAppState
? Or InitialGlobalState
?
@ethangracer: good thinking
easy to forget that possible confusion when we're always pushing people away from local state.
Thing is, I'm planning on using it for a bit more than just initial app state (though that is the primary use)
The new merge-state!
function (just pushed an update to clojars 0.5.3-SNAPSHOT if anyone wants to play) is experimentally using these constructors to establish a sane initial state for merge to target in cases where the object wasn't already in app state.
This is useful in cases where things like :ui/attr
or empty lists need an initial value for correct initial rendering
I'm toying with making it optional..but I'm not sure if the default should be that it uses it and you have to turn it off, or that it is available if you turn it on.
Given that the constructor accepts parameters, I'm thinking "off by default", and "on if you pass constructor parameters" is the correct thing to do
One place this was biting us in server push was in a case where a child had no props in state, but was doing a link join...e.g. [{[:thing '_] [:data]}]
. If you construct app state so that the parent's join of this query goes through "nothing", then the child query doesn't even run...meaning you need to at least plop an empty map into app state where the child lives.
Hm. I think off by default is probably the way to go at least to start. App-state can be pretty finicky, wouldn’t want to accidentally corrupt data.
constructor is good for this use-case. But then it occurred to me that if your default :ui/checked
or something was true
you'd have a similar problem
But I see what you mean
I can’t think of it off the top of my head, no
Just had enough times that I thought modifying the app state would be harmless and turned out… not so much
we're composing these things recursively, so you might end up with some children states you didn't intend...but that is my reasoning for taking parameters in the constructor.
right
so what to call it then? it is still kind of initial app state if it hasn’t existed before
oh i see
It is a constructor...there is no confusion there. It is used when you build a new component.
even when the component has already been constructed?
The only reason I'm considering using it in merge-state!
is that the function is meant to merge a new object into app state.
this causes a subtle difference between how initial affects progression and how merging an object from the server might act
Similar difference when doing mutations that create new things...but there is nothing that keeps you from explicitly calling these constructors to get your initial state (and I continnue to consider that your responsibility)
Then again, I could be convinced that these are just for initial app state, and you should do your own thing if you want some UI concern injected into your objects as you create/merge them into app state
@jasonjckn: @currentoor @therabidbanana Be interested in your input on this...kind of a possible "big deal" 🙂
side note on semantics I consider "constructors" to mean constructing an instance of a class, e.g. it would be called every time the factory is called. initLocalState is much closer to a 'constructor' than this function
@jasonjckn: Sort of...constructors initialize state, which is what this is doing. Factory in React creates a on-screen representation instance that just renders state
the state is in the app, and for all intents and purposes represents what there is of the "instance"
I would argue it is the constructor of the instance, and the fact that that is stored in a larger map is an artifact of implementation of the framework
I'm moving very rapidly to the view that it is initial app state after making that statement 🙂
Not sure I fully understand what we're talking about here - is there a good cookbook example with these new constructor things to take a look at?
I wish it was merely an artifact of the implementation, if i didn't have to think about the implementation at all that would be wonderful, but at the moment certain Constructor's like Tabs in a TabUnion aren't called( without much fore thought)
@therabidbanana: It's a way of being able to think about initial app state in the same context as queries...simplifies startup code quite a bit.
which is what led to this discussion...are they constructors on simply start-up helpers....or something else?
e.g. the fact that they accept parameters means you could leverage them in interesting ways in your mutations
all of those use-cases could be done with your own interfaces as well that you place on your component as static
is there a way to use the query fragments to automatically call constructors all the way down the tree
I think exposing access to the constructor would be nice, but not backing any functionality in with them. I agree that merging data would be potentially simpler in cases where you have the constructors params/values. But that should be a tool for the implementer
@jasonjckn: No need...you compose them like queries
the current implementation make initial-state feel more like a helper function because only one of your children gets constructed in a TabUnion. Whereas if the construction always happened unconditionally, it's simpler + i like the work constructor a lot more now
So, at the moment I'm still sticking with started-callback
to initialize union alternates
If we made an untangled/get-initial-state
function and used that for composition (like om/get-query
) then we could handle the union case automatically, I think
So, at the moment, I'm leaning towards the suggested name InitialAppState
for the protocol.
Not sure what to do for the merge-state!
cases from above...having a "constructor of new base state" is going to be pretty useful
I mean, users can certainly do (merge {:ui/checked true} incoming-object)
and then pass that to merge-state!
...but it isn't very good local reasoning in the context of a component
I guess them implementing their own protocol could fix that to: (merge (get-base-state Component) incoming-comp-data)
much less invasive, and leaves flexibility open...just doesn't feel as refined. Library design is always hard.
as much as om.next left a lot to be included, i still like how flexible it is by just giving you the building blocks
that could be useful, for example calling merge-state! for your tab in a tab union 😛
I think I've got the union thing solved...a lifecycle sounds like a big can of worms for misuse
anyways if you ever add that, you would want AppConstructor (initial-state) (on-app-started) or some protocol
It isn't really workable. The React onComponentMounted is the best you could do, and it would be identical...but chicken and egg problems
otherwise you have to scan the code for everything that is a defui component and run it's stuff...even if it isn't composed in in ways you understand (to-many, to-one?).
ok, well for the implementation, I would just walk the query tree , and call on-app-started 🙂
you need the data to resolve, but you don't have the data until you've called the callback...I don't know, maybe you're seeing something I'm not
I guess if you worked through all of the possible combinations and cases you could prove whether or not it could work. I'm not personally up for that at the moment, but I'd be game to look at such an analysis
The incremental steps I'm considering are: 1. Initial app state: I think we've solved this one 2. Some kind of "when making a new instance of this component in app state, here's stuff to include that won't come in from a server, like a checkbox state or child placeholder to ensure the child query is evaluated"
We've been relying on initLocalState for #2 mostly, though I know it's not ideal.
@therabidbanana: But if you use that, you don't get support viewer and good debug support
@jasonjckn: I was (earlier) suggesting that (1) could be used for (2), but I'm thinking now that that is a bad idea
Intial app state has children, which have initial state...some new thing coming in won't necessarily share that vision of what that subgraph should look like
I do like the support viewer and want to use it at some point, but we decided that for modals with forms where you might be changing state or canceling to revert, it was easier to go with local state. We might refactor at some point, but basically doing a transaction for every letter typed seemed like a bit heavy-handed solution (maybe we were doing something wrong there)
i wonder if we can make the default case in this framework where it's the same function for #1 # 2, but not preclude the user making #1 and #2 distinct
@therabidbanana: using component-local state for form field input is often what we do as well...for just that reason
@jasonjckn: not really about if we can 😉 About if we should
Okay, cool. It's the only case we rely on local state anyway, but that's really the only time we need extra ui/type fields to be initialized anyway - when editing data.
So for us the #2 case hasn't really happened outside of places where initLocalState isn't involved
i don't have enough use cases in my mind though, you're definitely more experienced than me, if 80% of apps have the same function for #1 and #2, then i think that should be the default (same function for #1 and #2)
The (2) case is rare here as well...mainly comes up when a child has only a link query...not any data of its own
@jasonjckn: 80%++ of the cases don't need (2) at all
On a mostly unrelated note - I'm to a feature I'm not sure how best to handle within untangled - autocomplete search. Has anyone here had to implement something like that/thought about how to?
And the server is going to talk to a slow third-party to generate the results - 1+ seconds will be the norm
so, remember that your renderer should be thought of as a pure function...you hand it data, it renders it. So your component will need state to represent exactly how it should look in all possible states. That is step one.
Step 2 is to set up a mutation that can trigger your external API request...typically through XHRIO or something...with callbacks and that whole mess
you'll need state to track in progress, etc...ok to close over app state and swap! on app state in the callbacks
Heh, yeah, that's why I said "mostly unrelated". 😄
For most of our selects we have this already: https://github.com/JedWatson/react-select - if we gave Select.Async a "loadOptions" callback we could probably circumvent mutations completely and rely solely on the local state of that component - it already does the debouncing, caching of results, etc
But that approach seems less than ideal.
I have not personally tried integrating external components, but I see no reason why it would not work...
maybe there are some re-render issues I'm not seeing, but can't speak to stuff I don't see 🙂
I'll give that approach a shot first, seems like the easiest way to deliver something, though potentially having these results in app state could be nice
(For our reporting project, we need to go and fetch a list of things you could filter the report on - if that list was cached somewhere in app state creating multiple widgets with same filters would potentially be quicker)
Yeah, that's what I'm debating between essentially
Go within the system we're already familiar with, or set up an entire sideband approach
In that case you could just shove off a load-data...the debouncing gets a bit more delicate
you might want to put a flag in app state that you set/unset when one is in progress, and use :parallel true on the load
for untangled-server: how do I choose the port 3001 using environment variables, i don't want to use a config file
@jasonjckn: use more than one config file, and use -DconfigFile 😉
then I need to generate config files using environment variables as the template parameters
We use environ in our project
+ a custom config component
Our default is meant for our production requirements: That you can't accidentally start something that isn't configured by the admin of the production system.
I'd be ok extending the config component to support a config file that references env variables, though, if anyone wants to contribute
sure that's a nice idea, but it's too restrictive, we have our own cluster setup and requirements
^ we use something like this
To boot the untangled system with a custom config component
Maybe we should add a cookbook for it?
@therabidbanana: so that code sets the untangled-server port right?
Yeah, port is the http port (8000 here, configurable via ENV[HTTP_PORT]). As long as untangled when it boots can find the port number at (get-in system [:config :value :port]) it'll be able to do the right thing.
avoids arbitrary names within the config from accidentally overwriting meta info in the config component itself
so in:
https://github.com/untangled-web/untangled-server/commit/c9d07f8b0f4582b210a99f80e3923866a1275e92
&
https://github.com/untangled-web/untangled-server/commit/3f2b61cbb22f944ca0df7eab52c409dd0fa9e090
i made it so you can reference environmental variables by having map values in the config be keywords with the namespace :env/ENV
or :env.edn/ENV
and it will look for them in System/getenv
the difference between env and env.edn is that i do a clojure.edn/read-string
on env.edn
yeah it kinda sneaked it
oh wow it really sneaked it, forgot to mention it in the changelog
since i wrote the config component, let me know if you need more or something changed
So, I've renamed Constructor to InitialAppState and pushed SNAPSHOT to clojars (updated training-2 as well)
None of the ideas for union initialization are working out...I'm still recommending merge-state for setting up those at startup
@tony.kay: what if we don't need every tab's app state initialized during application start-up, the problem we're solving is just default values for the app state, so if you have some API for getting the base state of a Tab, it'll be called when the user selects that tab, and with this we don't need to manually call merge-state! anymore
so the app state for the tab would get initialized automagically when choose-tab mutation is executed and untangled sees no default values
yes, but then you have to make extra logic in your mutation to see if the base stuff is there...and many tabs will have base state
ok, i thought maybe there's some logic you can write once in untangled for this, obviously i don't want to write this logic in the mutation
It is certainly something you can already do, but if you want your tabs to work out of the box with a simple mutation on an ident it would be nice to have them just work
For union components, IF you supply initial state, it MUST be a map with a default key
The you use a helper function (get-initial-state) instead of initial-state directly....so actually, it loks like this:
{:main (get-initial-state Main) :settings (get-initial-state Settings) :default :settings}
The get-initial-state helper detects the union case, and returns just the default branch (e.g. is would return the state for just Settings in the example)
Then, Untangled during startup can scan app state for this metadata and finish initializing them
yah, i'm open to it, personally i try to avoid adding new concepts where possible, with the current API at least initial-state always returns object data in every case
now you're adding a new type in a sense, the return value is object data OR special union data type
Unions are already an outlier that doesn't match the patterns (it has not state of its own)...this actually brings it more inline
i feel like there's more we could do along the lines I was talking about above, but that's the only other contender for this problem I think
"but then you have to make extra logic in your mutation to see if the base stuff is there...and many tabs will have base state" when you write code like (update-in [:current-tab 0] ...) it's short and sweet to update an ident, but i think this general pattern of checking for unitialized data could be quite powerful
this logic would come up again and again , you want to set an ident, and that object doesn't yet exist
yep. It is an initial state problem. Some of those you do solve in mutations (or remote queries)..but this one seems a common one where you know what you want to be there from the start
i think both things could be in untangled, your union fix, and some faciilty for automatically initializing state when the entity doesn't exist yet
I'll let you think about the latter...proposals are welcome...I'm going to keep my brain on task at hand
since you're adding magic, consider also just walking the Query to identify union case, here you're adding in meta data and walking the object-initial state structure
if you walked the query you wouldn't need these new concepts get-initial-state
and
{:main (get-initial-state Main) :settings (get-initial-state Settings) :default :settings}
nothing would change in the Constructor, the only difference is you would see union cases and call merge-state for the other tabsThink it through...Om does the query walk and normalization..I'd have to rewrite that or hack into it
maybe I don't understand, the walk seems pretty simple to me, postwalk the Root query, then locate all the unions, look at the OM meta data to grab the component used for the union and call merge-state!
Go read Om tree->db
...you're suggesting I rewrite that whole thing as far as I can tell..I see no way to do what you're saying otherwise
call initial-state for each component in the union and merge-state! the object data
so, then you don't have InitialAppState on the Union component at all..just the sub-components you want initialized
if you go this route than the Constructors never change 😄 they still just return (initial-state TabA nil)
I guess if the union initial-state return is a vector (to-many), then you skip this whole initialization for it
walking the AST should be straightforward (defmethod walkfn :default [] (recurse on children) ) (defmethod walkfn :union [] .... logic ... (recurse on children ))
@jasonjckn: Hm. As I suspected. I see no way to resolve the to-many to-one differences
Yes, walking the AST gets me the list of things to do for the to-one case, but that will be wrong in the to-many
you need to call initial state in the AST walk regardless to see which of the Tab is going to be initialized by default
it is a bit of a pain, because the union component itself is in the AST as a join...containing a union and union-entries
ok, what do we do if they don't supply an INitialAppState for the Union component itself...then we lose the detection
it is mysterious...I guess I'll add a log warning (which will not show in production)
Yeah, that'll work...only show the warning if the union children have initial app state but it is ignored because you forgot the intermediate
So trying to do this search through untangled as much as possible - so far I have - does it seem reasonable?
0. app state has :search-results/accounts {:results [] }, :search-queries {}
1. when search input is typed in, debounce: #(om/transact this [(search/query {:term %}])
2. search/query would run load-data-action for [:search-queries term] with a parallel post-mutation search/merge-results
3. read server-side makes actual request and returns data eventually
4. search/merge-results pulls queries out of some holder variable and puts them back into :search-results/accounts :results (merge and run distinct on existing results so we can search on results we've already pulled)
Alternative solution of skipping past untangled - I'd probably still want to be able to send a standard Om read to the endpoint, is it doable to get a hold of the networking code from the untangled client and make a request with it?
so in (1) you can put a marker that says you're in progress, which in turn can cause that mutation to become a no op while present
If you're skipping past Untangled, you should make your own request. Doesn't make sense to mix/match...the networking code would get too complicated trying to track what your side-band thing is doing...just use Closure XHRIO or something
Thought that would probably be the answer - we have stuff already set up for authentication and all that I'd have to redo going outside untangled, so hoping not to have to re-roll that. I'll see how the first approach works out.
FYI, automatic union initialization pushed on 0.5.3-SNAPSHOT...training-2 example updated.
There is a small bug where it adds some cruft, but that doesn't hurt anything..just an annoyance when viewing app state
@jasonjckn: That worked out. If you're using that stuff, I'd appreciate a trial run
no more manual merge-state! needed, and it auto-detects to-many and does nothing in that case...also got the warning in if you've forgotten the default state
@tony.kay: So i upgraded from 0.5.2 to 0.5.3 snapshot, changed Constructor to InitialAppState and took out the merge-state! for the tabs, when I load my app here's my app-state
:messages
{:tab
{:id :tab,
:which-tab :messages,
:results {:page-size 10, :type "message"},
[:listings [:which-tab]]
:untangled.client.impl.om-plumbing/not-found,
[:messages
[:which-tab {:results [:page-size :offset :items :query]}]]
:untangled.client.impl.om-plumbing/not-found,
[:settings [:which-tab :settings-content]]
:untangled.client.impl.om-plumbing/not-found}},
:listings
{:tab
{:id :tab,
:which-tab :listings,
[:listings [:which-tab]]
:untangled.client.impl.om-plumbing/not-found,
[:messages
[:which-tab {:results [:page-size :offset :items :query]}]]
:untangled.client.impl.om-plumbing/not-found,
[:settings [:which-tab :settings-content]]
:untangled.client.impl.om-plumbing/not-found}},
:om.next/tables #{},
:settings
{:tab
{:id :tab,
:which-tab :settings,
[:listings [:which-tab]]
:untangled.client.impl.om-plumbing/not-found,
[:messages
[:which-tab {:results [:page-size :offset :items :query]}]]
:untangled.client.impl.om-plumbing/not-found,
[:settings [:which-tab :settings-content]]
:untangled.client.impl.om-plumbing/not-found}}}
(defui MessagesTab [:which-tab
{:results (get-query SearchResults)}]
static InitialAppState
(initial-state [_ _]
{:id :tab
:which-tab :messages
:results (initial-state SearchResults nil)})
Object
(render [T]
(let [{:keys [results]} (props T)]
(div
(@SearchResults results)))))
(defui TabUnion {:listings (get-query ListingsTab)
:messages (get-query MessagesTab)
:settings (get-query SettingsTab)}
static Ident
(ident [T {:keys [which-tab]}] [which-tab :tab])
static InitialAppState
(initial-state [_ _] (initial-state MessagesTab nil))
Hm. It is possible I screwed up on extracting the query....if so, I'll fix it on Monday...I'm kinda done today 🙂