Skip to content

Commit f5eb7e3

Browse files
committed
[issue jakesgordon#28] - Added generalized callbacks for intercepting all events and state transitions (instead of having a callback per-event/state)
* onbeforeevent * onleavestate * onenterstate * onafterevent E.g. * `onbeforeevent` is called for every event, while `onbeforeGO` is only called before the GO event. * `onleavestate` is called for every state, while `onleaveRED` is only called when leaving the RED state. * `onenterstate` is called for every state, while `onenterGREEN` is only called when entering the GREEN state. * `onafterevent` is called for every event, while `onafterGO` is only called after the GO event. NOTE: deprecated the legacy `onchangestate` callback (its the same as `onenterstate`)
1 parent 4f584f0 commit f5eb7e3

8 files changed

Lines changed: 320 additions & 71 deletions

File tree

README.md

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,25 +89,47 @@ the same name if you prefer the verbose approach.
8989
Callbacks
9090
=========
9191

92-
4 callbacks are available if your state machine has methods using the following naming conventions:
92+
4 types of callback are available using the following naming conventions:
9393

94-
* onbefore**event** - fired before the event
95-
* onleave**state** - fired when leaving the old state
96-
* onenter**state** - fired when entering the new state
97-
* onafter**event** - fired after the event
94+
* `onbeforeEVENT` - fired before the event
95+
* `onleaveSTATE` - fired when leaving the old state
96+
* `onenterSTATE` - fired when entering the new state
97+
* `onafterEVENT` - fired after the event
98+
99+
>> (using your specific EVENT and STATE names)
98100
99101
You can affect the event in 3 ways:
100102

101-
* return `false` from an `onbeforeevent` handler to cancel the event.
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)
103+
* return `false` from an `onbeforeEVENT` handler to cancel the event.
104+
* return `false` from an `onleaveSTATE` handler to cancel the event.
105+
* return `ASYNC` from an `onleaveSTATE` handler to perform an asynchronous state transition (see next section)
104106

105107
For convenience, the 2 most useful callbacks can be shortened:
106108

107-
* on**event** - convenience shorthand for onafter**event**
108-
* on**state** - convenience shorthand for onenter**state**
109+
* `onEVENT` - convenience shorthand for `onafterEVENT`
110+
* `onSTATE` - convenience shorthand for `onenterSTATE`
111+
112+
In addition, 4 general-purpose callbacks can be used to capture **all** event and state changes:
113+
114+
* `onbeforeevent` - fired before any event
115+
* `onleavestate` - fired when leaving any state
116+
* `onenterstate` - fired when entering any state
117+
* `onafterevent` - fired after any event
118+
119+
The order in which callbacks occur is as follows:
120+
121+
>> assume event **go** transitions from **red** state to **green**
122+
123+
* `onbeforego` - specific handler for the **go** event only
124+
* `onbeforeevent` - generic handler for all events
125+
* `onleavered` - specific handler for the **red** state only
126+
* `onleavestate` - generic handler for all states
127+
* `onentergreen` - specific handler for the **green** state only
128+
* `onenterstate` - generic handler for all states
129+
* `onaftergo` - specific handler for the **go** event only
130+
* `onafterevent` - generic handler for all events
109131

110-
In addition, a generic `onchangestate()` callback can be used to call a single function for _all_ state changes:
132+
>> NOTE: the legacy `onchangestate` handler has been deprecated and will be removed in a future version
111133
112134
All callbacks will be passed the same arguments:
113135

@@ -141,10 +163,10 @@ Callbacks can be specified when the state machine is first created:
141163

142164
Additionally, they can be added and removed from the state machine at any time:
143165

144-
fsm.ongreen = null;
145-
fsm.onyellow = null;
146-
fsm.onred = null;
147-
fsm.onchangestate = function(event, from, to) { document.body.className = to; };
166+
fsm.ongreen = null;
167+
fsm.onyellow = null;
168+
fsm.onred = null;
169+
fsm.onenterstate = function(event, from, to) { document.body.className = to; };
148170

149171
Asynchronous State Transitions
150172
==============================

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Version 2.2.0 (unreleased)
22
--------------------------
33

4+
* Added generic event callbacks 'onbeforeevent' and 'onafterevent' (issue #28)
5+
* Added generic state callbacks 'onleavestate' and 'onenterstate' (issue #28)
46
* Fixed 'undefined' event return codes (issue #34) - pull from gentooboontoo (thanks!)
57
* Allow async event transition to be cancelled (issue #22)
68

state-machine.js

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,43 @@
8585
}
8686
},
8787

88-
beforeEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args); },
89-
afterEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); },
90-
leaveState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args); },
91-
enterState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); },
92-
changeState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args); },
88+
beforeAnyEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbeforeevent'], name, from, to, args); },
89+
afterAnyEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafterevent'], name, from, to, args); },
90+
leaveAnyState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleavestate'], name, from, to, args); },
91+
enterAnyState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenterstate'], name, from, to, args); },
92+
changeState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args); },
93+
94+
beforeThisEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args); },
95+
afterThisEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); },
96+
leaveThisState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args); },
97+
enterThisState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); },
98+
99+
beforeEvent: function(fsm, name, from, to, args) {
100+
if ((false === StateMachine.beforeThisEvent(fsm, name, from, to, args)) ||
101+
(false === StateMachine.beforeAnyEvent( fsm, name, from, to, args)))
102+
return false;
103+
},
104+
105+
afterEvent: function(fsm, name, from, to, args) {
106+
StateMachine.afterThisEvent(fsm, name, from, to, args);
107+
StateMachine.afterAnyEvent( fsm, name, from, to, args);
108+
},
109+
110+
leaveState: function(fsm, name, from, to, args) {
111+
var specific = StateMachine.leaveThisState(fsm, name, from, to, args),
112+
general = StateMachine.leaveAnyState( fsm, name, from, to, args);
113+
if ((false === specific) || (false === general))
114+
return false;
115+
else if ((StateMachine.ASYNC === specific) || (StateMachine.ASYNC === general))
116+
return StateMachine.ASYNC;
117+
},
118+
119+
enterState: function(fsm, name, from, to, args) {
120+
StateMachine.enterThisState(fsm, name, from, to, args);
121+
StateMachine.enterAnyState( fsm, name, from, to, args);
122+
},
93123

124+
//===========================================================================
94125

95126
buildEvent: function(name, map) {
96127
return function() {

state-machine.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/test_advanced.js

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,15 @@ test("callbacks are called when appropriate for multiple 'from' and 'to' transit
121121
{ name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' },
122122
],
123123
callbacks: {
124-
onchangestate: function(event,from,to) { called.push('onchange from ' + from + ' to ' + to); },
125124

125+
// generic callbacks
126+
onbeforeevent: function(event,from,to) { called.push('onbefore(' + event + ')'); },
127+
onafterevent: function(event,from,to) { called.push('onafter(' + event + ')'); },
128+
onleavestate: function(event,from,to) { called.push('onleave(' + from + ')'); },
129+
onenterstate: function(event,from,to) { called.push('onenter(' + to + ')'); },
130+
onchangestate: function(event,from,to) { called.push('onchange(' + from + ',' + to + ')'); },
131+
132+
// specific state callbacks
126133
onenterhungry: function() { called.push('onenterhungry'); },
127134
onleavehungry: function() { called.push('onleavehungry'); },
128135
onentersatisfied: function() { called.push('onentersatisfied'); },
@@ -132,6 +139,7 @@ test("callbacks are called when appropriate for multiple 'from' and 'to' transit
132139
onentersick: function() { called.push('onentersick'); },
133140
onleavesick: function() { called.push('onleavesick'); },
134141

142+
// specific event callbacks
135143
onbeforeeat: function() { called.push('onbeforeeat'); },
136144
onaftereat: function() { called.push('onaftereat'); },
137145
onbeforerest: function() { called.push('onbeforerest'); },
@@ -141,19 +149,59 @@ test("callbacks are called when appropriate for multiple 'from' and 'to' transit
141149

142150
called = [];
143151
fsm.eat();
144-
deepEqual(called, ['onbeforeeat', 'onleavehungry', 'onentersatisfied', 'onchange from hungry to satisfied', 'onaftereat']);
152+
deepEqual(called, [
153+
'onbeforeeat',
154+
'onbefore(eat)',
155+
'onleavehungry',
156+
'onleave(hungry)',
157+
'onentersatisfied',
158+
'onenter(satisfied)',
159+
'onchange(hungry,satisfied)',
160+
'onaftereat',
161+
'onafter(eat)'
162+
]);
145163

146164
called = [];
147165
fsm.eat();
148-
deepEqual(called, ['onbeforeeat', 'onleavesatisfied', 'onenterfull', 'onchange from satisfied to full', 'onaftereat']);
166+
deepEqual(called, [
167+
'onbeforeeat',
168+
'onbefore(eat)',
169+
'onleavesatisfied',
170+
'onleave(satisfied)',
171+
'onenterfull',
172+
'onenter(full)',
173+
'onchange(satisfied,full)',
174+
'onaftereat',
175+
'onafter(eat)',
176+
]);
149177

150178
called = [];
151179
fsm.eat();
152-
deepEqual(called, ['onbeforeeat', 'onleavefull', 'onentersick', 'onchange from full to sick', 'onaftereat']);
180+
deepEqual(called, [
181+
'onbeforeeat',
182+
'onbefore(eat)',
183+
'onleavefull',
184+
'onleave(full)',
185+
'onentersick',
186+
'onenter(sick)',
187+
'onchange(full,sick)',
188+
'onaftereat',
189+
'onafter(eat)'
190+
]);
153191

154192
called = [];
155193
fsm.rest();
156-
deepEqual(called, ['onbeforerest', 'onleavesick', 'onenterhungry', 'onchange from sick to hungry', 'onafterrest']);
194+
deepEqual(called, [
195+
'onbeforerest',
196+
'onbefore(rest)',
197+
'onleavesick',
198+
'onleave(sick)',
199+
'onenterhungry',
200+
'onenter(hungry)',
201+
'onchange(sick,hungry)',
202+
'onafterrest',
203+
'onafter(rest)'
204+
]);
157205

158206
});
159207

@@ -168,15 +216,24 @@ test("callbacks are called when appropriate for prototype based state machine",
168216

169217
myFSM.prototype = {
170218

171-
onchangestate: function(event,from,to) { this.called.push('onchange from ' + from + ' to ' + to); },
219+
// generic callbacks
220+
onbeforeevent: function(event,from,to) { this.called.push('onbefore(' + event + ')'); },
221+
onafterevent: function(event,from,to) { this.called.push('onafter(' + event + ')'); },
222+
onleavestate: function(event,from,to) { this.called.push('onleave(' + from + ')'); },
223+
onenterstate: function(event,from,to) { this.called.push('onenter(' + to + ')'); },
224+
onchangestate: function(event,from,to) { this.called.push('onchange(' + from + ',' + to + ')'); },
172225

226+
// specific state callbacks
227+
onenternone: function() { this.called.push('onenternone'); },
228+
onleavenone: function() { this.called.push('onleavenone'); },
173229
onentergreen: function() { this.called.push('onentergreen'); },
174230
onleavegreen: function() { this.called.push('onleavegreen'); },
175231
onenteryellow : function() { this.called.push('onenteryellow'); },
176232
onleaveyellow: function() { this.called.push('onleaveyellow'); },
177233
onenterred: function() { this.called.push('onenterred'); },
178234
onleavered: function() { this.called.push('onleavered'); },
179235

236+
// specific event callbacks
180237
onbeforestartup: function() { this.called.push('onbeforestartup'); },
181238
onafterstartup: function() { this.called.push('onafterstartup'); },
182239
onbeforewarn: function() { this.called.push('onbeforewarn'); },
@@ -203,16 +260,19 @@ test("callbacks are called when appropriate for prototype based state machine",
203260
equal(a.current, 'green', 'start with correct state');
204261
equal(b.current, 'green', 'start with correct state');
205262

206-
deepEqual(a.called, ['onbeforestartup', 'onentergreen', 'onchange from none to green', 'onafterstartup']);
207-
deepEqual(b.called, ['onbeforestartup', 'onentergreen', 'onchange from none to green', 'onafterstartup']);
263+
deepEqual(a.called, ['onbeforestartup', 'onbefore(startup)', 'onleavenone', 'onleave(none)', 'onentergreen', 'onenter(green)', 'onchange(none,green)', 'onafterstartup', 'onafter(startup)']);
264+
deepEqual(b.called, ['onbeforestartup', 'onbefore(startup)', 'onleavenone', 'onleave(none)', 'onentergreen', 'onenter(green)', 'onchange(none,green)', 'onafterstartup', 'onafter(startup)']);
265+
266+
a.called = [];
267+
b.called = [];
208268

209269
a.warn();
210270

211271
equal(a.current, 'yellow', 'maintain independent current state');
212272
equal(b.current, 'green', 'maintain independent current state');
213273

214-
deepEqual(a.called, ['onbeforestartup', 'onentergreen', 'onchange from none to green', 'onafterstartup', 'onbeforewarn', 'onleavegreen', 'onenteryellow', 'onchange from green to yellow', 'onafterwarn']);
215-
deepEqual(b.called, ['onbeforestartup', 'onentergreen', 'onchange from none to green', 'onafterstartup']);
274+
deepEqual(a.called, ['onbeforewarn', 'onbefore(warn)', 'onleavegreen', 'onleave(green)', 'onenteryellow', 'onenter(yellow)', 'onchange(green,yellow)', 'onafterwarn', 'onafter(warn)']);
275+
deepEqual(b.called, []);
216276

217277
});
218278

0 commit comments

Comments
 (0)