Skip to content

Commit 887ad3f

Browse files
committed
allow hooks to be specified during initial construction (instead of tacking them onto the object afterwards) and renamed 'hook' to 'callback' throughout
1 parent 4d5b75d commit 887ad3f

9 files changed

Lines changed: 292 additions & 278 deletions

File tree

README.md

Lines changed: 79 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,6 @@ want to start fading during an `onleavestate` handler, but not trigger the next
66

77
Something like
88

9-
var fsm = StateMachine.create({
10-
initial: 'green',
11-
events: [
12-
{ name: 'play', from: 'menu', to: 'game', async: true },
13-
{ name: 'lose', from: 'game', to: 'menu' },
14-
]});
15-
16-
fsm.onleavemenu = function() {
17-
$('menu').fade(function() {
18-
fsm.transition();
19-
});
20-
}
21-
22-
fsm.onentergame = function() {
23-
// this doesn't get called until fsm.transition() is called when the menu has finished fading
24-
}
25-
26-
fsm.play();
27-
28-
Also possible to simply return false from onleavestate hook instead of having to declare event as async:
29-
309
var fsm = StateMachine.create({
3110
initial: 'green',
3211
events: [
@@ -49,7 +28,7 @@ Also possible to simply return false from onleavestate hook instead of having to
4928

5029
Or.... something else ! Have to wait and see how it pans out (without breaking existing synchronous behavior)
5130

52-
Javascript Finite State Machine (v1.3.0)
31+
Javascript Finite State Machine (v2.0.0)
5332
========================================
5433

5534
This standalone javascript micro-framework provides a finite state machine for your pleasure.
@@ -105,24 +84,73 @@ along with the following members:
10584
* fsm.can(e) - return true if event `e` can be fired in the current state
10685
* fsm.cannot(e) - return true if event `e` cannot be fired in the current state
10786

108-
Hooks
109-
=====
87+
Multiple 'from' and 'to' states for a single event
88+
==================================================
89+
90+
If an event is allowed **from** multiple states, and always transitions **to** the same
91+
state, then simply provide an array of states in the `from` attribute of an event.
92+
93+
If an event is allowed **from** multiple states, but should transition **to** a different
94+
state depending on the current state, then provide multiple event entries with
95+
the same name:
96+
97+
var fsm = StateMachine.create({
98+
initial: 'hungry',
99+
events: [
100+
{ name: 'eat', from: 'hungry', to: 'satisfied' },
101+
{ name: 'eat', from: 'satisfied', to: 'full' },
102+
{ name: 'eat', from: 'full', to: 'sick' },
103+
{ name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' },
104+
]});
105+
106+
This example will create an object with 2 event methods:
107+
108+
* fsm.eat()
109+
* fsm.rest()
110110

111-
>> _NOTE: I'm using the word 'hook' to avoid overloading the word 'event'._
111+
The `rest` event will always transition to the `hungry` state, while the `eat` event
112+
will transition to a state that is dependent on the current state.
112113

113-
4 hooks are available if your object has methods using the following naming conventions:
114+
>> NOTE: The `rest` event in the above example can also be specified as multiple events with
115+
the same name if you prefer the verbose approach:
116+
117+
var fsm = StateMachine.create({
118+
initial: 'hungry',
119+
events: [
120+
{ name: 'eat', from: 'hungry', to: 'satisfied' },
121+
{ name: 'eat', from: 'satisfied', to: 'full' },
122+
{ name: 'eat', from: 'full', to: 'sick' },
123+
{ name: 'rest', from: 'hungry', to: 'hungry' }, // NOTE: this is a no-op.
124+
{ name: 'rest', from: 'satisfied', to: 'hungry' },
125+
{ name: 'rest', from: 'full', to: 'hungry' },
126+
{ name: 'rest', from: 'sick', to: 'hungry' },
127+
]});
128+
129+
Callbacks
130+
=========
131+
132+
4 callbacks are available if your state machine has methods using the following naming conventions:
114133

115134
* onbefore**event** - fired before an event
116135
* onafter**event** - fired after an event
117136
* onenter**state** - fired when entering a state
118137
* onleave**state** - fired when leaving a state
119138

120-
For convenience, the 2 most useful hooks can be shortened:
139+
For convenience, the 2 most useful callbacks can be shortened:
121140

122141
* on**event** - convenience shorthand for onafter**event**
123142
* on**state** - convenience shorthand for onenter**state**
124143

125-
Hooks can be added after the FSM is created:
144+
In addition, a generic `onchangestate()` callback can be used to call a single function for _all_ state changes:
145+
146+
All callbacks will be passed the same arguments:
147+
148+
* event name
149+
* from state
150+
* to state
151+
* _(followed by any arguments you passed into the original event method)_
152+
153+
Callbacks can be specified when the state machine is first created:
126154

127155
var fsm = StateMachine.create({
128156
initial: 'green',
@@ -131,77 +159,48 @@ Hooks can be added after the FSM is created:
131159
{ name: 'panic', from: 'yellow', to: 'red' },
132160
{ name: 'calm', from: 'red', to: 'yellow' },
133161
{ name: 'clear', from: 'yellow', to: 'green' }
134-
]});
135-
136-
fsm.onpanic = function() { alert('panic!'); };
137-
fsm.onclear = function() { alert('all clear!'); };
138-
fsm.ongreen = function() { document.body.className = 'green'; };
139-
fsm.onyellow = function() { document.body.className = 'yellow'; };
140-
fsm.onred = function() { document.body.className = 'red'; };
162+
],
163+
callbacks: {
164+
onpanic: function(event, from, to) { alert('panic!'); },
165+
onclear: function(event, from, to) { alert('all clear!'); },
166+
ongreen: function(event, from, to) { document.body.className = 'green'; },
167+
onyellow: function(event, from, to) { document.body.className = 'yellow'; },
168+
onred: function(event, from, to) { document.body.className = 'red'; },
169+
}
170+
});
141171

142172
fsm.panic()
143173
fsm.clear()
144174
...
145175

146-
*NEW in v1.3.0* is a generic `onchangestate(from,to)` hook was added to allow a single function
147-
to be called on all state changes:
148-
149-
fsm.onchangestate = function(from, to) { document.body.className = to; };
150-
151-
152-
Multiple 'from' states for a single event
153-
=========================================
176+
Additionally, they can be added and removed from the state machine at any time:
154177

155-
If an event is allowed **from** multiple states, and always transitions **to** the same
156-
state, then simply provide an array of states in the `from` attribute of an event:
178+
fsm.ongreen = null;
179+
fsm.onyellow = null;
180+
fsm.onred = null;
181+
fsm.onchangestate = function(event, from, to) { document.body.className = to; };
157182

158-
var fsm = StateMachine.create({
159-
initial: 'green',
160-
events: [
161-
{ name: 'warn', from: ['green'], to: 'yellow' },
162-
{ name: 'panic', from: ['green', 'yellow'], to: 'red' },
163-
{ name: 'calm', from: ['red'], to: 'yellow' },
164-
{ name: 'clear', from: ['red', 'yellow'], to: 'green' }
165-
]});
166-
167-
Multiple 'to' states for a single event
168-
=======================================
169-
170-
If an event is allowed **from** multiple states, but should transition **to** a different
171-
state depending on the current state, then provide multiple event entries with
172-
the same name:
173-
174-
var fsm = StateMachine.create({
175-
initial: 'hungry',
176-
events: [
177-
{ name: 'eat', from: 'hungry', to: 'satisfied' },
178-
{ name: 'eat', from: 'satisfied', to: 'full' },
179-
{ name: 'eat', from: 'full', to: 'sick' },
180-
{ name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' },
181-
]});
182-
183-
This example will create an object with 2 event methods:
184-
185-
* fsm.eat()
186-
* fsm.rest()
183+
Asynchronous State Transitions
184+
==============================
187185

188-
The `rest` event will always transition to the `hungry` state, while the `eat` event will transition to a state that is dependent on the current state.
186+
* **TODO**
189187

190188
State Machine Classes
191189
=====================
192190

193191
You can also turn all instances of a _class_ into an FSM by applying
194-
the state machine functionality to the prototype object and providing
195-
a `startup` event for use when constructing instances:
192+
the state machine functionality to the prototype, including your callbacks
193+
in your prototype, and providing a `startup` event for use when constructing
194+
instances:
196195

197196
MyFSM = function() { // my constructor function
198197
this.startup();
199198
};
200199

201200
MyFSM.prototype = {
202201

203-
onpanic: function() { alert('panic'); },
204-
onclear: function() { alert('all is clear'); },
202+
onpanic: function(event, from, to) { alert('panic'); },
203+
onclear: function(event, from, to) { alert('all is clear'); },
205204

206205
// other prototype methods
207206

@@ -250,7 +249,7 @@ implicit `startup` event will be created for you and fired when the state machin
250249
]});
251250
alert(fsm.current); // "green"
252251

253-
If your object already has a `startup` method you can change the name of the initial event
252+
If your object already has a `startup` method you can use a different name for the initial event
254253

255254
var fsm = StateMachine.create({
256255
initial: { state: 'green', event: 'init' },

RELEASE_NOTES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11

22
Version 2.0.0 (unreleased)
33
--------------------------
4+
5+
* consistent arguments for ALL callbacks, first 3 args are ALWAYS event name, from state and to state, followed by whatever arguments the user passed to the original event method.
46
* adding support for `async` event transitions (see README)
5-
* added a generic `onchangestate(from,to)` hook to detect all state changes with a single callback.
7+
* added a generic `onchangestate(event,from,to)` callback to detect all state changes with a single function
68

79
Version 1.2.0 (June 21st 2011)
810
------------------------------

demo/demo.js

Lines changed: 60 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,78 @@
1-
Demo = {
1+
Demo = function() {
22

3-
run: function() {
4-
StateMachine.create({
5-
target: this,
6-
initial: 'green',
7-
events: [
8-
{ name: 'warn', from: ['green'], to: 'yellow' },
9-
{ name: 'panic', from: ['green', 'yellow'], to: 'red' },
10-
{ name: 'calm', from: ['red'], to: 'yellow' },
11-
{ name: 'clear', from: ['red', 'yellow'], to: 'green' },
12-
]});
13-
},
3+
var output = document.getElementById('output'),
4+
demo = document.getElementById('demo'),
5+
panic = document.getElementById('panic'),
6+
warn = document.getElementById('warn'),
7+
calm = document.getElementById('calm'),
8+
clear = document.getElementById('clear'),
9+
count = 0;
1410

15-
onbeforestartup: function(event, from, to) { this.log("STARTING UP"); },
16-
onafterstartup: function(event, from, to) { this.log("READY"); },
11+
var log = function(msg, separate) {
12+
count = count + (separate ? 1 : 0);
13+
output.value = count + ": " + msg + "\n" + (separate ? "\n" : "") + output.value;
14+
demo.className = fsm.current;
15+
panic.disabled = fsm.cannot('panic');
16+
warn.disabled = fsm.cannot('warn');
17+
calm.disabled = fsm.cannot('calm');
18+
clear.disabled = fsm.cannot('clear');
19+
};
1720

18-
onbeforewarn: function(event, from, to) { this.log("START EVENT: warn!", true); },
19-
onbeforepanic: function(event, from, to) { this.log("START EVENT: panic!", true); },
20-
onbeforecalm: function(event, from, to) { this.log("START EVENT: calm!", true); },
21-
onbeforeclear: function(event, from, to) { this.log("START EVENT: clear!", true); },
21+
var fsm = StateMachine.create({
2222

23-
onwarn: function(event, from, to) { this.log("FINISH EVENT: warn!"); },
24-
onpanic: function(event, from, to) { this.log("FINISH EVENT: panic!"); },
25-
oncalm: function(event, from, to) { this.log("FINISH EVENT: calm!"); },
26-
onclear: function(event, from, to) { this.log("FINISH EVENT: clear!"); },
23+
events: [
24+
{ name: 'start', from: 'none', to: 'green' },
25+
{ name: 'warn', from: 'green', to: 'yellow' },
26+
{ name: 'panic', from: 'green', to: 'red' },
27+
{ name: 'panic', from: 'yellow', to: 'red' },
28+
{ name: 'calm', from: 'red', to: 'yellow' },
29+
{ name: 'clear', from: 'red', to: 'green' },
30+
{ name: 'clear', from: 'yellow', to: 'green' },
31+
],
2732

28-
onleavegreen: function(event, from, to) { this.log("LEAVE STATE: green"); },
29-
onleaveyellow: function(event, from, to) { this.log("LEAVE STATE: yellow"); },
30-
onleavered: function(event, from, to) { this.log("LEAVE STATE: red"); this.asyncTransition(); return false; },
33+
callbacks: {
34+
onbeforestart: function(event, from, to) { log("STARTING UP"); },
35+
onstart: function(event, from, to) { log("READY"); },
3136

32-
ongreen: function(event, from, to) { this.log("ENTER STATE: green"); },
33-
onyellow: function(event, from, to) { this.log("ENTER STATE: yellow"); },
34-
onred: function(event, from, to) { this.log("ENTER STATE: red"); },
37+
onbeforewarn: function(event, from, to) { log("START EVENT: warn!", true); },
38+
onbeforepanic: function(event, from, to) { log("START EVENT: panic!", true); },
39+
onbeforecalm: function(event, from, to) { log("START EVENT: calm!", true); },
40+
onbeforeclear: function(event, from, to) { log("START EVENT: clear!", true); },
3541

36-
onchangestate: function(event, from, to) { this.log("CHANGED STATE: " + from + " to " + to); },
42+
onwarn: function(event, from, to) { log("FINISH EVENT: warn!"); },
43+
onpanic: function(event, from, to) { log("FINISH EVENT: panic!"); },
44+
oncalm: function(event, from, to) { log("FINISH EVENT: calm!"); },
45+
onclear: function(event, from, to) { log("FINISH EVENT: clear!"); },
3746

38-
asyncTransition: function() {
39-
var self = this;
40-
self.logTransition(3);
47+
onleavegreen: function(event, from, to) { log("LEAVE STATE: green"); },
48+
onleaveyellow: function(event, from, to) { log("LEAVE STATE: yellow"); },
49+
onleavered: function(event, from, to) { log("LEAVE STATE: red"); async(to); return false; },
50+
51+
ongreen: function(event, from, to) { log("ENTER STATE: green"); },
52+
onyellow: function(event, from, to) { log("ENTER STATE: yellow"); },
53+
onred: function(event, from, to) { log("ENTER STATE: red"); },
54+
55+
onchangestate: function(event, from, to) { log("CHANGED STATE: " + from + " to " + to); }
56+
}
57+
});
58+
59+
var async = function(to) {
60+
pending(to, 3);
4161
setTimeout(function() {
42-
self.logTransition(2);
62+
pending(to, 2);
4363
setTimeout(function() {
44-
self.logTransition(1);
64+
pending(to, 1);
4565
setTimeout(function() {
46-
self.logTransition(0);
47-
self.transition();
66+
fsm.transition(); // trigger deferred state transition
4867
}, 1000);
4968
}, 1000);
5069
}, 1000);
51-
},
52-
53-
logTransition: function(n) {
54-
if (n)
55-
this.log("PENDING STATE: " + this.transition.to + " in ..." + n);
56-
},
70+
};
5771

58-
log: function(msg, separate) {
59-
this.count = (this.count || 0) + (separate ? 1 : 0);
72+
var pending = function(to, n) { log("PENDING STATE: " + to + " in ..." + n); };
6073

61-
var output = document.getElementById('output');
62-
output.value = this.count + ": " + msg + "\n" + (separate ? "\n" : "") + output.value;
74+
fsm.start();
75+
return fsm;
6376

64-
document.getElementById('demo').className = this.current;
65-
document.getElementById('panic').disabled = this.cannot('panic');
66-
document.getElementById('warn').disabled = this.cannot('warn');
67-
document.getElementById('calm').disabled = this.cannot('calm');
68-
document.getElementById('clear').disabled = this.cannot('clear');
69-
}
77+
}();
7078

71-
};

index.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ <h1> Finite State Machine </h1>
3434

3535
<script src="state-machine.js"></script>
3636
<script src="demo/demo.js"></script>
37-
<script>
38-
Demo.run();
39-
</script>
4037

4138
</body>
4239
</html>

0 commit comments

Comments
 (0)