Closed staltz closed 6 years ago
@staltz I think it's worth to distinguish all those types of collection. For instance the signature of isolateEach
should change:
const tasks = sources.onion
.toCollection(Task)
.uniqueBy(s => s.key) // UniqueCollection
.isolateEach((key: string) => {DOM: `.${key}`, '*': key})
const tasks = sources.onion
.toCollection(Task) // Collection
.isolateEach((i: number) => {DOM: `._${i}`, '*': `${i}`})
I thought null
was already "no isolation", I would add that
Good news! I pushed some commits and I like what we got.
We have the API toCollection(Task).uniqueBy(s => s.key).isolateEach(key => key).build(sources)
. And it supports not using keys, this makes a lot of sense for examples/lenses
where the children are fixed amount (yay! keys are not always required). This wouldn't be possible without the discussion we had. Thanks @abaco @ntilwalli @jvanbruegge
I still need to make a new version of isolate and then make a new release candidate for this, based on the new isolate.
Okay released
@cycle/isolate
v3.0cycle-onionify
v4.0.0-rc.10I'm using v4.0.0-rc.13 and it's working flawlessly.
It's a small detail, but I'm wondering what happens when you call uniqueBy
or isolateEach
twice in the the same chain. Is the last invocation overriding the previous ones? Is that useful is some cases? Or maybe it's better to remove uniqueBy
from UniqueCollection
? That would help with autocompletion.
@jvanbruegge is there a special isolation scope I need to use the directsources.onion.select('myArray')...
approach instead of creating an isolating wrapper? I have a component that works when I wrap the collection but not when I select
directly.
Also, I agree with @abaco, I've been using the latest stuff and it's been working well. 👍
I've noticed two bugs, one issue, dunno if it's a bug...
1) Order of given list is not preserved. I am using the toCollection
feature to pass in a stream of arrays of results which can be filtered under different conditions. The results are stored in the parent state and then the filtered results are passed to the collection. I'm using uniqueBy
. Between one set of filters and another, some of the same results will be preserved and some will be removed by the filter. As it's currently coded the new results get appended to the collection instead of maintaining their array position as passed in. (This issue goes away when I remove the uniqueBy
)
Example: Initially no filter -> Results: [1, 2, 3, 4, 5] -> Displays: [1, 2, 3, 4, 5] Add filter -> Results: [3, 5] -> Displays: [3, 5] Choose different filter -> Results: [1, 2, 3] -> Displays:: [3, 1, 2]
2) Changing the filter requires me to click the screen for a re-render even though the results have changed. I just need to click somewhere on the screen. Nowhere specific. Like I have to click the screen to be like, "Hey, wake up!" (This issue is totally independent of the other issue)
Thanks for the report, @ntilwalli, I'll look at it when I have time. (FYI some of my days are dedicated to Cycle development and some days are dedicated to other work, so my commits happen in waves)
@ntilwalli I find it hard to reproduce this, because actually we have filters like this in TodoMVC: https://github.com/cyclejs/todomvc-cycle/blob/51613793e4ec3fbb33bd4c67edabf926f62fe6bd/src/components/TaskList/index.js#L8-L26
Can you show some snippets of your code that differ from the approach in TodoMVC? I'm very curious what is the root cause of the issue you found.
@staltz I'll need to reconstruct that example, when I get a chance I will.
A different question, when I comment out the .uniqueBy(...)
line examples/mixed/src/List.ts
,
the incrementing happens by squares of 2. It increments by 2^(number of times I've clicked). Seems weird. Same behavior when I comment both .uniqueBy
and .isolateEach
. Works fine when they're both kept in...
https://github.com/staltz/cycle-onionify/blob/collection/examples/mixed/src/List.ts#L24
Another question...
When I build my collection like this, the onion
sinks for the individual collection items gets subscribed and the state changes get processed.
const collection_sources = sources.onion.toCollection(Rule)
.uniqueBy(item => item.key)
.isolateEach(key => String(key))
.build(sources)
But when I build my collection like this, the onion
sinks for the individual collection items are never subscribed it seems...
const collection_sources = sources.onion.toCollection(Rule)
.uniqueBy(item => item.key)
.isolateEach(key => {
return {
DOM: `.recurrence-rule-${key}`,
}
})
.build(sources)
Why does the first one work but the second does not?
@ntilwalli These seem like legit bugs. I'm investigating them right now.
I fixed the bugs in this commit: https://github.com/staltz/cycle-onionify/commit/9f7db133f3d5d2fde1d7602aca9589709a6a55f0
Would be nice if @abaco could review. I deleted some internal code, which I'm sure I previously believed was necessary, but maybe things changed enough that it's not anymore necessary.
I also tested it, ran it on the examples in the repo, and on TodoMVC, and everything worked.
v4.0.0-rc.14 released with the bug fix and tests and types for RxJS and most.js.
I'm hoping this is the last rc
, but please let me know if anything else should happen first.
The only thing I'd do in code before that is probably #50.
I've tried to understand the latest fix, but frankly I can't... 😳 I can confirm it works in my app code, though
Is isolateEach
required? Without it, this example doesn't see click
events from the children. Go to the itemCollection.ts
file, for the line to play with...
https://www.webpackbin.com/bins/-Kp4xUMScSrfjuhG5Nwz
Also, the latest version does not work with RxJS: https://www.webpackbin.com/bins/-Kp52uHZfk9olP_b1A25
I remember you talked about this at cycleConf but it's only now I took the time to read the full thread and I must say this is really exciting!! I love the fluent interface you propose and I can't wait to see this released :)
Also it seems to fix the major issue I have with @cycle/collection
which is: reordering elements https://github.com/cyclejs/collection/issues/3
My first tests seems to confirm that it handles sorting gracefully, can you confirm it does?
I love how you pay attention to every single detail of the API design!! Keep up the awesome work guys 🤘
I might have found an edge use case with reordering elements. The problem occurs if the collection component has some kind of "internal state" (say a state$
that is not in the sinks of the component).
Here is a simple example of such a component
export function Item(sources) {
const { onion, DOM } = sources;
// ▼ that state is only present in the component's instance
const selected$ = DOM.select(":root").events("click")
.fold(state => !state, false);
return {
DOM: xs.combine(selected$, onion.state$)
.map(([selected, element]) => div(element.text + (selected ? '✔' : '')))
}
}
When reordering, that internal state gets affected to another instance. (if I select the 4th element and I reorder the collection, the new fourth element will still be selected but it is not the one I selected in the first place). Here is a webpackbin that illustrate this https://www.webpackbin.com/bins/-KqrgCYRa4cOFC780EbK
I guess it always better to output those kind of state to onion
(in which case there is no problem), but I guess people could easily get confused by that behavior. Do you think also sorting the component's instances would be possible?
This might be another bug. If I create a collection just after a select
, the collection output nothing.
onion.select("selector").toCollection()
Here is an webpackbin that illustrate that point https://www.webpackbin.com/bins/-KqwYMNfcAhFLvWONQLa. As you can see the listener outputs something while the collection doesn't
I remember you talked about this at cycleConf but it's only now I took the time to read the full thread and I must say this is really exciting!!
@atomrc Great to hear this! Thanks :)
About the first bug when reordering, yes, I'd recommend to put all the state in onionify. The rule of thumb is you shouldn't have any fold
.
About the second bug, it's not immediately obvious to me why it happens, I'll take a careful look when I can. Did you mean really .select('selector').toCollection()
or .select('selector').toCollection(Item)
?
Oh yeah sure it toCollection(Item)
of course. The webpack bin I linked has the failing case :) https://www.webpackbin.com/bins/-KqwYMNfcAhFLvWONQLa
I like the rule of thumb "no fold", I'll keep that in mind.
Ok so if it helps, I did some investigations on why the select(..).toCollection(Item)
doesn't work.
I think it has to do with isolation here https://github.com/staltz/cycle-onionify/blob/collection/src/Collection.ts#L180
I believe here isolate
will isolate from the whole onion
state, not from the selected state. So the onion
passed to the Item
instance will try to focus on the property 0
, 1
... of the root state, not the selected array.
So I guess it would work with a wrapper component that isolate the array and then call the toCollection
on it.
Hope it helps!
Is isolateEach required? Without it, this example doesn't see click events from the children. Go to the itemCollection.ts file, for the line to play with...
@ntilwalli Yes isolateEach is required if you want isolation. We opted for explicit isolation and customizable, so it's consistent with the rest of Cycle.js (explicit over implicit). I couldn't open your Webpackbins (its backend seems unresponsive) but I suppose that was the problem (the lack of isolateEach).
Shouldnt the parent get the events from the children without isolation?
I also found a bug in rc.14, if you have an initial state in your parent init reducer, the onion does not seem to use it. Same code with empty array as init reducer worked. Might be part of @atomrc 's problem
Shouldnt the parent get the events from the children without isolation?
I tested in an example and the behavior I got was sibling components mixing up their dom events, which is expected when you don't have isolation. I wasn't able to reproduce the "not getting events" behavior that he told, couldn't open the Webpackbin.
I also found a bug in rc.14, if you have an initial state in your parent init reducer, the onion does not seem to use it. Same code with empty array as init reducer worked. Might be part of @atomrc 's problem
I'm trying to reproduce that one now. Also doesn't help that Webpackbin isn't working there either.
Okay, about Webpackbin, it turns out it depends on Google server connections, which my computer blocks. So I'm using it from Tor.
here is my bug: https://gist.github.com/jvanbruegge/0b31b83b9acda1c1ff3e6f36d7f69303 Not 100% the bug i remembered, but still a bug: You would expect one counter with the number 5, but you get one counter with the default number (zero). In the debugger you see that the prevState is undefined
Already learned something about ntilwalli's bug:
ItemCollection was isolated but it didn't have a corresponding DOM element (because its DOM sink was an Stream<Array>
) and isolation on an array just doesn't work. I need to think how to solve this, but it's basically just Cycle DOM related.
So, I'm kind of tempted to label this a misuse of Cycle DOM, because DOM sinks are supposed to be Stream<VNode>
, but I'll still look how it could be possible to isolate a DOM Sink of Stream<Array<VNode>>
(sorry for the comment storm) Okay I think I know what to do: this isn't an onionify issue, it's Cycle DOM, and it's debatable whether we want to support array sinks. Currently the typing of isolation in Cycle DOM is clearly for Stream<VNode | null | undefined>
sinks.
@staltz not supporting isolation on arrays sounds reasonable to me, I realized later that isolation on arrays was a problem but didn't connect it (in my mind) with this bug report....
My outstanding bug reports are: 1) RxJS only works if the children component sinks are converted to xstream streams... 2) It does not seem possible to isolate a collection directly. Collections still seem to require a wrapper (https://github.com/staltz/cycle-onionify/issues/28#issuecomment-310849544)
@ntilwalli rc.15 released which works with RxJS. Thanks for testing and reporting.
I debugged the issue @ntilwalli raised about using a collection directly without a wrapper component like List
.
The TL;DR is: select
is just one part of isolation (often, isolateSource == select), so if you manually perform just select but not the other parts of isolation, you will get buggy behavior.
I noticed first that it's not enough to write sources.onion.select('list').toCollection(....
because later when call .build(sources)
on the collection, this sources
argument doesn't have onion
isolated to the 'list'
scope. So what you need to do is also isolate that source:
const items = sources.onion.select('list')
.toCollection(Item)
.uniqueBy(s => s.key)
.isolateEach(key => key)
- .build(sources);
+ .build({...sources, onion: sources.onion.select('list')});
That makes it possible to grow/shrink the collection of items. However, if you try to interact with a particular item, notice that items.pickMerge('onion')
is a stream of reducers which is not isolated for the scope 'list'
, so it's going to be a stream of (inner)Reducers that operates on individual array entries, and then you would need to wrap that with an outerReducer that operates on the root state object.
So I'm glad I found the root cause for the issue, but I'm left thinking what can we do to either make it obvious how we need to have a wrapper List
component, or find a different API. I think we've talked a fair bit about this before, e.g. abaco said in this thread:
Frankly I don't mind wrapping my collections in a component for the sole purpose of isolating the onion source/sink. I even put that component in a separate file. I actually like that.
But the main goal of components, as far as I see them, is not isolation, it's reusability. So I understand that Jan doesn't want to create a component that he won't ever reuse, just because he needs to isolate the collection. I think it's fair to expect that there's a way to directly isolate the collection, without any wrapper component. That's why I was thinking of a possible equivalent of isolate, but for collections not for components.
I still think that introducing the need for a wrapper is not a nice API.
One solution would be to add a lens option on the builder:
-const items = sources.onion.select('list')
+const items = sources.onion
.toCollection(Item)
+ .lens(L.prop('list')) // L is some lens lib, defaults to identity lens
.uniqueBy(s => s.key)
.isolateEach(key => key)
.build(sources);
(name is debatable)
I still think that introducing the need for a wrapper is not a nice API.
I agree.
One solution would be to add a lens option on the builder
I thought about that as well, and since it would basically perform isolation, we could call it isolateAll
as a sister API to isolateEach
.
well, it is not really isolation, it's just using a different part of the state If it would isolate the collection as whole, things get confusing with the two levels of isolation (collection + indivdual items), so I would keep the collection transparent
well, it is not really isolation, it's just using a different part of the state
Hmm, I can see what you mean. isolateEach
will perform isolation on all channels, while this new lens
/isolateAll
would do isolation only on onion
channel (and indeed it is isolation, because "using different part of the state" == "isolation semantics for onionify").
If it would isolate the collection as whole, things get confusing with the two levels of isolation (collection + indivdual items), so I would keep the collection transparent
But there are two levels (in fact, "layers") of isolation. One from {list: [obj1, obj2]}
to [obj1, obj2]
and another from [obj1, obj2]
to obj1
. And also the reverse direction when reducers are applied.
What do you mean with collection transparent?
Hmm, I can see what you mean. isolateEach will perform isolation on all channels, while this new lens/isolateAll would do isolation only on onion channel (and indeed it is isolation, because "using different part of the state" == "isolation semantics for onionify").
Follow-up: actually it doesn't hurt to isolate all channels, so we could indeed call it isolateAll
.
If you would isolate
with list
, wouldn't that be total isolation?
If I know use isolateEach with sibling isolation, I am confused what is happening
isolateEach isn't sibling isolation, it's just a call of normal isolate
on each Item
component for each array item. By the way, we don't need to even think about sibling-versus-total in this context because it's something specific to drivers. E.g. it only makes sense for the DOM driver, and onionify has no code specific to DOM.
This is what I mean:
const items = sources.onion
.toCollection(Item)
.isolateAll('list')
.uniqueBy(s => s.key)
.isolateEach(key => '.' + key)
.build(sources);
const itemClick$ = DOM.select('.myitem').event('click');
That would not work with isolateAll, or am I wrong? (If isolate all isolates all channels)
isolateAll should only apply the scope given to the collection's onion channel and leave the other channels with null
itemClick$
wouldn't emit anything with or without isolateAll, because you have isolateEach that already performs total isolation.
No, with the dot, the DOM driver does sibling isolation
Okay, you're right. isolateAll would make itemClick$ not emit anything.
And because we then do not isolate all, I would just call it something like lens
What's your idea for its purpose? Simply isolateSource/isolateSink on the onion channel?
I pushed to the collection branch an experimental feature where we borrow an API from Cycle Collection and improve performance under the hood.
The gist is:
Create many instances of a component using collection
pickCombine('DOM') is pick('DOM') + mix(xs.combine)
pickMerge('onion') is pick('onion') + mix(xs.merge)
Note: this is an experiment. That's why it's in a branch.
The motivation for this was to make onionify faster for large amounts of child components from an array. The main solution for this was
pickCombine
and the internal data structure of "instances" whichcollection
builds.pickCombine
is a fusion of pick+mix to avoid a combine, a flatten, and a map, and does all of these together and takes shortcuts to avoid recalculating things in vain.collection
was sort a necessity from two perspectives: (1) easier than creating and isolating item components by hand, (2) with this performance improvement it would have become even harder to do it by hand.Check the advanced example.
We're looking for feedback in order to find the most suitable API that feels quick to learn for the average programmer, while solving the problems below too. In other words, the challenge here is API design for developer experience. The challenge is not "how" to solve the technical problem.
Open problems/questions:
mixed
?collection()
does a lot under the hood (creates the instances data structure, calls onionify, picks the key from each item state, creates item state lenses, onionifies each child component, etc). Do we want to have heavy configuration with many arguments, or do we want to break that down into other helper functions? And how?key
. Do we still want to allow keyless items in a list?Checklist before final release:
asCollection
totoCollection