Armed with the
concatAll functions, we can create fairly complex interactions in a simple way. We will use Observable to create a simple drag and drop example with basic DOM elements.
Now that we know how an Observable works, I want to draw your attention to the fact that we can actually build complex asynchronous programs using exactly the methods that we've already learned thus far, map, filter, and concat all.
Let's go back and take another look at that example of filtering the stock exchange. Let's jump back here. I want you to take another look at this example where we created a stocks collection by mapping over the exchanges collection.
We took each exchange and converted it into another collection. We mapped it into another collection by returning the stocks inside of that exchange but first filtering them so we only returned those stocks inside each exchange's array where the price was larger than a hundred.
Because for each item in a collection we are returning yet another collection, that means we have a two dimensional collection, just the same way that if I were to take one, two, three and I were to map it and return another collection. I would end up with this rather silly collection.
I'd end up with a two dimensional collection because for each item in the collection I'm returning yet another array. That's effectively what's going on down here. For each item in the exchanges collection we're returning another array, specifically the array of stocks.
Before we return that array of stocks we filter it so that only those stocks with a price larger than or equal to a hundred is returned. Then we take that two dimensional collection and we apply a flatten so that it's complete flat.
We've used concat all to take a two dimensional collection and flatten it by one dimension to a one dimensional collection. Finally we consume the data with for each. What would you say if I told you we could take code with nearly exactly the same structure and build a drag and drop with it? Watch this. I want you to look at this code that I've highlighted right here at the bottom of the screen.
Do you see that? I'm going to highlight this section of the screen. I'm going to switch over. Notice the two highlighted sections of screen. Look very carefully. I'm going to switch back and forth just a little bit. They have nearly exactly the same structure.
What is a mouse drag event? A mouse drag event is all of the mouse downs mapped into all of the mouse moves on a widget until the next mouse up. That creates a two dimensional collection. We flatten that two dimensional collection to create a stream of drags. Don't worry. We're going to go over this in more detail.
Then I for each over the drags and I do something, in this case move the widget around to the drag position on screen. Let's break this drag and drop example down. Shall we? Let's take a look. I'm going to bring up the html window so we can see what's going on here.
What we have is a red parent widget right here. That's the red box that you see on the right hand side of the screen. Inside we have another div which is just a widget with the words "drag me" inside. That's blue. What we'd like to do is be able to drag the blue widget across the screen inside of the red parent.
Now that we see that's just a very simple two nested divs, I'm going to get rid of the html window. Now inside of our code, first we're going to grab a reference to the parent. Then we're going to grab a reference to the widget, the parent being red, the widget being blue.
Then we are going to take each one. We're going to capture each one of the events that we're interested in, first the mouse downs on the widget then the mouse moves on the parent and the mouse ups on the parent, and we're going to convert all those DOM events to Observables so we can combine them together using the map, filter, concat all, and for each methods that we learned so far and then use for each to consume the collection that we build.
We're going to take the collections that we have, and we're going to use map and concat all to combine them into the collection we want. What we want to do is use these collections to build a new collection, a collection of all the drags on the widget.
We return all the mouse downs, but first we map over them. For every mouse down that occurs we return all of the mouse moves that we detect on the parent. But before returning that collection first we reduce the number of items in that collection by applying this new function, which I'll explain later on, called takeUntil.
What takeUntil does is it returns all of the items in a source collection, in this case mouse moves, but it completes the collection as soon as an item is detected in another collection, in this case a stock collection. We get all of the mouse moves until the next mouse up.
For each mouse down we are returning all of the subsequent mouse moves detected on the parent until a mouse up is detected on the parent. Notice that is, once again, a two dimensional collection. If I take an array and map over it and return another collection for each item in that array, I end up with a two dimensional collection as we saw in the previous example.
If you're returning another collection inside of the map function, you're going to end up with a two dimensional collection. How do we flatten a two dimensional collection? Concat all. What we end up with are all the mouse moves that happen between a mouse down and a mouse up. Those are the event objects that make it through into our drags collection.
Finally all we have to do is for each over the new drags collection that we've created and then reposition the widget at the location where we've detected a drag. Let's see if it works. As you can see, we've built a mouse drag.
I want to call your attention to a couple of things. Remember the for each method on an Observable in this case is abbreviated. Actually if we expand it out it's a little more complicated. Remember that for each returns a subscription object that we can use to stop listening to mouse drags.
Furthermore, for each can accept those two extra methods. It accepts three callbacks. The first is for the data that arrives. The next is for in the event an error occurs. Then finally a callback that will fire when the asynchronous collection completes.
The reason I've omitted these two callbacks in this scenario is that an error is never going to fire. In this particular case none of these DOM events emit errors, so I know for certain that an error is never going to come through here.
It's possible that if I throw, for example, inside of my map function or I do something stupid that causes an error to occur, that we might have an error end up in here. If I were to do something like throw an error in here, I want you to notice that Observable will take any errors that come in this expression inside of a map or a concat all or a filter and will automatically propagate them here.
Effectively it behaves as though there's a tricatch around this entire expression because any error that occurs in any of the functions that I provide will automatically be forwarded to this on air call. Now as I discussed, as long as we haven't done anything exceedingly stupid, this is never actually going to get called.
Finally, what about the on completed handler? The on completed handler is not going to get called either because these are based on DOM events and DOM events are never going to end. In other words, there could always be another mouse down. We could always begin dragging again, which means this collection will never really end.
I never really want to stop listing to it either, that's why I haven't bothered creating the subscription object. That's why I'm not bothering listening for a completion event. As you can see, these two functions are optional in this particular case because neither errors nor completion will ever come through. But they're there and they can be passed in.
Soon we're going to learn about situations where Observable streams do end, like, for example, if we create an Observable that represents an asynchronous computation or a WebSocket. Both of those things could end, and in those cases on completed is going to come in handy.
For now if we're just combining events together, which are just streams that go on forever, we don't need to worry so much about on air and on completed. Of course if I want to stop listing for a drag I can always just go subscription.dispose.
If I were to do this right here, it would be kind of silly because subscription.dispose would execute synchronously long before I had a chance to drag anything. I'm just going to leave that off. But if we did want to stop listing when another button was clicked, that would be something we could do.
Here's something else I want to call your attention to. Up until now you might have been thinking, "All of these array operations we're using like map, filter, and concat all, it's kind of expensive. Aren't they? I mean, every single time we map over an array we create an entirely new array. And every time we filter over an array we create an entirely new array."
Hopefully, now that you've seen what we plan to do with Observable, you're less concerned about this. The reality is Observables are very, very cheap to make copies of because they don't store items in memory. They're not really collecting up items in memory anywhere.
As soon as an item is received by an Observable it might simply forward it along by invoking another callback. Observables, at the heart of it, are just really objects with a for each function. They turn out to be very, very cheap to create and copy and clone.
That's why we've been taking this map and filter and copy, devil-may-care approach until now. It's not because doing this with arrays isn't inefficient. It's because when we move on to using Observables it's really not that inefficient.
Another thing I want to call your attention to is that for each on an Observable is relatively complicated. No longer do we just have to pass this callback. Sometimes if an error can occur we also have to handle errors.
When we nest for eaches things get a heck of a lot more complicated because then we need to trap...it's just like if you nest error callbacks nodejs where you can receive errors. You need to handle errors at multiple levels.
That's why from now on we're going to be doing everything we can to avoid nesting for each functions. Instead we're going to be using concat all to always flatten Observables so that when we call for each we don't ever need to nest two for each calls because otherwise we'd have to nest on air handlers and handle errors in multiple places.
As I discussed earlier, any error that occurs here is going to get propagated to this handler. That's why from now on we're going to learn to always use concat all so that by the time we use for each we're always operating on a flat collection.
Stay tuned. From now on we're going to jump back. I know it's exciting because we've started playing with Observables. We're going to jump back to arrays so that we can really master transforming collections with the methods we've learned so far. We're going to do a little bit more on flattening.
We're also going to learn a couple of new methods, really just a few more, before we dive head first into much more complicated asynchronous programs where we not only combine events together but we combine events, asynchronous requests, and animations. Stay tuned.