cyclejs/collection

Filtering Collection output

nicoespeon opened this issue · 6 comments

Hi everyone − and first of all, thanks for that great lib!

I have a use case on which I'd like to get your feelings before proposing a PR.

The use case

Let's say we have a Collection of Items and we would like to dynamically filter the collection like so:

image

The thing is that I don't know how to filter the output of Collection.pluck(items$, item => item.DOM) as it is an array of VDOM. I'd have liked to filter the output VDOM based on Item props.

As I couldn't figure out how to do so easily, so I used Collection.gather to update the Collection dynamically, based on filtered input: https://github.com/nicoespeon/webapp-exercise-cyclejs/blob/master/src/components/PersonList/PersonList.ts#L19-L29

The problem with my current solution

This is not ideal because:

  • that's not an obvious solution to implement. It takes me a few moment to imagine the proper way to do it since we'd imagine that filtering happen at the output of the Collection
  • if you want to keep the Collection ordered, then you need to fully reset it with filtered data, which requires you to hack out an id based on the search string so it'd work when the search string is changed
  • it's probably not performant since the Collection is reset every time we need to filter − didn't challenge that point though

The suggestion

My suggestion was to implement a Collection.filter method that would behave pretty much like Collection.pluck, but allowing to filter output items based on some property.

I'd like to get your feedbacks on that first: did I missed something? If not, what kind of API would seem consistent with the rest of the library?

Thanks 😄

is it possible to filter on the data before passing it to the Collection?

That's what I did here indeed: https://github.com/nicoespeon/webapp-exercise-cyclejs/blob/master/src/components/PersonList/PersonList.ts#L19-L29

Using Collection.gather and an ID that depends on the search string, I was able to implement dynamic filtering.

However, that means to figure out a custom implementation which may not be obvious + reset the whole Collection every time filter changes, which would not be necessary if we could filter the output I think.

Why don't you want to filter with css by adding a class to filtered items? It should be far faster.

Anyway, you can always do something like Collection.pluck(item$, item => predicate(item) ? item.DOM : '')

Why don't you want to filter with css by adding a class to filtered items? It should be far faster.

Didn't thought about it and that will be faster for the visual filtering. Thanks for the idea 👍

However I'd still have some "troubles" with that:

  • my Person component is a standalone one which is not aware of being in a Collection. Actually, it is also used elsewhere, without Collection. That's probably why I didn't go for modifying the Component code to implement filtering, which sounds much more of Collection's responsibility to me.
  • there is a VDOM part which is updated regarding the length of the Collection output, so it wouldn't work if filtering is done with pure CSS − even though I could probably re-implement this counter with pure CSS, but that's not really intuitive

Anyway, you can always do something like Collection.pluck(item$, item => predicate(item) ? item.DOM : '')

That's an interesting idea.

Actually I was suggesting here to implement a Collection.filter that would have pretty much the same signature, with a bit more of semantics.

The only thing here is that the filtering part is tangled with the plucking part, which I would differentiate:

  • there is the plucking part, which will work upon Component's sinks
  • there is filtering part, which should work upon internal stuff of the Collection, just as .gather uses an idAttribute to handle duplicates

And so if I want predicate(item) to work here, I need to expose filtering logic in item sinks so I can access it (a filterKey string, in my example).


To sum it up, I think that filtering the output consist in:

  1. defining a attributes to identify items in the Collection,
  2. then applying a predicate() function to determine whether each individual item should be part of the output or not,
  3. then there is the selector() function that is here to pluck a sink from the filtered Collection.

All of that in one Collection.filter(collection$, predicate, selector); function.

Or maybe even just a Collection.filter(collection$, predicate) which would return a filtered collection$ that can be used independently in a further pluck. That might keep things easier this way.

But I'm not sure on how to define attributes at the beginning. Something like Collection.gather does with the idAttributes.

What do you think? Is there anything that I didn't understand?

Actually, it is also used elsewhere, without Collection. That's probably why I didn't go for modifying the Component code to implement filtering, which sounds much more of Collection's responsibility to me.

Instead of doing the filtering in the component, it could be done inside of pluck.

What about something like this:

// Given persons$ is a collection, and each Person has a DOM and name$
// And filter$ is a stream of strings to filter persons$

const personVtrees$ = Collection.pluck(
  persons$,
  person => xs
    .combine(person.DOM, person.name$, filter$)
    .map(showViewIfMatchesFilter)
);

Where showViewIfMatchesFilter looks like:

function showViewIfMatchesFilter ([vtree, name, filter]) {
  if (name.includes(filter)) {
    return vtree;
  }

  const hiddenStyle = {display: 'none'};

  return {
    ...vtree,

    data: {
      ...vtree.data,

      style: hiddenStyle
    }
  };
}

This does require your Person to return a name$, but my rule of thumb is if you return any information as part of your DOM, it's fair to consider it public information and return it directly when convenient.

This is very comparable to the approach we use for filtering in the todolist example, but we have further abstracted to use functions:
https://github.com/cyclejs/collection/blob/master/examples/todolist/app.js#L125-L136

@Widdershin > Yup, I think you're right. Exporting name from Person component seems fair to me. The solution you proposed works like a charm.

I guess this was what @Hypnosphi got in mind too, but I didn't catch that precisely.

Thanks for your feedbacks. That's doing the job for me 👍
Eventually we might add the filtering scenario as a README example, but that may just be me who wasn't used to the philosophy.

Whatever, I'm closing the issue. Thanks again 😄