This article should you take through the event handling in Sproutcore.
Work in progress here. Also a note, that this work is based on code in master, not the gem.
DONE
TO BE DONE
When you started using sproutcore, you probably noticed, that a in lots of areas, Sproutcore has its own means, how to implement things. Event handling is no difference and that is for several reasons.
There is slightly different naming in the world of Sproutcore, so let's first look at that. In Sproutcore, there are events. These will be very similar to what you are used to, if you have done any client side web development. Events are emitted on basic user interactions like clicking the mouse buttons, moving it, touching your IPad screen, typing on keyboard etc. The difference here is mainly, how low level DOM events and listeners are managed. You do not listen on DOM elements for particular events. Instead, Sproutcore component called RootResponder (SC.RootResponder) will hook on the document for these events automatically, when a DOM event occurs, it captures that, does some heavy lifting and calls the appropriate method on view, which is interested in that event (more on that later). This principle is illustrated on the picture in a simplified manner.
The benefit here is the abstraction from the low level browser things and possible significant performance boost, since no matter how big your app is, the count of listeners on DOM elements is not increasing. The event, that the framework recognizes, but not all are (some might be added along time as long, as we have seen with touch events recently)
Time for the first example. Go ahead and create your application as usual. We'll use one called Example. You will find a LabelView inside your main_page.js file under resources. Your view file should look something like this:
labelView: SC.LabelView.design({
layout: { centerX: 0, centerY: 0, width: 200, height: 18 },
textAlign: SC.ALIGN_CENTER,
click: function() {alert("I was clicked");},
tagName: "h1",
value: "Welcome to SproutCore!"
})
Let's add a click method to it, so that the code now looks like this:
labelView: SC.LabelView.design({
layout: { centerX: 0, centerY: 0, width: 200, height: 18 },
textAlign: SC.ALIGN_CENTER,
click: function() {SC.Logger.log("I was clicked");},
tagName: "h1",
value: "Welcome to SproutCore!"
})
Start your application with sc-server, open it in a web browser and then click the label. You should see a message logged in your console. Nice!
Defining the handler of the click event was as simple as creating a method with the appropriate name. Sproutcore took care of calling the method at the appropriate time on your view. This means you don't need to care about listeners. This is the same with all events in Sproutcore. The handling means that you define method with an appropriate name on the view.
Besides events Sproutcore also has a different concept, which is called action. Action is similar to customEvents in many other frameworks. Most of the time in your application development, events are too low level. You are usually not interested in the details that some button was clicked or some DIV was dragged. Actions should be conceptually more high level and tuned to your problem domain. If you are creating an online shopping system then actions such as checkout, addToCart, paymentDeclined are appropriate to your problem domain. There are numerous similarities between actions and events, however there are some important differences. The important differences include how they are handled by Sproutcore. We will look at these shortly. Use the following mantra as a guideline "Do not create your events. Use them to empower your actions.". Let's see the simplest action...well...in action!
Let's use the previous Example application and add another view in the main_page.js
btn: SC.ButtonView.design({
layout: { centerX: 0, centerY: 100, width: 200, height: 40 },
textAlign: SC.ALIGN_CENTER,
title: "Click me",
target: "Example.mainPage.mainPane",
action: "beAwesome"
})
Let's add btn to the childViews
childViews: 'labelView btn'.w(),
...and now add the beAwesome method to your mainPane, which is defined in main_page.js file
beAwesome: function() {SC.Logger.log("I am an awesome app");}
Run your application and click on the button. You should see the above message in the browser's developer console. This is a simply illustration of how to inform the the button which object (here target) to look for and which method (the action) to call. Such actions are called targeted. You can also create untargeted actions. The framework has much more work to do with untargeted actions and decide who will handle them.
Let's back up a little and build deeper understanding about how things are wired together. This will allow you to better understand the inner workings of routing events and actions. In Sproutcore you build trees of views and our Example app is no different. In the Example app we have a label and a button. The parent of both of these views is a main pane. Because instantiation of these views is in the hands of Sproutcore there is an opportunity to do some adjustments, such as determining who the next responder of each view is. By default it is its parent (or super view), which we can prove very easily by calling this in our Example application in console.
Example.mainPage.mainPane.btn.get('nextResponder') === Example.mainPage.mainPane
Example.mainPage.mainPane.labelView.get('nextResponder') === Example.mainPage.mainPane
Interesting. You can add more views, to get the feel of it. From any view, down the tree, you should be able to go up to the mainPane. Another interesting thing is, that if you call this
Example.mainPage.mainPane.get('nextResponder') === null
you will get true, which means, that Pane (SC.Pane) is special in a way (and this is not only special thing about panes to stand out among other views), that it ends such a chain. In Sproutcore we call this chain of views beginning at some particular view (for example a btn here) and going through all the next responders up to its parent pane (mainPane here) a responder chain. As you can see, there are many potential responder chains, but only one can be in effect at any given time inside a pane. Let's have a look at an image, which illustrates responder chains.
As you can see, there are two possible responder chains depicted one is starting with buttonView, going over containerView and ending at Pane, the other is labelView and Pane. From the picture it might seem logical, that the responder chain will start at some leaf node at the tree, but this does not need to be the case. We will see in a minute, what determines both. Which of all possible chains is used and where such a chain starts.
Pane is almost like any other view, except it does not need to have a parent view. It can do much more thanks to a mixin called SC.ResponderContext.
formulate this better
I have told you about root responder. One of it's responsibilities is to listen on the DOM events. Second responsibility is to hold on to a list of panes in your application. Verify this in our Example application.
SC.RootResponder.responder.get('panes').length // => 1
SC.RootResponder.responder.get('panes')[0] === Example.mainPage.mainPane
SC.RootResponder.responder.get('mainPane') === Example.mainPage.mainPane
So when root responder captures a DOM event, it transforms it to an event, finds out the view, on which the event was caused and asks the pane in which hierarchy the view is, to take care of delivering an event.
Now, knowing all the mechanisms that are involved. Let's sum up, how many basic mouse events like click (mouseUp, mouseDown, doubleClick etc) work.
Depict the chain in graphics
You can see, that concept very similar to event bubbling in DOM is in effect here. Some remarks.
Again, time for short example.
TODO Paste the code of example
You should see all three methods get called, if you will remove return NO from any of them, chain search will stop there.
As I have already said before, actions are very similar to events in many ways, yet they are different beasts in many others. Let's start to look at differences.
One difference is their origin. If you remember something about actions from the beginning, it should be easy to see, that actions are not predefined and taken care about by RootResponder. You will have to define them yourself and since actions are usually tied closely to some events (addToCart action will be probably fired by clicking a button), they will be coming from your responders (or views), where events are handled.
Other difference is in the way actions are handled. Despite the fact, that the principle is very similar to events, they usually take different paths in chains.
You have already seen an example with a targeted action, that is an action, where you defined an action along with an target, that should handle the action. Since, this is pretty easy, we can tackle this special case right away. In targeted actions only the target is tried to handle the action.
You can easily see this, when adding beAwesome to your defaultResponder on mainPane. Even, that it is defined there, it does not get called, even if you will comment out the handler on mainPane. Sproutcore assumes, that you know, what you are doing in such cases.
One remark
The second more challenging case is the case of untargeted action. To be able to grasp the path, which the action must go to get handled, we miss one more concept in our toolbox and that is a firstResponder. FirstResponder is defined on pane similar as a defaultResponder, but if you think about a defaultResponder as a last resort, firstResponder is something like the first choice of a pane.
The handling of an untargeted action goes like this.
Not that tough after all, was it? Time for example. Let's assume, our labelView wants to be awesome when clicked as well.
TODO -> add code
It probably works as expected, but if you try to put the handler of the action directly on the label, one could expect, that it could be handled right there on the label. Not the case here, what happenned? It is something to do with the firstResponder of the pane. If you fire up your console one more time and type this into it
Example.mainPage.mainPane.get('firstResponder')
returns null, hmm that might be the problem. So, maybe if we set up label to be first responder, it wold get a chance to handle the action immediately as the first Responder of the chain. Now, you are probably tempted to do this
# DO NOT DO THIS Example.mainPage.mainPane.set('firstResponder', Example.mainPage.mainPane.labelView)
but there is certain protocol, that is wise to keep, when setting a view as a first responder of a pane and method becomeFirstResponder on SC.View is here to perform this task for you. The protocol is as follows
TODO => check, that this protocol is valid
So run
Example.mainPage.mainPane.labelView.becomeFirstResponder()
Example.mainPage.mainPane.get('firstResponder')
null again. Somethign is still wrong. If you will look at the point 1 of the list, we have just seen, there is stated, that view is asked if it wishes to be the first responder. This is the problem here since our labelView does not want to. Easy to fix, go to main_page.js and set
acceptsFirstResponder: YES
repeat the upper two commands, and you should be set with new responder. Now, label itself should be able to handle the action.
Again, couple of remarks on the topic