Views-HandlingEvents


 

Outline

 

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

 

 

 

Event Handling in Sproutcore

 

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.

 

 

 

Events

 

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.

 

Actions

 

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.

 

Responder chains

 

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.

 

 

The role of panes

 

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.

 

Handling mouse click events

 

Now, knowing all the mechanisms that are involved. Let's sum up, how many basic mouse events like click (mouseUp, mouseDown, doubleClick etc) work.

 

  1. You click your mouse
  2. RootResponder will capture DOM mouse click event
  3. RootResponder will find the view based on the information from DOM event
  4. it will ask the view's pane to deliver an event to view
    1. the pane will look at view and will check, that it implements the appropriate handler (notice, that this view will form the bottom of our current responder chain)
    2. if it does not, it will go to the view's next responder and will repeat it thus going up the responder chain all the way to the pane (including).
  5. if handler is not found in the whole responder chain, pane will try panes's default responder 
  6. if the default responder is an event context (which would usually mean another pane) it would ask it to resolve the event by itself

 

     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.

 

Handling actions

 

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.

 

Targeted actions

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

 

 

Untargeted actions

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.

 

  1. pane is send an action by calling a method sendAction on it
  2. pane will first try the firstResponder to take care of handling the action
  3. if not successful, it goes up the responder chain up to the pane
  4. now I would expect the pane will pick up the default responder, but due to (IMHO bug) current implementation it does not Resolve this with Charles

 

 

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

 

First Responders

 

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

 

  1. view si asked, if it wants to be a firstResponder, it is a great responsibility after all
  2. if yes, the current firstResponder is notified and asked to resign on the first responder status
  3. view is made a first responder and notified about that

 

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