knockout / knockout

Knockout makes it easier to create rich, responsive UIs with JavaScript
http://knockoutjs.com/
Other
10.45k stars 1.52k forks source link

map/filter/reduce helpers on observables. #1834

Open benjamingr opened 9 years ago

benjamingr commented 9 years ago

Hey, I'm just wondering if there is any work done on computed observables having Rx like capabilities by being able to create descendent computed observables via .map .filter .reduce flatMap etc.

For example:

var o = ko.computed(() => 2 );
var four = o.map(x => 2 * x); // new computed observable, depends on the old one.
// waits for the promise to resolve before emitting change event
var xH = o.flatMap(x => $.getJSON("/api/users/"+x)); 

It sounds like a few extra lines of code could make programming the non-ui parts a lot easier with Knockout, it doesn't have to be a full reactive-extensions library like an old closed issue suggested - it would just help a lot with sugar since I often find myself writing:

     var four = ko.computed(() => return o * 2)); // have to reference the other one explicitly, not fun
     var xH = ko.observable("");
     four.subscribe(function(result) {
          $.getJSON("/api/users" + result).then(xH);
     });

Which is a lot clunkier and somewhat more error prone.

brianmhunt commented 9 years ago

@benjamingr There are a number of excellent plugins that you can use, including:

These are well tested, and worth checking out.

Since there seems to be no apparent issue with KO per se, I'll close this, but please let me know @benjamingr if you think there's something you feel the KO core could be doing better. :grin:

Cheers

benjamingr commented 9 years ago

Hmm, it looks like all these are for arrays. I was thinking adding declarative method to observables - not arrays - as observables themselves are multiple values over time :)

brianmhunt commented 9 years ago

@benjamingr You use any observable with these plugins by using the .extend({trackArrayChanges: true}) extender.

If you are doing an arbitrary transform, the “canonical” way is to use a computed.

benjamingr commented 9 years ago

I'm terrible at expressing myself :D I'm well aware of what computed observables are - I'm just saying it'd be super useful to have a more declarative way to create them - as in the examples in the original post.

Potentially aligning with ES observables would also be nice (but unrelated, maybe I'll open another issue after the next TC meeting).

brianmhunt commented 9 years ago

Thanks @benjamingr – appreciate your tenacity :)

Would an extender along the following lines be what you've got in mind:

ko.extenders.transform = function(target, fn) {
  return ko.computed({
     read: () => fn(target()),
     write: target,
     owner: target
  })
}

Then one can do things like x = ko.observable().extend({ transform: (r) => r.toLowerCase() }) ... and other syntactic sugar.

Once could of course do this for specific transforms, but above feels like the generic case.

benjamingr commented 9 years ago

Well, writing ko.observable().extend({ transform: (r) => r.toLowerCase() }), is very verbose :) A lot more than ko.observable(3).map(x => 2*x).

The interesting case is flatMap, where you want to wait for a promise (like in Rx), this is the really interesting case:

// flatMap can be named something else
// can substitute $.getJSON for `fetch`, `$http.get`
y = ko.observable("5").flatMap(x => $.getJSON("/api/users/3"))`

Where the returned computed observable updates when the $.getJSON call returns.

In general, these methods are super useful in observable libraries - just 3-4 methods would go a long way.

brianmhunt commented 9 years ago

@benjamingr Ok, I think I get it... I'm with ya, now. :) Which functions were you proposing should be replicated by ko, and what's a good precedent?

Reopening to keep this alive and for discussion.

The two concerns I have are:

  1. name collisions with the plugins i.e. .map, etc., on observableArrays
  2. adding non-generic functionality to ko that can easily be done by end users i.e adding .map and .flatMap and .reduce and .reduceRight, if these can easily be accomplished by e.g. something like the transform extender above. In other words, my personal feeling is that the core library should be generic and flexible. But I am open to persuasion. :)
IanYates commented 9 years ago

The thoughts here are neat and are ending up being a blend between KO and Rx. Rx offers a bit more though in that you can compose multiple streams and so on. I use Rx heaps in my server-side C# code and toyed with using it for the client-side UI but ended up not doing so. For the flatMap() proposal, there are various discussions, including in the KO GitHub Wiki, about an "asyncComputed". I wrote my own which probably has many flaws. I would certainly like to see this sort of thing properly implemented once rather than each of us having our own slightly different versions.

But, how far should it be taken?
Is it possibly better to see what some of the KO/Rx libraries already do and see if one of them can be "blessed" as the library the community tends to use (for me I see this similar to how I made extensive use of @rniemeyer 's AMD Helpers library before components became a part of KO).

benjamingr commented 9 years ago

So, here is what I had in mind:

mbest commented 9 years ago

@benjamingr, I don't see how adding these methods would be that useful. But even if I'm wrong, this seems like a perfect opportunity for you to create a plugin.