Getting Started: Part 2
In Part 1, we walked through the basic elements of a SproutCore app. In Part 2 we introduce some of the guiding concepts behind a SproutCore application such as views, controllers, and statecharts.
- Learn about statecharts and how they can improve your application.
- Learn about views and how they should handle events.
- Learn how to separate views in individual files with the use of sc_require().
- Learn how to pass events to the application's statechart.
1 - Why Statecharts?
If you've ever built any sizable application, or even small ones for that matter, you've probably run into a bug that was caused by some piece of logic that didn't work quite as you intended.
Perhaps it was an if statement with an invalid test expression, or some piece of code that changed a variable that you weren't expecting to be updated. Some bugs happen because you don't understand how your app would react under a particular set of conditions.
Statecharts help reduce uncertainty, by defining more explicitly what can and cannot happen within your application. You think of discrete states, such as "showing login panel." You think of transitions between states. For example, you would think about what should happen as the login panel is shown, what could happen while it is showing, and what should happen when it is taken down.
2 - What is a Statechart?
A statechart is a simple nested construct that helps you better organize your code into logical hierarchical states. Your applications will have many states, and each state may have many substates within it. Each state can respond to a certain set of actions, the name for events associated with a statechart.
The term "event" is more generic than the term "action." We can speak of the event system, generally, that includes normal web events like **mouseDown** along with actions in the statechart. A statechart action is a type of event.
Consider a SproutCore application for mapping, with the following top-level states, substates, and actions:
- LOADING
- loadingFinished
- LOADED
- viewTermsOfService
- viewPrivacyPolicy
- LOGGED_OUT
- login
- LOGGED_IN
- createBasemap
- zoomMapView
- showPointData
- other mapping actions...
In the list above, the states are in ALL-CAPS, the actions are in italics.
The Getting Started Todos applications use ALL_CAPS for state names, for the sake of continuity. However, states are standard SC objects and can follow any naming convention you want. Many developers prefer CamelCase (i.e., MyApp.LoggedInState) while others prefer namespacing their states (i.e., MyApp.States.LoggedIn). Some prefer verb-style over noun-style state names. Whatever method you choose is fine, just be consistent, so you, and other developers, can more easily follow your code.
The term state is generic: you can use state to refer to states and substates. When you use the term substate, the reference is more specific, and is relative to a parent state.
2.1 - Decoding the Statechart
In our mapping example here, the application can be in one of the two top level states: LOADING or LOADED. If it is in one of these states, it cannot be in the other. When it is in the LOADING state, the only available action is the loadingFinished action. The statechart/application should not respond to any other events. Once control moves into the LOADED state, it can move to two additional states, substates of the LOADED state: LOGGED_OUT or LOGGED_IN. Again, the program can only be in one at a given time, so when the user is "logged out", after the application has loaded, the only available action is the login action.
However, notice that the viewTermsOfService action and the viewPrivacyPolicy action are both available in the LOADED state. This means that regardless of whether the program is in the LOGGED_OUT state or the LOGGED_IN state, the user should be allowed to view the terms of service or the privacy policy.
Once the user has authenticated and logged in, they have various actions available in the mapping system. Imagine a map user interface that appears, with buttons in a dashboard, with live clickable elements on the map, etc. The example actions given above, createBasemap, zoomMapView, and showPointData are just a sample of what would be many actions tied to user interface elements.
States are used in all parts of an application, not just to user interface parts. A large behind-the-scenes data import system in our mapping example could be programmed using statecharts.
2.2 - How Statecharts Help
An application only responds to certain actions at certain times. This makes it much easier to verify that an app is working properly, by ensuring that events are handled exactly when they need to be handled. Furthermore, if, say, a bug in the design of the user interface allows an action to happen when it shouldn't (like allowing some action when the user is logged out), the application itself may maintain a consistent state and continue to operate. Programming with statecharts can help isolate parts of the system.
SproutCore generates statechart-based applications by default. The basis of the statechart is formed by the statechart.js file in the application root directory, and in the states directory readystate.js_:
- statechart.js, and
- states/readystate.js_
The statechart.js file describes your statechart and the initial state that should be entered when the statechart is first initialized. Open it, and you should see the following:
apps/TodosOne/statechart.jsTodosOne.statechart = SC.Statechart.create({ initialState: 'readyState', readyState: SC.State.plugin('TodosOne.ReadyState'), // someOtherState: SC.State.plugin('TodosOne.SomeOtherState') });
This example uses CamelCase state names.
Note the use of SC.Statechart.create(), instead of SC.Statechart.extend(). Views and models generally use .extend() when you are defining them, because elsewhere in the system, instantiation of objects happens. Controllers and the statechart use .create() because we need to directly create objects to work with.
Also note the use of the SC.State.plugin() function. This allows you to put your states in separate files in a states directory.
Open states/readystate.js_ and take a look:
apps/TodosOne/states/ready_state.jsTodosOne.ReadyState = SC.State.extend({ enterState: function() { TodosOne.getPath('mainPage.mainPane').append(); }, exitState: function() { TodosOne.getPath('mainPage.mainPane').remove(); } });
States may define an enterState() function to prepare for the new state, and an exitState() function to clean up after themselves. For user interface programming, these functions are where you show and hide panels, as you see here, with the use of .append() to show and .remove() to hide.
The TodosOne.ReadyState is the only state in this "Hello World" app, so it only appends the main page's main pane.
3 - Where Views Fit In
We've covered statecharts, so you know a little bit about how they work. Let's talk about how statecharts can help organize views and make your code super-clean.
The resources/mainpage.js_ file defines the main user interface of your application.
Edit the file to add a second child view, labelView2, taking care to add labelView2 to the childViews array of view references, which uses the 'space delimited strings'.w() SproutCore idiom to make an array:
apps/TodosOne/resources/main_page.jsTodosOne.mainPage = SC.Page.design({ mainPane: SC.MainPane.design({ childViews: 'labelView labelView2'.w(), labelView: SC.LabelView.design({ layout: { centerX: 0, centerY: 0, width: 200, height: 18 }, textAlign: SC.ALIGN_CENTER, tagName: "h1", value: "Welcome to SproutCore!" }), labelView2: SC.LabelView.design({ layout: { centerX: 0, centerY: 15, width: 200, height: 18 }, textAlign: SC.ALIGN_CENTER, tagName: "h1", value: "Now we're rolling!" }) }) });
Throughout the Getting Started guides we have minimized in-code comments for brevity.
Note the use of the .design() method here. .design() is often confused with .extend(). This confusion is caused by a SproutCore "wart." .design() exists so that views will work with a graphical designer tool for SproutCore (like Interface Builder for Mac OS X) that has been in development for a long time, but remains unfinished, with development long ago stalled. A designer tool might be created in the future, but regardless, there is a logic you can follow, wherein you use .design() when you are using a view, and .extend() where you are defining it. Here we are using .design(), but rest assured that SC.LabelView uses extend where it is defined in the SproutCore codebase.
When you make your own views, remember to use _.extend()_ when you are making the view in the first place, and _.design()_ elsewhere, where you use it.
If you load up http://localhost:4020/TodosOne/ in your browser, you should see both labels now; the second one right below the first.
3.1 - Separating Views
Before we get into using views with statecharts, let's first arrange view files for TodosOne. Technically, you can put all view code in main_page.js, but that will get impractical very fast. Instead you can separate each view into it's own file. This is accomplished by using the special screquire()_ functionality. Modify the mainPane of the mainPage view to have two sc_require() statements at the top, to declare two views as dependencies, and to change the childViews of the pane to use these views:
apps/TodosOne/resources/main_page.jssc_require('views/welcome'); sc_require('views/rolling'); TodosOne.mainPage = SC.Page.design({ mainPane: SC.MainPane.design({ childViews: 'welcomeView rollingView'.w(), welcomeView: TodosOne.WelcomeView.design(), rollingView: TodosOne.RollingView.design() }) });
screquire()_ functionality:
- The file path used in screquire()_ is relative to the application's root directory.
- The reference in screquire()_ has the filename without the .js extension.
- The load order for javascript files will be alphabetical, unless otherwise controlled with sc_require(). Sometimes you get lucky on the default file loading order, if you omit a sc_require() statement. Sometimes you don't, and you will get a dependency error.
Now create the view files for our new WelcomeView and RollingView. Make sure the files have the following content and are located in the views directory:
apps/TodosOne/views/welcome.jsTodosOne.WelcomeView = SC.LabelView.extend({ layout: { centerX: 0, centerY: 0, width: 200, height: 18 }, textAlign: SC.ALIGN_CENTER, tagName: "h1", value: "Welcome to SproutCore!" });
apps/TodosOne/views/rolling.jsTodosOne.RollingView = SC.LabelView.extend({ layout: { centerX: 0, centerY: 15, width: 200, height: 18 }, textAlign: SC.ALIGN_CENTER, tagName: "h1", value: "Now we're rolling!" });
If you refresh the page, everything should look exactly as it did before, but now your code is cleaner and more manageable.
3.2 - Handling Events and Statechart Actions
Standard events for a web application include the ubiquitous mouseDown, and many others. Here we add an event to watch for mouseDown in our TodosOne.RollingView, associating it with a statechart action:
apps/TodosOne/views/rolling.jsTodosOne.RollingView = SC.LabelView.design({ layout: { centerX: 0, centerY: 15, width: 200, height: 18 }, textAlign: SC.ALIGN_CENTER, tagName: "h1", value: "Now we're rolling!", mouseDown: function(evt) { var someParam = "Woot!"; TodosOne.statechart.sendAction('proveIt', someParam); } });
We set a proveIt action to happen on the mouseDown event, using the statechart.sendAction() function. We must handle the action in the appropriate state in the statechart.
The statechart has a property for the current state. When the statechart recieves an action event, it will first ask the current state to handle it. If the current state cannot handle it, the statechart will ask the current state's parent state to handle the event. Attempts continue in this manner, bubbling up until either an ancestor state is found that can handle the event, or it will log a warning for the developer. You can see warnings, as well as other informational messages (such as for state transitions), by turning on tracing on the statechart.
Do that now:
apps/TodosOne/statechart.jsTodosOne.statechart = SC.Statechart.create({ trace: YES, // <-- Add this line initialState: 'readyState', readyState: SC.State.plugin('TodosOne.ReadyState') });
Let's make our TodosOne.ReadyState handle the proveIt action that was defined on the mouseDown event in TodosOne.RollingView above:
apps/TodosOne/states/ready_state.jsTodosOne.ReadyState = SC.State.extend({ enterState: function() { TodosOne.getPath('mainPage.mainPane').append(); }, exitState: function() { TodosOne.getPath('mainPage.mainPane').remove(); }, // Prove that we are rolling! proveIt: function(someParam) { alert(someParam); } });
proveIt() is a function. It is an action in the ReadyState state.
Save your files and refresh the page. When you click on the "Now we're rolling!" label, you should see an alert. If you look at the console output, you can also see the tracing reports, with the event being handled by the statechart, as well as the initialization and the transition to the readyState.
4 - Moving Forward
You are now ready to move on to creating a full-scale SproutCore app!
Part 3 will introduce you to a more complete Todos application, called TodosThree, and walk you through the process of building it from scratch. You will learn about models, controllers and how they tie in with the statechart and views.
Dive in to Getting Started: Part 3.
5 - Changelog
- May 5, 2011: initial version by Piotr Sarnacki
- May 9, 2011: Stylistic edits by Tom Dale
- March 6, 2012: rewrite for SproutCore 1.8 by the 1.8 release sprint team, including the following who did much work on this task: Tim Evans, Topher Fangio, Jeff Pittman
- July 30, 2013: converted to Markdown format for DocPad guides by Eric Theise
- July 30, 2013: fix issues with formatting and updated Changelog by Topher Fangio
- Jan 17, 2019: Updated the guide as the default application already uses the state chart by Maurits Lamers