View
 

Views-HandlingEvents

Page history last edited by Tomas Svarovsky 2 mos ago

 

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

 

  • action/event
  • RootResponder role 
  • click based mouse events
  • untargeted actions 
  • targeted actions 

 

 

TO BE DONE

 

  • mouse move events
  • dragging
  • key events
  • mainPane, menuPane, keyPane  

 

 

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.

 

  • The events are designed to work in more application centric way, rather than follow the DOM structure
  • Performance gains, since framework is abstracting you away from dealing with listeners yourself, you can rely on it, to be doing it efficiently 

 

 

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 wit touch events recently)

 

 

  • mouseDown
  • mouseUp
  • mouseDragged
  • mouseEntered 
  • mouseOut 
  • mouseMoved
  • doubleClick
  • click
  • mouseWheel
  • selectStart
  • keyDown
  • keyUp
  • touchStart 

Time for the first example. Create your application as usual. I called main Example. You should be able to find a LabelView inside your main_page.js. Views definition should look 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!"

})

 

Add a click method to it, so it 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 and click the label. You should see a message logged in your console. Nice.

 

You see, defining handler of the click event was just creating a method with appropriate name, Sproutcore took care of calling the method at appropriate time on your view, you don't need to care about listeners. This is the same with all events, the handling means, that you define method with an appropriate name on the view.

 

Actions

 

Besides events Sproutcore has also different concept, which is called action. Action are closest, what is called customEvents in many other frameworks. Most time in your application development, events are too low level. You are usually not interested in the fact, that some button was clicked, or some DIV dragged. Actions should be more high level and tuned to your problem domain. If you are creating a e-shop system, actions like checkout, addToCart, paymentDeclined is what you might be interested in more. Although, there are numerous similarities between actions and events, there are actually some important differences. Mainly in the way, they are handled by Sproutcore and we will look into these shortly. Rule of thumb from what I was able to understand so far is "Do not create your events. Use them to empower your actions.". Let's see the simplest action in action.

 

Let's use the previous Example application, 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"

})

 

add btn to childViews

 

childViews: 'labelView btn'.w(),

 

and add the hello method to your mainPane, which is defined in main_page.js

 

beAwesome: function() {SC.Logger.log("I am an awesome app");}

 

Run your application, click the button and again, you should see some message in console. The reason, why I before claimed the fact, that this is the most simple action example is because, you told the button which object (here target) to look for and which method (the action) to call. Such actions are called targeted. Yes, you guessed it right, you can create also untargeted actions, with which framework has much more work to do, to decide, who will handle them.

 

Responder chains

 

Let's back up a little and build better understanding, how things are wired together, so we can talk about the inner workings of routing events and actions. As you have seen without doubt, in Sproutcore you build trees of views. Our example app is no difference. Here, we have a label a button and parent of both is a main pane. Since instantiation of these views is in hands of Sproutcore, it has an opportunity, to do some more setup during this setup. One of those things is 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 null, 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.

 

  • Often, you can have many views interested in some event (thus they implement the method with appropriate name), but handling it only in some circumstances. If you will explicitly return NO from such method, the search in the chain will continue, everything else will mean, that you have handled it properly and further search will be stopped.
  • defaultResponder on pane objects is a very useful thing, since you can switch objects that will implement default behavior in different states of your app very easily.
  • Despite the fact, that Sproutcore will set up views nextResponder property to mimick your view tree, you are free to rearrange them as you wish, if you need to.
  • talking about responders. Responder is an instance of class SC.Responder. Actually SC.View inherit from SC.Responder, so technically all views are responders, so when I say a responder, any instance of SC.View will do just fine.

 

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

 

  • SC.Button is the probably the only one view in the base Sproutcore framework, that is able to have actions/target defined via view DSL language. If you want to achieve that, have a look how it is implemented in the code

 

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

 

  • acceptsFirstResponder comes from the SC.Responder, where it is set to YES, but is overridden in SC.View, where it is set to NO, so your views are by default unable to became first responders, with some notable exceptions like SC.TextFieldView
  • It is usually good idea, to set up some kind of first responder on the application start
  • during your day to day usage of actions, you will probably be just fine with sendAction on SC.Pane, but sometimes, you can be forced to use some finer grained behavior SC.Object tryToPerform, SC.EventContext sendAction and SC.RootResponder sendAction might be useful

 

 

Comments (0)

You don't have permission to comment on this page.