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
- Create an
EventStream
of AJAX requests for use withjQuery.ajax()
- Create a stream of responses by issuing an AJAX request for each request event and capturing AJAX responses into the new stream.
- Define the
usernameAvailable
Property based on the results - 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
- Create a response stream for each request in the request stream
- 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- It calls your function for each value in the source stream
- It expects your function to return a new EventStream
- It collects the values of all created streams into the result stream
Like in this diagram.
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- Request A is sent
- Request B is sent
- Result of request B comes
- 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.
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 likeusernameAvailable.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?
How to implement the zip function ?
ReplyDeleteOh, 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 :)
DeleteThis comment has been removed by the author.
ReplyDeleteFor the mysterious gentleman who added a comment and removed it:
ReplyDelete1) 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.
Something on lifecycle?
ReplyDeleteCovering 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?)
I guess I partly answered your questions already. What do you think?
ReplyDeleteAnyway, 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!
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.
ReplyDeleteThanks 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!
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 :)
ReplyDeleteI don't have a full-solution based on flatMapLatest for you, but I'll try to describe the general idea.
ReplyDeleteSo, 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!
Ok, fix pushed to master.
ReplyDeleteFinished in 10.619 seconds
182 tests, 1173 assertions, 0 failures
OK, that makes sense - thanks for the explanation.
ReplyDelete> 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!
LOL!
ReplyDeleteOh, 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!
Yeah. Using Bacon.js with Node.js eventStreams and other Node.JS things.
ReplyDeleteHey, the diagrams are broken. Could you make the links point at https://github.com/baconjs/bacon.js/wiki/Diagrams ? Thanks.
ReplyDeleteThanks Juho! I fixed them diagrams.
ReplyDeleteIs 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!
ReplyDeleteI think it's necessary to check the equality of the requested username and the entered username before enabling the register button.
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:
ReplyDeletecheckPending = availabilityRequest.awaiting(availabilityResponse)
And the take this into account in our enabling logic:
buttonEnabled = buttonEnabled.and(checkPending.not())
So the only difference between a stream and a property is that a property has an initial value?
ReplyDeleteAlso, any anti-patterns for making side-effects?
So the only difference between a stream and a property is that a property has an initial value?
ReplyDeleteAlso, any anti-patterns for making side-effects?
Your blog has given me that thing which I never expect to get from all over the websites. Nice post guys!
ReplyDeletemelbourne web developer
I love visiting the technical blogs designed only for certain professions. Only they understand what is going on. Rise credit card account login
ReplyDeletethanks for sharing such a wonderful information from this post
ReplyDeletedigital marketing
I enjoyed your blog Thanks for sharing such an informative post. We are also providing the best services click on below links to visit our website.
ReplyDeletedigital marketing company in nagercoil
digital marketing services in nagercoil
digital marketing agency in nagercoil
best marketing services in nagercoil
SEO company in nagercoil
SEO services in nagercoil
social media marketing in nagercoil
social media company in nagercoil
PPC services in nagercoil
digital marketing company in velachery
digital marketing company in velachery
digital marketing services in velachery
digital marketing agency in velachery
SEO company in velachery
SEO services in velachery
social media marketing in velachery
social media company in velachery
PPC services in velachery
online advertisement services in velachery
online advertisement services in nagercoil
web design company in nagercoil
web development company in nagercoil
website design company in nagercoil
website development company in nagercoil
web designing company in nagercoil
website designing company in nagercoil
best web design company in nagercoil
web design company in velachery
web development company in velachery
website design company in velachery
website development company in velachery
web designing company in velachery
website designing company in velachery
best web design company in velachery
Thanks for Sharing - ( Groarz branding solutions )
This blog has given me that thing which I never expect to get from all over the websites.
ReplyDeleteDigital Marketing Training in Chennai | Certification | SEO Training Course | Digital Marketing Training in Bangalore | Certification | SEO Training Course | Digital Marketing Training in Hyderabad | Certification | SEO Training Course | Digital Marketing Training in Coimbatore | Certification | SEO Training Course | Digital Marketing Online Training | Certification | SEO Online Training Course
Now let’s take a closer look how to differentiate sin(3x+4y(x)) with respect to x: best roti maker
ReplyDeletesad shayari will validate your skills and
ReplyDelete