Introduction
For a quick introduction to Stapes, follow the
tutorial, and write a todo app in less than 100 lines of code.
Write your Stapes modules like this
var Module = Stapes.subclass({
constructor : function(name) {
this.name = name;
},
sayName : function() {
console.log('My name is: ' + this.name);
}
});
Then, use it like this.
var module = new Module('Emmylou');
module.sayName(); // 'My name is Emmylou'
Examples
There are three examples available to get a taste on how to write a
Stapes.js application.
Code for these examples is available in the development download.
Note that the two todo examples are also available from
TodoMVC.
Creation methods
These methods aid in the creation and extension of classes, or
modules. These terms are used interchangeably in this
document.
subclass
Module.subclass( [object] )
Stapes.subclass( [object, classOnly] )
Create a new Stapes class that you can instantiate later on with
new
.
Note that until Stapes 0.6.0 the preferred way of creating new modules was by using Stapes.create
. This has been replaced by subclass
since 0.7.0. Read this blogpost why.
You can add a constructor
(the function that is run when you
instantiate the class) property to the object. All other properties
will become prototype methods. To add static methods to your class use
extend
. If you want to add
more properties to the prototype of the class later on, use
proto
.
var Module = Stapes.subclass({
constructor : function(name) {
this.name = name;
},
getName : function() {
return this.name;
}
});
var module = new Module('emmylou');
module.getName(); // 'emmylou'
When calling subclass
on a Stapes class you can extend
it into a new class that will inherit all prototype properties of the
parent class.
var Module = Stapes.subclass({
sayName : function() {
console.log('i say my name!');
}
});
var BetterModule = Module.subclass({
sayNameBetter : function() {
this.sayName(); // Inherits 'sayName' from 'Module'
console.log('i say my name better!');
}
});
var module = new BetterModule();
module.sayNameBetter(); // 'i say my name! i say my name better!'
Note that it's perfectly valid to call subclass
without
any arguments at all. In that case you'll simply get a new class
with an empty constructor.
Also note that if you call subclass
on a Module the child
module won't automatically inherit the constructor
of the parent. This is by design. To inherit the constructor of a parent
class simply specify it in your subclass
setup:
var Parent = Stapes.subclass({
constructor : function(name) {
this.name = name;
}
});
var Child = Parent.subclass({
// inherit the constructor from parent
constructor : Parent.prototype.constructor
});
Because subclass
set ups the prototype chain correctly
the instanceof
parameter will work as expected.
module instanceof BetterModule; // true
module instanceof Module; // true
All modules automatically get a parent
property that links
back to the prototype of the parent. You can use this to override a
method in a new class, but still call the method of the parent class, like
the super
method that is available in lots of languages.
var BetterModule = Module.subclass({
// Note that this method name is the same as the one in Module
sayName : function() {
BetterModule.parent.sayName.apply(this, arguments);
console.log('i say my name better');
}
});
var module = new BetterModule();
module.sayName(); // 'i say my name! i say my name better!'
The optional classOnly
flag can be set to true to make
a class without any event or
data methods. You will get all of the
creation methods, so you can still subclass
and extend your class. Use this flag to use Stapes as a simple
class creation library.
var Class = Stapes.subclass({
constructor : function(name) {
this.name = name;
}
}, true /* <-- 'classOnly' flag */);
'subclass' in Class; // true
'get' in new Class(); // false
extend
module.extend( object[, object...])
Stapes.extend( object )
Extend your class instance properties by giving an
object. Keys with the same value will be overwritten. this
will be set to the objects context.
var Module = Stapes.subclass();
var module = new Module();
module.extend({
"names" : ['claire', 'alice'],
"sayName" : function(i) {
console.log( "Hello " + this.names[i] + "!" );
}
});
module.sayName(1); // 'alice'
Note that using extend
is exactly the same as directly
assigning properties to the module:
module.names = ['claire', 'alice'];
// Is the same as
module.extend({
names : ['claire', 'alice'];
});
Both extend
and proto
accept
multiple objects as arguments:
var module = new (Stapes.subclass());
var instruments = { guitar : true };
var voices = { singing : true };
module.extend(instruments, voices);
console.log('guitar' in module, 'singing' in module); // true, true
extend
is also useful for stuff like configuration
properties.
var Module = new Stapes.subclass({
constructor : function(props) {
this.extend( props );
},
sayInfo : function() {
console.log(this.name + ' plays ' + this.instrument);
}
});
var module = new Module({
'name' : 'Johnny',
'instrument' : 'guitar'
});
module.sayInfo();
Stapes.extend()
can be used for writing extra methods
and properties that will be available on all Stapes modules,
even after you have created them. This is very useful for
writing plugins for functionality that isn't in Stapes.
Stapes.extend({
// Sets an DOM element for views
"setElement" : function(el) {
this.el = el;
this.$el = $(el); // jQuery or Zepto reference
}
});
var Module = Stapes.subclass();
var module = new Module();
module.setElement( document.getElementById("app") );
console.log( module.el ); // "#app" DOM element
console.log( module.$el ); // jQuery or Zepto element
All internal methods are available as the
Stapes._
object, so if you want you can overwrite
and hack virtually all Stapes behaviour. Note that these
methods aren't documented, so you should take a look at the
source.
proto
Module.proto( object )
Adds properties and methods to the prototype of the module.
Note that it's usally easier to directly add methods to the prototype
using the subclass
method.
However,if you want to add methods to the prototype later on,
proto
might be useful.
var Module = Stapes.subclass();
Module.proto({
'sayName' : function() {
console.log('I say my name');
}
});
'sayName' in Module; // false, not a static method
'sayName' in Module.prototype; // true
// Note that using proto is the same as:
Module.prototype.sayName = function() {
console.log('I say my name');
};
Both proto
and extend
accept
multiple objects as arguments. This can be very handy for mixins.
var Module = Stapes.subclass();
var instruments = { guitar : true };
var voices = { singing : true };
Module.proto(instruments, voices);
var module = new Module();
console.log(module.guitar, module.singing); // true, true
Event methods
on
module.on(eventName, handler [, context] )
module.on(object [, context] )
Stapes.on(object [, context] )
Stapes.on(eventName, handler [, context] )
Add an event listener to a Stapes module or object
with mixed-in events. When an event is
triggered the handler
will be called
with two arguments: data
, which may contain any data, and an
eventObject
that contains some useful information about the
event, such as the scope, target and event name.
As an optional third (when defining a single listener) or second
(when defining multipe listeners using an object) parameter you can set what the value of
this
should be in the scope of the event handler.
This is handy to prevent having to temporarily save the scope
to a self
or that
variable or use the
new bind
property of EcmaScript 5.
var Module = Stapes.subclass();
var module = new Module();
module.on('ready', function() {
console.log('your module is ready!');
});
module.on({
"talk" : function() {
console.log('your module is talking!');
},
"walk" : function() {
console.log('your module is walking!');
},
"eat" : function( food ) {
console.log('your module is eating ' + (this.get('food') || 'nothing'));
}
});
var feedme = new Module();
// Set the scope for the event handler to 'module'
feedme.on({
"feed" : function( food ) {
this.set('food', food);
}
}, module);
module.emit('eat');
// "your module is eating nothing"
feedme.emit('feed', 'cookies');
module.emit('eat');
// "your module is eating cookies"
module.get('food');
// "cookies"
feedme.get('food');
// null
The on
method on the global Stapes
object can be used to listen to events from all defined modules.
Listening to the special all
event on the Stapes
global will trigger on all events thrown in all
objects. Very useful for debugging, but be careful not to leave it in
your production code :)
var Module = Stapes.subclass({
constructor : function(name) {
this.name = name;
},
beReady : function() {
this.emit('ready');
}
});
var module1 = new Module('module1');
var module2 = new Module('module2');
Stapes.on('ready', function(data, e) {
console.log('a ready event was triggered in: ' + e.scope.name);
});
module1.beReady(); // 'a ready event was triggered in module 1';
module2.beReady(); // 'a ready event was triggered in module 2';
The all
event listener is also available on every module, so
you can listen to all events from a specific module.
var Module = Stapes.subclass({
"go" : function() {
this.emit('foo');
this.emit('bar');
}
});
var module = new Module();
module.on("all", function(data, e) {
console.log(e.type);
});
module.go(); // first 'foo', then 'bar'
off
module.off(eventType, handler)
module.off(eventType)
module.off()
Removes event handlers from an object. Giving both an eventType
and a handler will remove that specific handler from a module.
Giving only an eventType will remove all handlers bound to
that event type.
With no arguments at all, off()
will remove all event handlers from a module.
var Module = Stapes.subclass();
var module = new Module();
var handler = function(){};
module.on({
"foo" : handler,
"bar" : function(){}
});
module.off("foo", handler); // Removes only the specific handler for foo
module.off("bar"); // Removes all 'bar' handlers
module.off(); // Removes all handlers
emit
module.emit(eventName[, data])
Trigger an event on the module. eventName
can be a space seperated
string if you want to trigger more events. data
can be any
Javascript variable you want, and will be passed to any event listeners
var Module = Stapes.subclass({
"sleep" : function() {
this.emit('sleeping', 'very deep');
}
});
var module = new Module();
module.on('sleeping', function(how) {
console.log("i'm sleeping " + how);
});
module.sleep(); // "i'm sleeping very deep"
mixinEvents
Stapes.mixinEvents([object])
It's possible to add Stapes' event handling
methods to any Javascript object or function. This can be very
handy if you want to create an object that only uses event handlers, or
for an object or function that already exists and you don't want
to convert to a Stapes module.
Here's how to add event methods to an object:
var module = {};
Stapes.mixinEvents(module);
module.on('sing', function() {
console.log("i'm singing!");
});
module.sing = function() {
this.emit('sing');
}
module.sing(); // i'm singing!
Stapes.mixinEvents
returns the object
, so
you could write the first two lines from the previous example even shorter:
var module = Stapes.mixinEvents( {} );
No, wait! It can be even shorter! Without any arguments Stapes.mixinEvents
returns a new object with mixed-in events.
var module = Stapes.mixinEvents();
You can also add event methods to a function:
function Module(what) {
this.what = what;
Stapes.mixinEvents(this);
}
Module.prototype.sing = function() {
this.emit('sing', this.what);
}
var m = new Module("Happy Birthday");
m.on('sing', function(what) {
console.log("i'm singing " + what + "!");
});
m.sing(); // i'm singing Happy Birthday!
Note that these events are also triggered on the main Stapes
object, so you can use Stapes.on
to catch events
from these mixed-in objects as well.
Stapes.on('sing', function(data, e) {
console.log("Singing from " + e.scope.name);
});
var module = Stapes.mixinEvents( {} );
module.name = "a cool module!";
module.sing = function() {
this.emit('sing');
}
module.sing(); // 'Singing from a cool module!'
Data methods
each
module.each( function, [context] );
Iterate over all attributes of a module.
When giving a context
parameter this will be set as
the this
value in the iterator function. If context
is not set it will be set to the module itself.
var Module = Stapes.subclass();
var singers = ['Johnny', 'Emmylou', 'Gram', 'June'];
var module = new Module();
module.msg = "I'll be your ";
module.push(singers);
module.each(function(singer) {
console.log(this.msg + singer);
});
// Using the second context parameter of each()
module.singers = new (Stapes.subclass());
module.singers.push( singers );
module.singers.each(function(singer) {
console.log(this.msg + singer);
}, module);
filter
module.filter( function );
Gets an array of attribute values using a custom function.
The callback function
gets two arguments: the value of the attribute, and the original key.
module.push([
{
name : 'Johnny',
playing : false
},
{
name : 'Emmylou',
playing : true
}
]);
var playing = module.filter(function(singer) {
return singer.playing;
}); // [ { name : 'Emmylou', playing : true }]
get
module.get( key );
module.get( key1, key2, ... );
module.get( function );
Gets an attribute by key. If the item is not available will return
null
var Module = Stapes.subclass();
var module = new Module();
module.set({
'instrument': 'guitar',
'name': 'Johnny'
});
module.get('instrument'); // 'guitar'
You can get multiple key / value pairs when you
use more than one argument:
module.get('instrument', 'name'); // { 'instrument' : 'guitar', 'name' : 'Johnny' }
You can also use a function to get a specific value. This is
comparable to filter
,
however, filter
always returns an array of results,
while get
always returns a single result.
getAll
module.getAll();
Returns all the attributes of a module as an object. Handy for JSON
serializing and persistence layers.
Note that this method returns a copy/clone instead of a reference.
getAllAsArray
module.getAllAsArray();
Returns all attributes as an array, so you can easily iterate.
Note that the original key of the attribute is always available as a
'id' key in the the value, provided your value is an object.
Note that this method returns a copy/clone instead of a reference.
module.set({
"name" : "Johnny",
"instrument" : "guitar"
});
module.getAllAsArray().length; // '2'
has
module.has( key );
Checks if a key is available and returns true or false.
module.set('singer', 'Johnny');
module.has('singer'); // true
module.has('instrument'); // false
map
module.map( function, [context] )
Just like each
, map
iterates over
all attributes of a module. map
returns a new array, where every attribute
value of a module has been passed through function
.
module.push([1, 2, 3]);
var arr = module.map(function(nr) {
return nr * 2;
});
console.log(arr); // '[2, 4, 6]'
By default map
gets the module as the value of this
, but this
can be overwritten with the second argument.
push
module.push( value, [silent] );
module.push( array, [silent] );
Sets a value, automatically generates an unique uuid as a key.
You can also push an array of values.
Just as with set
the optional silent
flag will
prevent any change events from being emitted.
For the rest of the behaviour of this method see
set
.
m.push("foo");
m.getAll(); // will look something like { "5323be61-afb8-4034-b408-51132756cd43" : "foo"}
m.push([
"foo",
"bar",
"baz"
]);
push
returns the module, so this method is chainable.
remove
module.remove( key, [silent] );
module.remove( function );
module.remove();
Deletes an attribute. Triggers remove
and
change
events.
remove
also triggers namespaced change
and
remove
events (e.g. change:foo
and
remove:foo
).
You can either use a key
as an argument or a
function
module.remove(function(item) {
return item.done === true;
});
It's possible to remove multiple keys in one go, simply space seperate them:
module.remove('singer instrument johnny');
If you do not want your remove
to trigger an event set
the optional silent
flag to true.
module.remove('singer', true /* <-- silent flag */);
Without any arguments, remove
deletes all attributes in a module and triggers change
and remove
events.
module.push([1,2,3]);
module.size(); // 3
module.remove();
module.size(); // 0
remove
returns the module, so this method is chainable.
set
module.set(key, value, [silent]);
module.set(object, [silent]);
Sets an attribute. Use push
if you want to 'push' a value
with a random uuid, for collections.
To set multiple attributes in one go, use an object as an first argument.
Every attribute will trigger a change
event. A key
that doesn't exist will trigger a create
event, a key
that does exist will trigger an update
event.
All events will have the key of the attribute as their event value.
Special namespaced events will also be triggered. These events have a value
of eventType:key
. So for example, a set
on
an attribute called 'name' will generate a change:name
event. These events will have the attribute value instead of the
key as a data argument in the event callback.
module.on({
"change" : function(key) {
console.log('Something happened with ' + key);
},
"change:name" : function(value) {
console.log('name was changed to ' + value);
},
"create" : function(key) {
console.log("New attribute " + key + " added!");
},
"update" : function(key) {
console.log("Attribute " + key + " was updated!");
}
});
module.set('name', 'Elvis'); // will trigger 'change' and 'create' events
module.set('name', 'Johnny'); // will trigger 'change' and 'update' events
module.set({
"name" : "Elvis",
"instrument" : "guitar"
});
If you do not want your set
to trigger any events set
the optional silent
flag to true
module.set('singer', 'Johnny', true /* <-- silent flag */);
This also works for objects:
module.set({
'singer' : 'Johnny',
'instrument' : 'guitar'
}, true /* silent */);
To get the old or previous value of an attribute
use the mutate
event instead of change
.
There are both namespaced and general versions of this event, just like
change
. Instead of the value of the attribute it
will return an object with oldValue
and newValue
properties. For convenience it also returns the key
of the attribute.
module.set('name', 'Johnny');
module.on({
"mutate:name" : function(values) {
console.log(values.oldValue, values.newValue); // "Johnny, Emmylou"
},
"mutate" : function(values) {
// Returns 'Emmylou'
// Note that this is identical to writing:
// module.on(
// 'change',
// function(key) {
// console.log( module.get(key) );
// }
// );
console.log(values.newValue);
}
});
module.set('name', 'Emmylou');
Note that it's still perfectly fine to assign properties to the
Stapes module directly, as long as you don't overwrite existing
properties. All the data methods are useful if you want to do
model-like stuff, but for ordinary properties that don't need
change events setting attributes using get
and
set
is fine.
module.on('change:name', function() {
// Obviously, this event hander will never trigger
});
module.name = "Elvis";
console.log(module.name); // "Elvis"
set
returns the module, so this method is chainable
Stapes doesn't have a native way of doing validation, but you could overwrite the set
(and possibly update
) method to accomplish the same:
var Person = Stapes.subclass({
set : function(key, value) {
if (key === 'email') {
if (value.indexOf("@") === -1) {
this.emit('error', 'Invalid email adress');
} else {
Stapes.prototype.set.apply(this, arguments);
}
} else {
// Normal set() behavior
Stapes.prototype.set.apply(this, arguments);
}
}
});
var johnny = new Person();
johnny.on('error', function(msg) {
alert( msg ); // 'Invalid email adress'
});
johnny.set('email', 'invalid#email.com');
size
module.size();
Returns the number of attributes in a module.
module.push(['Johnny', 'Emmylou', 'June', 'Gram']);
console.log( module.size() ); // '4'
update
module.update( key, fn, [silent] );
module.update( fn );
Updates an attribute with a new value, based on the return value
of a function.
Just as with set
update
will generate change
and update
events.
module.set('singer', 'Elvis');
module.update('singer', function(singer) {
return 'Johnny';
});
console.log( module.get('singer') ); // 'Johnny';
You can also pass a single function as an argument. In that case,
update
will run on all attributes in the module.
module.push([
{ "name" : "Johnny", "singer" : false},
{ "name" : "Emmylou", "singer" : false}
]);
module.update(function(item) {
item.singer = true;
return item;
});
The callback function in update
gets two arguments: the
value being modified, and the original key. The this
value of the callback will be set to the module you're updating.
The silent
flag will prevent any change events from firing.
update
returns the module, so this method is chainable.
Miscellaneous
version
Stapes.version
Returns the current version of Stapes. Note that this property
is only available on the Stapes global variable, not on
individual modules.
Cookbook
Here are some notes on using Stapes in various different situations.
Module loading
Stapes can be used together with an AMD module loader like Require.js, CommonJS loaders like the one in Node or simply as an old-fashioned global variable.
Here's the example from the introduction using Require.js
// 'module.js'
require(["path/to/Stapes"], function(Stapes) {
var name;
function showPrivate() {
console.log("You shouldn't be seeing this: ", name);
}
var Module = Stapes.subclass({
constructor : function(aName) {
name = aName;
showPrivate();
}
});
return Module;
});
// somewhere else
require(['module'], function(Module) {
var module = new Module('Elvis'); // You shouldn't be seeing this: Elvis
});
Private variables
If you want to use private variables and functions, use Stapes like this:
var Module = (function() {
var name;
function showPrivate() {
console.log("You shouldn't be seeing this", name);
}
var Module = Stapes.subclass({
constructor : function(aName) {
name = aName;
showPrivate();
}
});
return Module;
})();
var module = new Module('Emmylou');
However, note that this will only work with a single instance of the class because the scope stays the same. Creating an extra module will overwrite the private variables from the previous instance.
Odds and ends
Bugs and known limitations
Attributes are stored in an object internally. According to the Javascript spec
objects don't guarantee that properties are returned in order. However, all browsers
do return object properties in order except for Chrome
where numbered keys won't return in order.
In the future this will be fixed, but it requires a big rewrite. For now the best way to
prevent this problem is to simply avoid using numbered attributes. So don't write something like
module.set('1', 'one');
History
- 1.0.0
- Feature
get
now accepts multiple arguments so you can use it like Underscore's pick
(Issue #38). Thanks for the suggestion Santervo!
- Feature
mutate
events are now emitted whenever an attribute is removed (issue #35). Thanks Jasper!
- Not a feature per se, but the website got a complete makeover using the wonderful Bootstrap framework.
- 0.8.0 - July 9th 2013
create()
has been completely removed after the deprecation in 0.7.0
- Feature
set
now also accepts the silent
flag for objects (issue #28). Thanks for the suggestion Neogavin!
- Feature
remove
without any arguments now removes all attributes in a module.
- Bugfix The
silent
flag did not work with push
(issue #39). Thanks for reporting Gamadril!
- 0.7.1 - February 27th 2013
- Thanks to josher19 the docs now have nice tooltips
for all the methods in the in-code examples!
- Bugfix
Stapes.extend
was broken (issue #33). Thanks gonchuku!
- Bugfix Fixed the todos examples, thanks Erik!
- Bugfix Fixed events in subclasses. (issue #29),
thanks Eric!
- Bugfix Fixed a problem where unbinding an event would throw an error in some rare cases
(issue #30). Thanks Jasper!
- 0.7.0 - January 14th 2013
- Feature Completely revised the creation of new modules with
subclass
. The old create
is still available for backwards compatibility. Read here why i deprecated create
.
- Feature Added a
map
function to every module.
- Feature
update
now gets the module set as the
this
context, and gets the key in the callback function.
- Feature
update
now has a silent
option too.
- Feature
remove
now accepts multiple keys to remove.
- Bugfix
getAllAsArray
no longer overwrites
an id
value if its present in the object
- 0.6 - October 14th 2012
- Feature
remove
now also triggers namespaced events
(issue #13).
- Feature
set
, push
and
remove
now have an optional silent
flag that
will preveny any events from being triggered (issue #18)
- Feature
push
, set
,
remove
and update
now return the
module, so you can chain these methods (issue #17)
- Bugfix Specific events that were emitted on
Stapes.on
gave a wrong scope (issue #19)
- Bugfix Events didn't bubble up to the global
Stapes
object
(issue #12)
- Bugfix Fix for the
remove
handler in IE8. Thanks @wellcaffeinated!
Stapes.util
has been removed. If your code depends on it you can use a compatibility plugin.
- 0.5.1 - July 20th 2012
- Feature All private methods are now part of the
Stapes._
which means
plugin authors can overwrite and redefine every part of Stapes.
- Bugfix Fixed a bug where calling create() on a module wouldn't increase the guid
(issue #8). Thanks
@wellcaffeinated!
- 0.5 - July 2nd 2012
- Feature Add event to any method using the
mixinEvents
method
- Feature Added the
off
event method to remove event handlers
- The two todos examples are updated to the latest version from
TodoMVC
- 0.4 - May 10th 2012
- Feature Added the whole bunch of utility methods.
- Feature Added an
each
method to Modules for easy iteration over attributes
- Feature Added a
size
method to get the number of attributes in a module.
- Bugfix Data in event handlers was silently changed to
null
if it was falsy,
so passing false
as a data argument would result in handlers getting null
instead of
the expected data argument.
- Updated the two todos examples, using the versions from the TodoMVC collection
- Removed the undocumented
init
method that was part of every Stapes module
- Rewrote the util functions for cleaner code (including a
map
and an each
with a context parameter).
- 0.3 - April 5th 2012
- Feature Added the
mutate
change event.
- Feature
update
now accepts a single function as an argument too.
- Bugfix Setting an attribute with the same value as the old value doesn't trigger a change event anymore.
getAll()
and getAllAsArray()
now return a clone of the attributes instead of a reference.
- Removing the undocumented
changemany
and createmany
events.
- 0.2.1 - March 28th 2012
- Fixed a bug where global events didn't always work
because the guid was set to 0 initially. Thanks
@frenkie!
- 0.2 - March 4th 2012
Contributors
Thanks to all of you for contributing code, docs, reporting bugs, giving good advice and inspiration!