https://github.com/cyclejs/collection
An easier way to do collections in Cycle.js
https://github.com/cyclejs/collection
Last synced: 10 months ago
JSON representation
An easier way to do collections in Cycle.js
- Host: GitHub
- URL: https://github.com/cyclejs/collection
- Owner: cyclejs
- License: mit
- Created: 2016-05-23T05:54:55.000Z (about 10 years ago)
- Default Branch: master
- Last Pushed: 2018-12-14T02:55:52.000Z (over 7 years ago)
- Last Synced: 2025-08-15T18:47:22.236Z (11 months ago)
- Language: JavaScript
- Homepage:
- Size: 165 KB
- Stars: 60
- Watchers: 10
- Forks: 10
- Open Issues: 10
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# cycle-collections
Collection is no longer actively maintained. We recommend you check out [@cycle/state](https://cycle.js.org/api/state.html)
---
An easier way to do collections in Cycle
Components can be hard to manage in Cycle.js. They can be especially painful when you're working with lists of components.
`Collection` is a helper function that makes managing your lists of components a cinch.
Installation
---
```bash
$ npm install @cycle/collection --save
```
How does it work?
---
```js
import Collection from '@cycle/collection';
```
Let's say we have a `TodoListItem` component, and we want to make a `TodoList`.
```js
function TodoListItem (sources) {
// ...
return sinks;
}
```
You can make a collection stream by calling `Collection()` and passing it a component.
```js
const todoListItems$ = Collection(TodoListItem);
```
It's common in Cycle that you want to pass your `sources` to your children. You can pass a `sources` object as the second argument. Each item in the collection will be passed these sources.
```js
const todoListItems$ = Collection(TodoListItem, sources);
```
To actually populate the collection, you pass an `add$` stream. Its emitted values may be sources objects, which will be merged with the sources object you passed when you created the `collection$`. This is useful for passing `props$`. For Collection versions 0.6.0+ (based on Cycle Unified), any stream type supported by Cycle.js should work automatically. See the [old README](https://github.com/cyclejs/collection/tree/47e988ba5bf19d2a8172aba1f581200335f46b70#diversity) if you are older versions of Cycle.js (Cycle.js Diversity).
```js
const todoListItems$ = Collection(TodoListItem, sources, xs.of(additionalSources));
```
`add$` can emit an array, if multiple items should be added at once.
```js
const todoListItems$ = Collection(TodoListItem, sources, xs.of([firstSources, secondSources]));
```
`Collection()` returns a stream with arrays of items as values. Those arrays are cloned from internal ones, so changes will not impact the state of the `collection$`.
Collections are **immutable**. This is because in Cycle.js values that change are represented as streams.
If we put it all together in our `TodoList` it looks like this:
```js
function TodoList (sources) {
const addTodo$ = sources.DOM
.select('.add-todo')
.events('click')
.mapTo(null); // to prevent adding click events as sources
const todoListItems$ = Collection(TodoListItem, sources, addTodo$);
const sinks = {
DOM: xs.of(
div('.todo-list', [
button('.add-todo', 'Add todo')
])
)
}
return sinks;
}
```
Wait, how do we get the `todoListItems` to show up in the `DOM`?
---
`Collection.pluck` to the rescue!
```js
const todoListItemVtrees$ = Collection.pluck(todoListItems$, item => item.DOM);
```
`Collection.pluck` takes a collection stream and a selector function and returns a stream of arrays of the latest value for each item. Selector function takes the sinks object and returns a stream. So for the `DOM` property each item in the stream is an array of vtrees. It handles the map/combine/flatten for you and also ensures that any vtree streams have unique keys on their values. This improves performance quite a bit and helps snabbdom tell the difference between each item.
We can now map over `todoListItemVtrees$` to display our todoListItems.
```js
function TodoList (sources) {
const addTodo$ = sources.DOM
.select('.add-todo')
.events('click')
.mapTo(null); // to prevent adding click events as sources
const todoListItems$ = Collection(TodoListItem, sources, addTodo$);
const todoListItemVtrees$ = Collection.pluck(todoListItems$, item => item.DOM);
const sinks = {
DOM: todoListItemVtrees$.map(vtrees =>
div('.todo-list', [
button('.add-todo', 'Add todo'),
div('.items', vtrees)
])
)
}
return sinks;
}
```
But wait, there's more!
---
There is another common and hard to solve problem with lists in Cycle.js.
Say our `TodoListItem` has a 'remove' button. What happens when you click it?
A `TodoListItem` can't really remove itself. The state of the parent element needs to change.
All that a `TodoListItem` can do is return a `remove$` stream as part of it's `sinks`, along with `DOM`.
Normally, to solve this problem you would need to create a circular reference between the sinks of the items in your collections and the stream of `reducers` you're `fold`ing over. This is achieved using `imitate` in `xs` or `Subject` in `rx`. This can be tricky code to write and read, and often adds quite a bit of boilerplate to your component.
When you create a `Collection` you can optionally pass a `removeSelector` function that returns a stream which will trigger item's removal.
```js
const todoListItems$ = Collection(TodoListItem, sources, add$, item => item.remove$);
```
All together now!
```js
function TodoList (sources) {
const addTodo$ = sources.DOM
.select('.add-todo')
.events('click')
.mapTo(null); // to prevent adding click events as sources
const todoListItems$ = Collection(TodoListItem, sources, addTodo$, item => item.remove$);
const todoListItemVtrees$ = Collection.pluck(todoListItems$, item => item.DOM);
const sinks = {
DOM: todoListItemVtrees$.map(vtrees =>
div('.todo-list', [
button('.add-todo', 'Add todo'),
div('.items', vtrees)
])
)
}
return sinks;
}
```
And how do we process fetched data?
---
It's a quite common use case when a collection is built from fetched data. Usually it comes in a form of items' state snapshot. `Collection.gather` takes a stream of those snapshots and turns into a stream of collections. It takes `Collection` and `sources` arguments, just as `Collection` does, plus the snapshots stream `itemState$`, an optional `idAttribute` argument, which defaults to `'id'`, and an optional `transformKey` function for converting source keys.
```js
const tasks$ = Collection.gather(Task, sources, fetchedTasks$, 'uid', key => `${key}$`) // converts 'props' in snapshots to 'props$' in sources
```
It uses a set of rules:
- items are keyed by `idAttribute`.
- items that weren't present in the previous snapshot are added to collection.
- each added item tracks it's own state, turning the sequence of each field's values into a source.
- item is removed from collection if it's no more present in a snapshot.
So what if our components issue HTTP requests?
---
There are kinds of sinks that rather represent actions than states. HTTP sink is a good example. If we want to get a stream of all HTTP requests issued by collection's items, `Collection.merge` will provide us one. It works basically the same as `Collection.pluck`, but merges the sinks instead of combining them into array.
```js
const tasksRequest$ = Collection.merge(tasks$, item => item.HTTP);
```
Importing `Collection` directly is the same as calling `makeCollection()`.