read

You’ve made it through the Todos tutorial and believe SproutCore is the Next Big Thing, so you start to dig deeper. The term statechart keeps popping up here and there, but it’s not entirely clear how to model and implement one for your app (see discussion).

Now there are some excellent writeups on using statecharts in SproutCore, and in fact I highly recommend reading them prior to this post. The only problem with these writeups is that they use the statechart framework that Erich Ocean wrote, whereas SproutCore’s responders are considered a better fit for all but the most complex apps. It’d also be well worth the time to bite the bullet and read David Harel’s original paper on statecharts, which is remarkably readable.

Signup

We’ll start by looking at a very simple example of managing state using responders, the sproutcore-samples app Signup (code here and demo here). It has just three states: START, READY and SIGNUP. Although I don’t know how the original author of the Signup sample would have drawn the statechart for this (or if he would have even bothered), here’s one way to draw it:

We’ll trace through the app and see how it goes from one state to another, and what happens in each state.

START state

The START state is transient, meaning that the app should exit from it to another state automatically based on an internal event. In order to get into the START state, the main() function makes it the first responder when the app loads:

// main.js
Signup.main = function main() {
  ...
  Signup.makeFirstResponder(Signup.START);
  ...
};

Once in the START state, the didBecomeFirstResponder method is automatically invoked. Inside this method, we see that it simply looks at the hasContent property of the current account controller, and goes to the READY state if it has content or the SIGNUP state if it doesn’t. So we see that the START state doesn’t really do anything, it just serves as a switch between the two other states:

// responders/start.js
didBecomeFirstResponder: function() {
  var state = Signup.currentAccountController.get('hasContent') ? Signup.READY : Signup.SIGNUP ;
  Signup.invokeLater(Signup.makeFirstResponder, 1, state)
}

SIGNUP state

Since the currentAccountController won’t have any content when the app loads, it will go into the SIGNUP state. The didBecomeFirstResponder method will be automatically invoked, and do two things: set up a new user to edit and display the signup dialog. One interesting tidbit here is that a nested store is set up by calling Signup.store.chain(). This allows the entire user record can be committed at once when the signup dialog is submitted (or discarded if instead the user cancels).

There are two ways the application can exit the SIGNUP state, either by the Submit button or Cancel button being hit in the signup dialog. Here are those buttons:

// english.lproj/signup_page.js
okButton: SC.ButtonView.design({
  layout: { bottom: 20, right: 20, width: 90, height: 24 },
  title: "_OK".loc(),
  isDefault: YES,
  action: "submit"
}),

cancelButton: SC.ButtonView.design({
  layout: { bottom: 20, right: 120, width: 90, height: 24 },
  title: "_Cancel".loc(),
  isCancel: YES,
  action: "cancel"
})

Note that these buttons don’t have to set a ‘target’ property, since the actions will automatically go to the first responder, which is currently Signup.SIGNUP. In order for this to work though, the MainPane has to have it’s default responder set to the app’s namespace:

// english.lproj/main_page.js
mainPane: SC.MainPane.design({
  ...
  defaultResponder: 'Signup',

If the user hits submit, then the nested store with the user record is committed and assigned to the currentAccountController, and if the user hits cancel then the user record is discarded. Either way, the last step of both the ‘submit’ and ‘cancel’ methods calls ‘Signup.makeFirstResponder(Signup.READY)’. One last thing does happen though as the state is changing. The willLoseResponder method is called in Signup.SIGNUP, which clears the nested store and signupController, and displays the mainPage (effectively closing the dialog).

READY state

The READY state displays Signup.mainPage, which will show the user associated with the accounts controller, or if there’s no user it will show a message indicating that. The READY state can only be exited by the ‘signup’ action, which just transitions the app back to the SIGNUP state:

// responders/ready.js
signup: function() {
  Signup.makeFirstResponder(Signup.SIGNUP);
},

The other possible action is ‘logout’, which clears the current account controller but doesn’t affect the state.

// responders/ready.js
logout: function() {
  Signup.currentAccountController.set('content', null);
}

What’s gained by using Responders?

Hopefully it’s now a bit more clear how responders can be used to manage state in this simple example, but an interesting question to ask at this point is what the app would look like without responders, and therefore what we’ve gained. Essentially, you’d have to weave all the state management in the responders into the other parts of your app, presumably the controllers. This would probably look fairly reasonable in the Signup example, but for more complex apps where you’d have multiple states that could be accessed from a single interface, you’d end up with lots of logic in your controllers to make them state-aware. SproutCore makes a good argument for making responders their own layer, beyond the standard MVC layers.

Blog Logo

Bruz Marzolf


Published

Image

Destructured

Back to Overview