FrozenCanuck / Ki

A Statechart Framework for SproutCore
http://frozencanuck.wordpress.com
Other
105 stars 7 forks source link

Go back to previous state? #9

Closed ghost closed 13 years ago

ghost commented 13 years ago

I have these states:

I always enter either SHOWING_PAGE_ONE or SHOWING_PAGE_TWO.

When I enter SHOWING_MENU_PANE and then close it I want to go back to previous state. How do I tell Ki which one of SHOWING_PAGE_ONE and SHOWING_PAGE_TWO was the previous state?

I could of course use currentStates each time I'm in a state and push it to an array to save the information.

But I wonder if there is another way of doing this with Ki? Has this to do with History states?

/Johnny

FrozenCanuck commented 13 years ago

Based on your three states, SHOWING_PAGE_ONE, SHOWING_PAGE_TWO, and SHOWING_MENU_PANE, it seems like a user can click a particular button independent of either showing page one or showing page two in order to show the menu pane. In addition, you probably want to continue to show the pages even when the menu pane is showing. Let's work with this idea and start with the initial concept of the what the statechart could be:

YourApp.statechart = Ki.Statechart.create({

  rootState: Ki.State.design({

    initialSubstate: 'showPageState',

    showPageState: Ki.State.design({

      showMenu: function() { 
        this.gotoState('showMenuState'); 
      },

      initialSubstate: 'showPageOneState',

      showPageOneState: Ki.State.design({ 
        enterState: function() { /* show page one */  },
        exitState: function() { /* hide page two */ },
        showPageTwo: function() {
          this.gotoState('showPageTwoState');
        }
      });

      showPageTwoState: Ki.State.design({
        enterState: function() { /* show page two */  },
        exitState: function() { /* hide page two */ },
        showPageOne: function() {
          this.gotoState('showPageOneState');
        }
      });

    });

    showMenuState: Ki.State.design({
      enterState: function() { /* show menu pane */ },
      exitState: function() { /* hide menu pane */ },

      returnToShownPage: function() { 
        this.gotoHistoryState('showPageState'); 
      }
    });

  })

});

Above we have four states. There is a parent showPageState that contains two substates: showPageOneState and showPageTwoState. You can only be in one of the pages at any time. The showPageSate also handles the event showMenu which will cause a state transition to the fourth state showMenuState. The showMenuState is responsible for showing and hiding a menu pane. In addition, the showMenuState handles the returnToShownPage event which will go to the showPageState's history state. Finally, each page state handles an event to transition between the two pages. Now let's try to go through the motions of using our currently implemented statechart.

When we first initialize our statechart, the current state is the showPageOneState state. As well, showPageState's history state is showPageOneState. Why? A state's history state is based on the last substate that was entered. This then means that the root state's history state is showPageState. Cool. So now let's say the user clicks a button visible on page one that causes the showPageTwo event to be fired. This means that the showPageOneState will handle the event and cause a state transition to the showPageTwoState. What does this mean with respect to history states? Well, because the showPageTwoState was entered this then means that the showPageState's history state is now showPageTwoState since that was the last substate to be entered. Let's now look at going to the showMenuState.

In the app, there is a button to show the menu and it is visible when you are either showing page one or page two. When the button is clicked a showMenu event is raised and the showPageState handles it that causes a state transition to the showMenuState. When the showMenuState is entered it will display a menu pane to the user. In order to get back to the page state that we were previously at, we call this.gotoHistoryState('showPageState'). This functionality will go to the showPageState history state. So if we were last in the showPageTwoState, then we would make a transition back to showPageTwoState. Pretty simple.

Hmm, hold on for one sec. There's something not right. In order to go to the showMenuState that means we had to leave the showPageState, right? When the showPageState is exited the currently showing page state must be exited first. Take a look at the exitState functions for both showPageOneState and showPageTwoState. What do you notice? They both hide the page they are responsible for. This means that when the menu is visible no page is visible. That's not what we want. But can't we just remove that exit state functionality? Well, no entirely. A state should be focused on a particular feature or task. Any time you leave a state, it should, in general, clean up after itself. Alright, then if we want to continue to show the page and also show the menu, what do we do? For that, we need concurrent substates.

Let's update our original statechart to be the following:

 YourApp.statechart = Ki.Statechart.create({

  rootState: Ki.State.design({

    substatesAreConcurrent: YES,

    showPageState: Ki.State.design({

      initialSubstate: 'showPageOneState',

      showPageOneState: Ki.State.design({ 
        enterState: function() { /* show page one */  },
        exitState: function() { /* hide page two */ },
        showPageTwo: function() {
          this.gotoState('showPageTwoState');
        }
      });

      showPageTwoState: Ki.State.design({
        enterState: function() { /* show page two */  },
        exitState: function() { /* hide page two */ },
        showPageOne: function() {
          this.gotoState('showPageOneState');
        }
      });

    });

    menuState: Ki.State.design({

      initialSubstate: 'hideMenuState',

      hideMenuState: Ki.State.design({ 
        enterState: function() { /* hide menu pane */  },
        showMenu: function() {
          this.gotoState('showMenuState');
        }
      });

      showMenuState: Ki.State.design({
        enterState: function() { /* show menu pane */ }
        hideMenu: function() {
          this.gotoState('hideMenuState');
        }
      });
    });

  })

});

Looking at the code above, a couple things have change. First, the showMenuState has become a substate of the new menuState. Second, the menuState and the showPageState are now concurrent to each other. Next, the menuState has a hideMenuState, and the hideMenuState now has the showMenu event handler, which the showPageState used to have. Finally, the showMenuState now handles the hideMenu event that makes a state transition to the hideMenuState. Okay, so what do all these changes mean? Let's find out.

Since the root state now has two substates that are concurrent to each other, this then means the statechart itself now will have two current states instead of just one like in our previous code. Therefore when an event is sent to the statechart both current states will receive the event. The concurrent states also further decouples the workflow when transitioning from one state to another state. Before, the showPageState had to handle the showMenu event in order to make a state transition to the showMenuState. Now the showPageState no longer has to be concerned with the menu states, and the menu states don't have to be concerned with the page states.

Again, we decoupled the workflow within our statechart. If you where to draw a diagram this would mean you would see less state transition lines. The main advantage we now have is that we can remain in either page state but still show and hide the menu without affecting the visibility of those pages. Hurray! So concurrent substates have come to our rescue.

What about history states then? Are they useless? Nope. Concurrent substates and history states have their purposes. Concurrent substates are usefull when you logically need to be in more then one state at a time, which is what we witnessed for this example. History states are useful when you actually leave a state but want to get back to it at a later time. An example of this would be a wristwatch. A wristwatch has many modes that you can flip between, but you can only be in one of those modes at any one time. In certain modes, you'd like to continue where you left off in that mode, and going to the history state of the mode makes it easy to do.