Skip to content

Commit 686b593

Browse files
committed
Allow event to be cancelled by returning false from onleavestate handler (issue jakesgordon#13)
- WARNING: this breaks backward compatibility for async transitions (you now need to return `StateMachine.ASYNC` instead of `false`)
1 parent cafa48b commit 686b593

5 files changed

Lines changed: 67 additions & 58 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,11 @@ Callbacks
9696
* onenter**state** - fired when entering the new state
9797
* onafter**event** - fired after the event
9898

99-
You can affect the event in 2 ways:
99+
You can affect the event in 3 ways:
100100

101101
* return `false` from an `onbeforeevent` handler to cancel the event.
102-
* return `false` from an `onleavestate` handler to perform an asynchronous state transition (see next section)
102+
* return `false` from an `onleavestate` handler to cancel the event.
103+
* return `ASYNC` from an `onleavestate` handler to perform an asynchronous state transition (see next section)
103104

104105
For convenience, the 2 most useful callbacks can be shortened:
105106

@@ -155,7 +156,7 @@ A good example of this is when you transition out of a `menu` state, perhaps you
155156
fade the menu away, or slide it off the screen and don't want to transition to your `game` state
156157
until after that animation has been performed.
157158

158-
**New in v2.0** you can now return `false` from your `onleavestate` handler and the state machine
159+
You can now return `StateMachine.ASYNC` from your `onleavestate` handler and the state machine
159160
will be _'put on hold'_ until you are ready to trigger the transition using the new `transition()`
160161
method.
161162

@@ -179,14 +180,14 @@ For example, using jQuery effects:
179180
$('#menu').fadeOut('fast', function() {
180181
fsm.transition();
181182
});
182-
return false; // tell StateMachine to defer next state until we call transition (in fadeOut callback above)
183+
return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in fadeOut callback above)
183184
},
184185

185186
onleavegame: function() {
186187
$('#game').slideDown('slow', function() {
187188
fsm.transition();
188189
};
189-
return false; // tell StateMachine to defer next state until we call transition (in slideDown callback above)
190+
return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in slideDown callback above)
190191
}
191192

192193
}

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Version 2.1.0 (January 7th 2012)
22
--------------------------------
33

4+
* Allow event to be cancelled by returning `false` from `onleavestate` handler (issue #13) - WARNING: this breaks backward compatibility for async transitions (you now need to return `StateMachine.ASYNC` instead of `false`)
45
* Added explicit return values for event methods (issue #12)
56
* Added support for wildcard events that can be fired 'from' any state (issue #11)
67
* Added support for no-op events that transition 'to' the same state (issue #5)

state-machine.js

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ StateMachine = {
2020
},
2121

2222
WILDCARD: '*',
23+
ASYNC: 'async',
2324

2425
//---------------------------------------------------------------------------
2526

@@ -105,29 +106,35 @@ StateMachine = {
105106
if (false === StateMachine.beforeEvent(this, name, from, to, args))
106107
return StateMachine.CANCELLED;
107108

108-
if (from !== to) {
109-
110-
var fsm = this;
111-
this.transition = function() { // prepare transition method for use either lower down, or by caller if they want an async transition (indicated by a false return value from leaveState)
112-
fsm.transition = null; // this method should only ever be called once
113-
fsm.current = to;
114-
StateMachine.enterState( fsm, name, from, to, args);
115-
StateMachine.changeState(fsm, name, from, to, args);
116-
StateMachine.afterEvent( fsm, name, from, to, args);
117-
return StateMachine.SUCCEEDED;
118-
};
119-
120-
if (false !== StateMachine.leaveState(this, name, from, to, args)) {
121-
if (this.transition) // in case user manually called it but forgot to return false
122-
return this.transition();
123-
}
124-
125-
return StateMachine.ASYNC; // transition method took care of (or, if async, will take care of) the afterEvent, DONT fall through
109+
if (from === to) {
110+
StateMachine.afterEvent(this, name, from, to, args);
111+
return StateMachine.NOTRANSITION;
126112
}
127113

128-
StateMachine.afterEvent(this, name, from, to, args); // this is only ever called if there was NO transition (e.g. if from === to)
114+
// prepare a transition method for use EITHER lower down, or by caller if they want an async transition (indicated by an ASYNC return value from leaveState)
115+
var fsm = this;
116+
this.transition = function() {
117+
fsm.transition = null; // this method should only ever be called once
118+
fsm.current = to;
119+
StateMachine.enterState( fsm, name, from, to, args);
120+
StateMachine.changeState(fsm, name, from, to, args);
121+
StateMachine.afterEvent( fsm, name, from, to, args);
122+
};
123+
124+
var leave = StateMachine.leaveState(this, name, from, to, args);
125+
if (false === leave) {
126+
this.transition = null;
127+
return StateMachine.CANCELLED;
128+
}
129+
else if ("async" === leave) {
130+
return StateMachine.ASYNC;
131+
}
132+
else {
133+
if (this.transition)
134+
this.transition(); // in case user manually called transition() but forgot to return ASYNC
135+
return StateMachine.SUCCEEDED;
136+
}
129137

130-
return StateMachine.SUCCEEDED;
131138
};
132139
}
133140

test/test_async.js

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ test("state transitions", function() {
1515
{ name: 'clear', from: 'yellow', to: 'green' }
1616
],
1717
callbacks: {
18-
onleavegreen: function() { return false; },
19-
onleaveyellow: function() { return false; },
20-
onleavered: function() { return false; }
18+
onleavegreen: function() { return StateMachine.ASYNC; },
19+
onleaveyellow: function() { return StateMachine.ASYNC; },
20+
onleavered: function() { return StateMachine.ASYNC; }
2121
}
2222
});
2323

@@ -48,9 +48,9 @@ test("state transitions with delays", function() {
4848
{ name: 'clear', from: 'yellow', to: 'green' }
4949
],
5050
callbacks: {
51-
onleavegreen: function() { return false; },
52-
onleaveyellow: function() { return false; },
53-
onleavered: function() { return false; }
51+
onleavegreen: function() { return StateMachine.ASYNC; },
52+
onleaveyellow: function() { return StateMachine.ASYNC; },
53+
onleavered: function() { return StateMachine.ASYNC; }
5454
}
5555
});
5656

@@ -88,9 +88,9 @@ test("state transition fired during onleavestate callback - immediate", function
8888
{ name: 'clear', from: 'yellow', to: 'green' }
8989
],
9090
callbacks: {
91-
onleavegreen: function() { this.transition(); return false; },
92-
onleaveyellow: function() { this.transition(); return false; },
93-
onleavered: function() { this.transition(); return false; }
91+
onleavegreen: function() { this.transition(); return StateMachine.ASYNC; },
92+
onleaveyellow: function() { this.transition(); return StateMachine.ASYNC; },
93+
onleavered: function() { this.transition(); return StateMachine.ASYNC; }
9494
}
9595
});
9696

@@ -115,7 +115,7 @@ test("state transition fired during onleavestate callback - with delay", functio
115115
{ name: 'panic', from: 'green', to: 'red' }
116116
],
117117
callbacks: {
118-
onleavegreen: function() { setTimeout(function() { fsm.transition(); }, 10); return false; },
118+
onleavegreen: function() { setTimeout(function() { fsm.transition(); }, 10); return StateMachine.ASYNC; },
119119
onenterred: function() {
120120
equals(fsm.current, 'red', "panic event should transition from green to red");
121121
start();
@@ -130,7 +130,7 @@ test("state transition fired during onleavestate callback - with delay", functio
130130

131131
//-----------------------------------------------------------------------------
132132

133-
test("state transition fired during onleavestate callback - but forgot to return false!", function() {
133+
test("state transition fired during onleavestate callback - but forgot to return ASYNC!", function() {
134134

135135
var fsm = StateMachine.create({
136136
initial: 'green',
@@ -141,9 +141,9 @@ test("state transition fired during onleavestate callback - but forgot to return
141141
{ name: 'clear', from: 'yellow', to: 'green' }
142142
],
143143
callbacks: {
144-
onleavegreen: function() { this.transition(); /* return false; */ },
145-
onleaveyellow: function() { this.transition(); /* return false; */ },
146-
onleavered: function() { this.transition(); /* return false; */ }
144+
onleavegreen: function() { this.transition(); /* return StateMachine.ASYNC; */ },
145+
onleaveyellow: function() { this.transition(); /* return StateMachine.ASYNC; */ },
146+
onleavered: function() { this.transition(); /* return StateMachine.ASYNC; */ }
147147
}
148148
});
149149

@@ -178,11 +178,11 @@ test("state transitions sometimes synchronous and sometimes asynchronous", funct
178178
fsm.calm(); equals(fsm.current, 'yellow', "calm event should transition from red to yellow");
179179
fsm.clear(); equals(fsm.current, 'green', "clear event should transition from yellow to green");
180180

181-
// but add callbacks that return false and it magically becomes asynchronous
181+
// but add callbacks that return ASYNC and it magically becomes asynchronous
182182

183-
fsm.onleavegreen = function() { return false; }
184-
fsm.onleaveyellow = function() { return false; }
185-
fsm.onleavered = function() { return false; }
183+
fsm.onleavegreen = function() { return StateMachine.ASYNC; }
184+
fsm.onleaveyellow = function() { return StateMachine.ASYNC; }
185+
fsm.onleavered = function() { return StateMachine.ASYNC; }
186186

187187
equals(fsm.current, 'green', "initial state should be green");
188188
fsm.warn(); equals(fsm.current, 'green', "should still be green because we haven't transitioned yet");
@@ -202,7 +202,7 @@ test("state transitions sometimes synchronous and sometimes asynchronous", funct
202202
fsm.transition(); equals(fsm.current, 'yellow', "warn event should transition from green to yellow");
203203
start(); // move on to next test
204204
}, 10);
205-
return false;
205+
return StateMachine.ASYNC;
206206
}
207207
}
208208
fsm.onleaveyellow = fsm.onleavered = null;
@@ -229,9 +229,9 @@ test("state transition fired without completing previous transition", function()
229229
{ name: 'clear', from: 'yellow', to: 'green' }
230230
],
231231
callbacks: {
232-
onleavegreen: function() { return false; },
233-
onleaveyellow: function() { return false; },
234-
onleavered: function() { return false; }
232+
onleavegreen: function() { return StateMachine.ASYNC; },
233+
onleaveyellow: function() { return StateMachine.ASYNC; },
234+
onleavered: function() { return StateMachine.ASYNC; }
235235
}
236236
});
237237

@@ -261,12 +261,12 @@ test("callbacks are ordered correctly", function() {
261261
callbacks: {
262262
onchangestate: function(event,from,to) { called.push('onchange from ' + from + ' to ' + to); },
263263

264-
onentergreen: function() { called.push('onentergreen'); },
265-
onenteryellow: function() { called.push('onenteryellow'); },
266-
onenterred: function() { called.push('onenterred'); },
267-
onleavegreen: function() { called.push('onleavegreen'); return false; },
268-
onleaveyellow: function() { called.push('onleaveyellow'); return false; },
269-
onleavered: function() { called.push('onleavered'); return false; },
264+
onentergreen: function() { called.push('onentergreen'); },
265+
onenteryellow: function() { called.push('onenteryellow'); },
266+
onenterred: function() { called.push('onenterred'); },
267+
onleavegreen: function() { called.push('onleavegreen'); return StateMachine.ASYNC; },
268+
onleaveyellow: function() { called.push('onleaveyellow'); return StateMachine.ASYNC; },
269+
onleavered: function() { called.push('onleavered'); return StateMachine.ASYNC; },
270270

271271
onbeforewarn: function() { called.push('onbeforewarn'); },
272272
onbeforepanic: function() { called.push('onbeforepanic'); },
@@ -310,9 +310,9 @@ test("cannot fire event during existing transition", function() {
310310
{ name: 'clear', from: 'yellow', to: 'green' }
311311
],
312312
callbacks: {
313-
onleavegreen: function() { return false; },
314-
onleaveyellow: function() { return false; },
315-
onleavered: function() { return false; }
313+
onleavegreen: function() { return StateMachine.ASYNC; },
314+
onleaveyellow: function() { return StateMachine.ASYNC; },
315+
onleavered: function() { return StateMachine.ASYNC; }
316316
}
317317
});
318318

test/test_basics.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,8 +460,8 @@ test("event return values (github issue #12) ", function() {
460460
{ name: 'start', from: 'ready', to: 'running' }
461461
],
462462
callbacks: {
463-
onbeforefake: function(event,from,to,a,b,c) { return false; }, // this event will be cancelled
464-
onleaveready: function(event,from,to,a,b,c) { return false; } // this state transition is ASYNC
463+
onbeforefake: function(event,from,to,a,b,c) { return false; }, // this event will be cancelled
464+
onleaveready: function(event,from,to,a,b,c) { return StateMachine.ASYNC; } // this state transition is ASYNC
465465
}
466466
});
467467

0 commit comments

Comments
 (0)