2012-12-11

Bacon.js Tutorial Part III : AJAX and Stuff


This is the next step in the Bacon.js tutorial series. I hope you've read Part II already! This time we're going to implement an "as you type" username availability check with AJAX. The steps are
  1. Create an EventStream of AJAX requests for use with jQuery.ajax()
  2. Create a stream of responses by issuing an AJAX request for each request event and capturing AJAX responses into the new stream.
  3. Define the usernameAvailable Property based on the results
  4. Some side-effects: disable the Register button if username is unavailable. Also, show a message.
I suggest you checkout the example code and switch to the tutorial-2 branch which will be the starting point for the coding part of this posting. If you just want to have a look at what we'ge got so far, have a peek.
So, at this point we've got a Property called username which represents the current value entered to the username text input field. We want to query for username availability each time this property changes. First, to get the stream of changes to the property we'll do this:
username.changes()
This will return an EventStream. The difference to the username Property itself is that there's no initial value (the empty string). Next, we'll transform this to a stream that provides jQuery compatible AJAX requests:
availabilityRequest = username.changes().map(function(user) { return { url: "/usernameavailable/" + user }})
The next step is extremely easy, using Bacon.UI.js:
availabilityResponse = availabilityRequest.ajax()
This maps the requests into AJAX responses. Looks very simple, but behind the scene it takes care of request/response ordering so that you'll only ever get the response of the latest issued request in that stream. This is where, with pure jQuery, we had to resort to keeping a counter variable for making sure we don't get the wrong result because of network delays.
So, what does the ajax() method in Bacon.UI look like? Does it do stuff with variables? Lets see.
Bacon.EventStream.prototype.ajax = function() {
  return this["switch"](function(params) { return Bacon.fromPromise($.ajax(params)) })
}
Not so complicated after all. But let's talk about flatMap now, for a while, so that you can build this kind of helpers yourself, too.

AJAX as a Promise

AJAX is asynchronous, hence the name. This is why we can't use map to convert requests into responses. Instead, each AJAX request/response should be modeled as a separate stream. And it can be done too. So, if you do
$.ajax({ url : "/usernameavailable/jack"})
you'll get a jQuery Deferred object. This object can be thought of as a Promise of a result. Using the Promise API, you can handle the asynchronous results by assigning a callback with the done(callback) function, as in
$.ajax({ url : "/usernameavailable/jack"}).done(function(result) { console.log(result)})
If you try this in you browser, you should see true printed to the console shortly. You can wrap any Promise into an EventStream using Bacon.fromPromise(promise). Hence, the following will have the same result:
Bacon.fromPromise($.ajax({ url : "/usernameavailable/jack"})).log()
This is how you make an EventStream of an AJAX request.

AJAX with flatMap

So now we have a stream of AJAX requests and the knowhow to create a new stream from a jQuery AJAX. Now we need to
  1. Create a response stream for each request in the request stream
  2. Collect the results of all the created streams into a single response stream
This is where flatMap comes in:
function toResultStream(request) {
  return Bacon.fromPromise($.ajax(request))
}
availabilityResponse = availabilityRequest.flatMap(toResultStream)
Now you'll have a new EventStream called availabilityResponse. What flatMap does is
  1. It calls your function for each value in the source stream
  2. It expects your function to return a new EventStream
  3. It collects the values of all created streams into the result stream
Like in this diagram.
flatMap
So here we go. The only issue left is that flatMap doesn't care about response ordering. It spits out the results in the same order as they arrive. So, it may (and will) happen that
  1. Request A is sent
  2. Request B is sent
  3. Result of request B comes
  4. Result of request A comes
.. and your availabilityResponse stream will end with the wrong answer, because the latest response is not for the latest request. Fortunately there's a method for fixing this exact problem: Just replace flatMap with flatMapLatest (previously called "switch") and you're done.
switch
Now that you know how it works, you may as well use the ajax() method that Bacon.UI provides:
availabilityResponse = availabilityRequest.ajax()
POW!

The easy part : side-effects

Let's show the "username not available" message. It's actually a stateful thing, so we'll convert the availabilityResponse stream into a new Property:
usernameAvailable = availabilityResponse.toProperty(true)
The boolean value is used to give the property a starting value. So now this property starts with the value true and after that reflects that value from the availabilityRequest stream. The visibility of the message element should actually equal to the negation of this property. So why not something like
usernameAvailable.not().onValue(setVisibility, unavailabilityLabel)
Once again, this is equivalent to
usernameAvailable.not().onValue(function(show) { setVisibility(unavailabilityLabel, show) })
... the idea in the former being that we partially-apply the setVisibility function: Bacon will call the function withunavailabilityLabel fixed as the first argument. The second argument to the function will be the value from theusernameAvailable.not() Property.
Finally, we'll also disable the Register button when the username is unavailable. This is done by changing
buttonEnabled = usernameEntered.and(fullnameEntered)
to
buttonEnabled = usernameEntered.and(fullnameEntered).and(usernameAvailable)
That's it for now. The result code can be found in the tutorial-3 branch.
Ideas for Part IV?

17 comments:

  1. How to implement the zip function ?

    ReplyDelete
    Replies
    1. Oh, the zip function, right. Implementation would be somewhat similar to the merge implementation. Do you really need it? I decided to implement it the first time I actually need it. It's been a year or so, and still not a single actual use case :)

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. For the mysterious gentleman who added a comment and removed it:

    1) From the garbage collection viewpoint, having an EventStream on top of DOM events is similar to using jQuery events only, except that with an EventStream it's much easier to ensure that the event listener relationship is properly cleared. Just say element.asEventStream(...).take(1) for instance, to clear the listener after just one event. There's no central bookkeeping in Bacon.js, so your streams are eligible for GC when nobody has a reference to them. Also note that the Bacon.js jQuery-based streams are lazy; they add the listener to the jQuery source object only when they have listeners themselves, and clear this listener automatically when they're done.

    2) Assume you want to listen to events dynamically in the sense that you listen to events from element1 first and later switch to element2, while keeping the same listener listener listening to the same stream. Now you'll have at least three options.

    2A) Use a Bus (implements EventStream). Manually push events to the Bus from the sources you like.

    2B) Use jQuery live selectors. Assuming that all your dynamically added/removed elements are under element $(".form") and have the class "component", you can do $(".form").asEventStream("keyup", ".component") to get events from all those and let jQuery do the bookkeeping for you.

    2C) Use flatMapLatest. You might have a "changeEvents" stream from where you spawn new streams using flatMapLatest. The resulting stream will always include events from the latest spawned stream. Bacon.js will do the bookkeeping, adding/removing the listeners to/from the spawned streams.

    ReplyDelete
  4. Something on lifecycle?

    Covering the basics, like how to end a stream either from the subscriber or the publisher or unsubscribing from a stream.

    Some web specifics would also be great, for instance:
    - what happens when the DOM node the event was triggered from (using Bacon.fromEventTarget) was removed from the DOM?
    - Does Bacon keep a hold of the reference to the node using fromEventSource, or is there a way to clear it? (looking at the source, it looks like it might leak, because the reference to the DOM element is held as long as no subscribers explicitly state they no longer want to receive events). If so what do we need to do to ensure the node gets GC-ed
    - in the scenario where an area of the screen is redrawn; the DOM element which pushed events no longer exists, but another is now in its place. We don't want to lose the subscribers to the stream, so we want the original event source; but can something else be triggering the events? (I'm thinking that a function needs to be bound to the node event e.g. using jQuery.bind, which when the event is fired causes a callback which pushes the event to a bus perhaps?)

    ReplyDelete
  5. I guess I partly answered your questions already. What do you think?

    Anyway, your actual observers (the onValue cases) don't need to worry about return values. It's usually better to use take(n), takeUntil or takeWhile to stop handling events after they are no longer desired. When an element is actually removed, well, would you have to remove your listeners if you were using jQuery or DOM events? If yes, you should use an appropriate end condition for your stream too. As you've seen, the Bacon.js interface to these sources is very simple. Please enlighten me if there's something I've missed!

    ReplyDelete
  6. I was the mysterious gentleman. I was reformulating my comment; Blogger doesn't seem to let you edit, but I only found that out after hitting the 'delete' button, because the buttons are in what to my untrained eye looks like Finnish(?), which I unfortunately can't read.

    Thanks for the responses. 1), 2a) and 2b) make sense (2b) is especially nice.) I haven't looked at flatMapLatest yet, but I don't really understand what it does. Am I understanding correctly that you are suggesting to a) create a new DOM event stream e.g. using $('.x').asEventStream('click'), b) create a 'changesEvent' stream from which I have a flatMap which references the latest DOM event stream, and c) have listeners listen to that?

    e.g.
    var xStream = $('.x').asEventStream('click');
    var changesStream = Bacon.once(1).flatMap(function() {
    return xStream;
    });
    changesStream.onValue(function(val) {
    $('.y').text(val);
    });

    A few example/explanation of that would be nice if you are looking for further blog post topics (I did see the ajax one, but a few other scenarios might be nice.) Also, some examples of using properties and some of its specifics (e.g. sampledBy) would be nice.

    The scenario I was thinking of for 1) is:
    a) the screen is drawn
    b) a stream is created using Bacon.fromEventTarget
    c) a listener is added to that stream listening continously for new events (e.g. a listener which validates input from a text field.)
    d) the screen is redrawn.
    e) the listener hasn't stopped listening for events, but the original element no longer exists.
    f) however, the event handler isn't unbound from the element, because the listener is still listening for events.

    I've only been using the library since this morning but really enjoying it so far - thanks!

    ReplyDelete
  7. Holy shit! You can't read Finnish?! Ok, I'll have to change some blog settings immediately, as it now seems that not everybody in the world is Finnish :)

    ReplyDelete
  8. I don't have a full-solution based on flatMapLatest for you, but I'll try to describe the general idea.

    So, you start with some Property that describes the state of your application. This property wil later be used for both switching the UI to different state and switching the result Property to receive input from the current UI DOM elements. Say

    var state = $("#selectedBrand").asEventStream("change").toProperty()

    You want your UI as a whole to provide you with a Property called, say currentFormData, that contains the whole user input. The result from this Property should eventually contain the values from the latest DOM elements, of course. Let's try:

    var currentFormData = state.flatMapLatest(function(newState) {
    var newUI = updateUI()
    return newUI.formData()
    }).toProperty()

    So now your UI will update each time the "state" property changes. The updateUI function does what ever DOM changes are required and returns a new Property that is composed of the event streams from the new DOM elements. The flatMapLatest thing will now switch between these Properties, cleaning up all the references to the old ones when you switch state.

    Does this make sense?

    Btw I actually discovered that flatMapLatest needs a little fix.. I'll ship it in soon.

    I'll definitely have to cover this in some form of documentation later. Unfortunately (?) I'm quite busy right now, so it may take some time. Contributions are always very welcome!

    ReplyDelete
  9. Ok, fix pushed to master.

    Finished in 10.619 seconds
    182 tests, 1173 assertions, 0 failures

    ReplyDelete
  10. OK, that makes sense - thanks for the explanation.

    > Ok, fix pushed to master.
    That was quick!

    > it now seems that not everybody in the world is Finnish
    I know - I myself was surprised to find out I wasn't Finnish :-)

    I've started knocking up a Typescript definition for bacon.js; could I send it over to you at some point to look through? The idea was to add it to this: https://github.com/borisyankov/DefinitelyTyped

    I'm also pretty busy currently (deadline soon - hence working on Sat evening), but it might also be a starting point for some documentation - I could at least create the JSDoc with the type info from the Typescript definition as a starting point.

    PS. Thanks for changing the blog settings!

    ReplyDelete
  11. LOL!

    Oh, the TypeScript definition would be cool. I'll have a look. Haven't done any TypeScript so far, but I'm a great fan of types, so it must be good:) The original Bacon (reactive-bacon) is a strongly typed (obviously) Haskell lib, you might want to have a look. (https://github.com/raimohanska/reactive-bacon). It's of course not identical to Bacon.js in design, but the basic concepts are the same, so that might help you with the typed version.

    Oh no, time to sleep. Happy hacking!

    ReplyDelete
  12. Yeah. Using Bacon.js with Node.js eventStreams and other Node.JS things.

    ReplyDelete
  13. Hey, the diagrams are broken. Could you make the links point at https://github.com/baconjs/bacon.js/wiki/Diagrams ? Thanks.

    ReplyDelete
  14. Thanks Juho! I fixed them diagrams.

    ReplyDelete
  15. Is it just me, or is there a bug in the code? It seems to me that usernameAvailable is the availability of the username when the request was sent, not the currently entered username. It would be possible for the user to enter an available username, wait for the 'register' button to become enabled, then alter the username to an unavailable one and quickly click the register button!
    I think it's necessary to check the equality of the requested username and the entered username before enabling the register button.

    ReplyDelete
  16. James, you're right! This is a simlified version of a working solution. My approach to fixing this would be to disable the button when the availability check is pending. We can introduce a new Property like this:

    checkPending = availabilityRequest.awaiting(availabilityResponse)

    And the take this into account in our enabling logic:

    buttonEnabled = buttonEnabled.and(checkPending.not())

    ReplyDelete