batman.js
batman.js is a framework for building rich single-page browser applications. It is written in CoffeeScript and its API is developed with CoffeeScript in mind, but of course you can use plain old JavaScript too.
It's got:
- a stateful MVC architecture
- a powerful binding system
- routable controller actions
- pure HTML views
- toolchain support built on node.js and cake
Batman's API is heavily inspired by Rails and designed to make Rails devs feel right at home.
Platforms
Batman runs in node and the browser. Batman supports node v0.4.x and v0.6.x. We're targeting Chrome, Safari 4+, Firefox 3+, and IE 7+ for compatibility. Batman expects ES5 features like Function::bind and Array::map, so we recommend poly-filling these for older browsers with something like es5shim or augmentjs. Batman also expects JSON.parse, which can be implemented in browsers without it using json2.js.
Installation
Batman is available as a standalone download or as an npm package.
Standalone Browser Version
Versions of batman suitable for use in a web application are available at GitHub.
node.js
If you haven't already, you'll need to install node.js. Then:
npm install -g batman
Generate a new batman.js app somewhere, called my_app:
cd ~/code
batman new my_app
Fire it up:
cd my_app
batman server # (or just "batman s")
Now visit http://localhost:1047 and start playing around!
Architecture
Batman prescribes an MVC architecture for client side applications which fits together like this:
- Models have validations, lifecycle events, a built-in identity map, and can be told how to persist themselves. (
Batman.LocalStorage,Batman.RestStorage, andBatman.RailsStorageare included). - Views are JavaScript classes which render templates written in pure HTML. The templates use
data-*attributes to create bindings to model data and trigger event handlers. - Controllers are persistent objects which mediate access to the model data, render the views, and handle events from the views.
A batman.js application is served up in one page load, followed by asynchronous requests for various resources as the user interacts with the app. Navigation within the app can use pushState where supported, or fall back on to hash-bang fragment identifers.
Batman Helpers
Batman includes an number of useful, general purpose helper functions and references. They can all be found attached to the Batman object and can optionally be exported into the global namespace with a $ prefix.
container
Batman.container points to either the window object if running the browser, or the global object if running in node. This is useful if you want to add something to the global scope in all environments.
typeOf(object) : string
typeOf determines a more specific type of an object than the native typeof operator in JavaScript. This is useful for a number of situations like dealing with Object promoted strings and numbers, or arrays which look like objects to typeof. Use typeOf when you need more than "object" from typeof.
Note: typeOf is substantially slower than typeof. typeOf works in a somewhat hackish manner by getting the Object::toString representation of the object and slicing it to retrieve the name of the constructor.
typeOf returns "String" for both strings and Object strings
primitive = "test"
objectified = new String("test")
typeof primitive
"string"
typeof objectified
"object"
Batman.typeOf(primitive)
"String"
Batman.typeOf(objectified)
"String"
typeOf returns Array for arrays
array = []
typeof array
"object"
Batman.typeOf(array)
"Array"
mixin(subject, objects...) : subject
mixin, occasionally known elsewhere as extend or merge, flattens a series of objects onto the subject. Key/value pairs on objects passed as later arguments (arguments with a higher index) take precedence over earlier arguments. Returns the subject passed in with the new values.
mixin also has special properties that make it different than the canonical extend functions:
- If the
subjecthas asetfunction,subject.set(key, value)will be used to apply keys instead ofsubject[key] = value. This means that if the subject is aBatman.Object, observers and thus bindings on the object will be notified when other (Batmanified or not) objects are mixed into it. - If a mixed-in
objecthas aninitializefunction defined, that function will be called and passed thesubject. This is useful for custom extension logic, similar toself.includedin Ruby. For this reason, the keysinitializeanduninitializeare skipped bymixin. mixinonly iterates over keys for which thehasOwnPropertytest passes.
Note: mixin is destructive to (only) the first argument. If you need a non-destructive version of mixin, just pass an empty object as the first object, and all keys from the successive arguments will be applied to the empty object.
mixin merges argument objects
subject = {}
Batman.mixin(subject, {
fit: true
}, {
fly: true
}, {
funky: true
})
{
fit: true,
fly: true,
funky: true
} // mixin returns the subject
subject
{
fit: true,
fly: true,
funky: true
} // the subject is modified destructively
mixin merges argument objects
unmodified = {
fit: true
}
Batman.mixin({}, unmodified, {
fly: true
}, {
funky: true
})
{
fit: true,
fly: true,
funky: true
} // mixin returns the subject
unmodified
{
fit: true
} // argument objects are untouched allowing non-destructive merge
mixed in objects passed as higher indexed arguments take precedence
subject = {}
Batman.mixin(subject, {
x: 1,
y: 1
}, {
x: 2
})
{
x: 2,
y: 1
}
unmixin(subject, objects...) : subject
unmixin "unmerges" the passes objects from the subject. If a key exists on any of the objects it will be deleted from the subject. Returns the subject.
unmixin, similar to mixin, supports calling an uninitialize function for each of the objects being unmixed in. If an uninitialize function exists on each
unmixin removes keys found on the unmixined objects on the subject
subject = {
fit: true,
fly: true,
funky: true
}
Batman.unmixin(subject, {
fit: true
}, {
fly: true
})
{
funky: true
} // unmixin returns the subject
subject
{
funky: true
} // the subject is destructively modified.
isChildOf(parent : HTMLElement, child : HTMLElement) : boolean
isChildOf is a simple DOM helper which returns a boolean describing if the passed child node can be found in the descendants of the passed parent node.
setImmediate(callback : Function) : object
setImmediate (and it's sister clearImmediate) are a more efficient version of setTimeout(callback, 0). Due to timer resolution issues, setTimeout passed a timeout of 0 doesn't actually execute the function as soon as the JS execution stack has been emptied, but at minimum 4ms and maxmium 25ms after. For this reason Batman provides a cross browser implementation of setImmediate which does it's best to call the callback immediately after the stack empties. Batman's setImmediate polyfill uses the native version if available, window.postmessage trickery if supported, and falls back on setTimeout(->, 0).
setImmediate returns a handle which can be passed to clearImmediate to cancel the future calling of the callback.
clearImmediate(handle)
clearImmediate stops the calling of a callback in the future when passed it's handle (which is returned from the setImmediate call used to enqueue it).
forEach(iterable : object, iterator : Function[, context : Object])
The forEach Batman helper is a universal iteration helper. When passed an iterable object, the helper will call the iterator (optionally in the context) for each item in the iterable. The iterable can be:
- something which has it's own
forEach, in which case theiteratorwhill just be passed toiterable.forEach. - an array like object, in which case a JavaScript
for(;;)loop will be used to iterate over each entry - or an object, in which case a JavaScript
for-inloop will be used to iterate over each entry.
The forEach helper is useful for iterating over objects when the type of those objects isn't guaranteed.
forEach iterates over objects with forEach defined
set = new Batman.SimpleSet('a')
Batman.forEach(set, function(x) {
return log(x);
})
logged.last
"a"
forEach iterates over array like objects
ArrayLike = function() {}
ArrayLike.prototype = []
imitation = new ArrayLike
Array.prototype.push.call(imitation, "a")
Array.prototype.push.call(imitation, "b")
Batman.forEach(imitation, function(x) {
return log(x);
})
logged.last
"b"
forEach iterates over objects
object = {
x: true
}
Batman.forEach(object, function(key, val) {
return log({
key: key,
val: val
});
})
logged.last
{
key: 'x',
val: true
}
objectHasKey(object, key) : boolean
objectHasKey returns a boolean describing the presence of the key in the passed object. objectHasKey delegates to the object's hasKey function if present, and otherwise just does a check using the JavaScript in operator.
objectHasKey verifies if a key is present in an object
subject = {
fit: true
}
Batman.objectHasKey(subject, 'fit')
true
Batman.objectHasKey(subject, 'flirty')
false
objectHasKey verifies if a key is present in an object with `hasKey` defined
subject = new Batman.SimpleHash({
fit: true
})
Batman.objectHasKey(subject, 'fit')
true
Batman.objectHasKey(subject, 'flirty')
false
contains(object, item) : boolean
contains returns a boolean describing if the given object has member item. Membership in this context is defined as:
- the result of
object.has(item)if theobjecthas ahasfunction defined - the result of
item in objectif theobjectis arraylike - the result of the Batman.objectHasKey otherwise
Note: When passed an object without a has function, contains will return true if the object has item as a key, not as a value at any key.
contains is useful for checking item membership when the type of the object can't be relied on.
get(object, key) : value
get is a general purpose function for retrieving the value from a key on an object of an indeterminate type. This is useful if code needs to work with both Batman.Objects and Plain Old JavaScript Objects. get has the following semantics:
- if the
objecthas agetfunction defined, return the result ofobject.get(key) - if the object does not have a
getfunction defined, use an ephemeralBatman.Propertyto retrieve the key. This is equivalent toobject[key]for single segmentkeys, but if thekeyis multi-segment (example: 'product.customer.name'),getwill do nested gets until the eitherundefinedor the end of the keypath is reached.
get returns the value at a key on a POJO
subject = {
fit: true
}
Batman.get(subject, 'fit')
true
Batman.get(subject, 'flirty')
void 0
get returns the value at a key on a Batman.Object
subject = Batman({
fit: true
})
Batman.get(subject, 'fit')
true
Batman.get(subject, 'flirty')
void 0
get returns the value at a deep key on a POJO
subject = {
customer: {
name: "Joe"
}
}
Batman.get(subject, 'customer.name')
"Joe"
Batman.get(subject, 'customer.age')
void 0
get returns the value at a deep key on a Batman.Object
subject = Batman({
customer: {
name: "Joe"
}
})
Batman.get(subject, 'customer.name')
"Joe"
Batman.get(subject, 'customer.age')
void 0
escapeHTML(string) : string
escapeHTML takes a string of unknown origin and makes it safe for display on a web page by encoding control characters in HTML into their HTML entities.
Warning: Do not rely on escapeHTML to purge unsafe data from user submitted content. While escapeHTML is applied to every binding's contents by default, it should not be your only line of defence against script injection attacks.
escapeHTML encodes special characters into HTML entities
Batman.escapeHTML("& < > \" '")
"& < > " '"
Batman.helpers
Batman.helpers is a namespace for Batman's helpful string manipulation helpers.
Note: Batman's pluralization functions mirror those of Rails' exactly.
ordinalize(value : [number|string]) : string
ordinalize converts a given integer into an ordinal string for describing position in a list, like 1st, 2nd, or 20th.
ordinalize converts numbers to their ordinal form
Batman.helpers.ordinalize(1)
"1st"
Batman.helpers.ordinalize("2")
"2nd"
Batman.helpers.ordinalize(1002)
"1002nd"
Batman.helpers.ordinalize("1003")
"1003rd"
Batman.helpers.ordinalize(-11)
"-11th"
Batman.helpers.ordinalize(-1021)
"-1021st"
singularize(pluralString : string) : string
singularize converts the plural form of a word to a singular form.
singularize converts plural words to singular words
Batman.helpers.singularize("posts")
"post"
Batman.helpers.singularize("octopi")
"octopus"
Batman.helpers.singularize("sheep")
"sheep"
Batman.helpers.singularize("word")
"word"
Batman.helpers.singularize("CamelOctopi")
"CamelOctopus"
pluralize(singularString : string) : string
pluralize converts the singular form of a word to the plural form.
pluralize converts plural words to singular words
Batman.helpers.pluralize("post")
"posts"
Batman.helpers.pluralize("octopus")
"octopi"
Batman.helpers.pluralize("sheep")
"sheep"
Batman.helpers.pluralize("words")
"words"
Batman.helpers.pluralize("CamelOctopus")
"CamelOctopi"
camelize(string, [lowercaseFirstLetter = false]) : string
camelize converts the passed string to UpperCamelCase. If the second argument is passed as true, then lowerCamelCase is returned.
camelize returns the CamelCase version of an under_scored word
Batman.helpers.camelize("batman_object")
"BatmanObject"
Batman.helpers.camelize("batman_object", true)
"batmanObject"
underscore(string) : string
underscore returns the underscored version of a CamelCase word.
underscore converts CamelCase to under_scores
Batman.helpers.underscore("BatmanObject")
"batman_object"
capitalize(string) : string
capitalize does a word-wise capitalization of a phrase or word.
capitalize makes the first letter of each word in the string uppercase
Batman.helpers.capitalize("batmanObject")
"BatmanObject"
Batman.helpers.capitalize("batman object")
"Batman Object"
Batman.helpers.capitalize("AlreadyCapitalized")
"AlreadyCapitalized"
Batman.EventEmitter
EventEmitter is a mixin which can be applied to any object to give it the ability to fire events and accept listeners for those events.
on(key, handler)
Attaches a function handler to the event with name key. This function will be executed every time the event fires.
event handlers execute when attached with on
dynamite = $mixin({}, Batman.EventEmitter)
dynamite.on('detonate', function() {
return log("detonated");
})
true
fire(key, arguments...)
Calls all previously attached handlers on the event with name key. All handlers will receive the passed arguments.
Note: Calling fire doesn't guarantee the event will fire since firing can be prevented with prevent or preventAll.
event handlers are fired
dynamite = $mixin({}, Batman.EventEmitter)
dynamite.on('detonate', function(noise) {
return log("detonated with noise " + noise);
})
dynamite.fire('detonate', "BOOM!")
logged.last
"detonated with noise BOOM!"
hasEvent(key) : boolean
Asks if the EventEmitter has an event with the given key.
events can be tested for presence
dynamite = $mixin({}, Batman.EventEmitter)
dynamite.on('detonate', function() {
return log("detonated");
})
dynamite.hasEvent('detonate')
true
dynamite.hasEvent('click')
false
oneShot : boolean
Events can be set to fire only once, and then fire subsequently attached handlers immediately if they are attached after the initial firing. This is useful for events similar to window.onload where they really only happen once in the lifespan of the application, but you don't want to check if they have happened already when attaching event handlers.
Access the Event object to set the oneShot property on them using EventEmitter::event.
one shot events fire handlers attached after they have fired for the first time
dynamite = $mixin({}, Batman.EventEmitter)
dynamite.event('detonate').oneShot = true
dynamite.fire('detonate')
dynamite.on('detonate', function() {
return log("detonated immediately!!");
})
logged.last
"detonated immediately!!" // The handler was called as soon as it was attached.
prevent(key) : EventEmitter
Prevents the event with name key from firing, even if .fire is called. This is useful if you need to guarantee a precondition is in fulfilled before allowing event handlers to execute. Returns the event emitting object.
Undo event prevention with allow or allowAndFire.
Note: prevent can be called more than once to effectively "nest" preventions. allow or allowAndFire must be called the same number of times or more for events to fire once more.
events can be prevented
dynamite = $mixin({}, Batman.EventEmitter)
dynamite.prevent('detonate')
dynamite.on('detonate', function() {
return log("This shouldn't fire");
})
dynamite.fire('detonate')
logged.last
void 0 // The event handler wasn't fired.
prevent returns the event emitter
dynamite = $mixin({}, Batman.EventEmitter)
dynamite
dynamite.prevent('detonate')
allow(key) : EventEmitter
Allows the event with name key to fire once more, after prevent had been called previously. allow will not fire the event when called, regardless of weather or not the event can now be fired or if an attempt to fire it was made while the event was prevented. Returns the event emitting object.
Note: prevent can be called more than once to effectively "nest" preventions. allow or allowAndFire must be called the same number of times or more for events to fire once more.
events can be allowed after prevention
dynamite = $mixin({}, Batman.EventEmitter)
dynamite.prevent('detonate')
dynamite.on('detonate', function() {
return log("This will only fire once");
})
dynamite.fire('detonate')
logged.length
0 // The event handler wasn't fired.
dynamite.allow('detonate')
dynamite.fire('detonate')
logged.length
1 // The event handler was fired.
events must be allowed the same number of times they have been prevented
dynamite = $mixin({}, Batman.EventEmitter)
dynamite.prevent('detonate')
dynamite.prevent('detonate')
dynamite.on('detonate', function() {
return log("This will only fire once");
})
dynamite.fire('detonate')
logged.length
0 // The event handler wasn't fired, the prevent count is at 2.
dynamite.allow('detonate')
dynamite.fire('detonate')
logged.length
0 // The event handler still wasn't fired, but the prevent count is now at 1.
dynamite.allow('detonate')
dynamite.fire('detonate')
logged.length
1 // The event handler was fired.
allow returns the event emitter
dynamite = $mixin({}, Batman.EventEmitter)
dynamite
dynamite.allow('detonate')
allowAndFire(key)
Allows the event with name key to fire once more, and tries to fire it. allowAndFire may fail to fire the event if prevent has been called more times than allow or allowAndFire have previous.
events can be allowed and fired after prevention
dynamite = $mixin({}, Batman.EventEmitter)
dynamite.on('detonate', function() {
return log("This will only fire once");
})
dynamite.prevent('detonate')
dynamite.fire('detonate')
logged.length
0 // The event handler wasn't fired.
dynamite.allowAndFire('detonate')
logged.length
1 // The event handler was fired.
events must be be allowed and fired the same number of times they have been prevented
dynamite = $mixin({}, Batman.EventEmitter)
dynamite.on('detonate', function() {
return log("This will only fire once");
})
dynamite.prevent('detonate')
dynamite.prevent('detonate')
dynamite.allowAndFire('detonate')
logged.length
0 // The event handler wasn't fired.
dynamite.allowAndFire('detonate')
logged.length
1 // The event handler was fired.
Batman.Observable
Batman.Observable is a mixin which gives objects the ability to notify subscribers to changes on its properties. Observable also adds functionality for observing keypaths: arbitrarily deeply nested properties on objects.
get(keypath) : value
Retrieves the value at a key on an object. Accepts keypaths.
Note: get must be used for property access on any object in Batman's world. This is so that Batman can implement neat things like automatic dependency calculation for computed properties, property caching where it is safe, and smart storage mechanisms. With Batman, you must use get instead of normal . property access.
get retrieves properties on Batman objects
song = Batman({
length: 340,
bpm: 120
})
song.get('length')
340
song.get('bpm')
120
get retrieves properties on nested Batman objects using keypaths
post = Batman({
text: "Hello World!",
author: Batman({
name: "Harry"
})
})
post.get('author.name')
"Harry"
get retrieves properties on Batman objects when . property access doesn't
song = new Batman.Model({
length: 340,
bpm: 120
})
typeof song.length
"undefined"
song.get('length')
340
set(keypath, newValue) : newValue
Stores the value at a key on an object. Accepts keypaths. Returns the new value of the property.
Note: Once more, set must be used for property mutation on all objects in the Batman world. This is again so that Batman can implement useful functionality like cache busting, eager recalculation of computed properties, and smarter storage.
Note: Custom setters can mutate the value during setting, so the value which was passed to set and set's return value are not guaranteed to be identical.
set stores properties on batman objects.
song = Batman({
length: 340,
bpm: 120
})
song.get('length')
340
song.set('length', 1000)
1000
song.get('length')
1000
set stores properties on nested Batman objects using keypaths
author = Batman({
name: "Harry"
})
post = Batman({
text: "Hello World!",
author: author
})
post.set('author.name', "Nick")
"Nick"
author.get('name')
"Nick" // The value was set on the nested object.
set is incompatible with '.' property mutation
song = new Batman.Model({
length: 340,
bpm: 120
})
song.get('length')
340
song.length = 1000
1000
song.get('length')
340 // The song length reported by Batman is unchanged because set wasn't used to change the value.
unset(keypath) : value
Removes the value at the given keypath, leaving it undefined. Accepts keypaths. Returns the value the property had before unsetting.
unset is roughly equivalent to set(keypath, undefined), however, custom properties can define a nonstandard unset function, so it is best to use unset instead of set(keypath, undefined) wherever possible.
unset removes the property on Batman objects
song = Batman({
length: 340,
bpm: 120
})
song.get('length')
340
song.unset('length')
340
song.get('length')
void 0 // The value is unset.
unset removes the property at a keypath
author = Batman({
name: "Harry"
})
post = Batman({
text: "Hello World!",
author: author
})
post.unset('author.name')
"Harry"
author.get('name')
void 0 // The value was unset on the nested object.
observe(key, observerCallback) : this
Adds a handler to call when the value of the property at the key changes upon setting. Accepts keypaths.
observe is the very core of Batman's usefulness. As long as set is used everywhere to do property mutation, any object can be observed for changes to its properties. This is critical to the concept of bindings, which Batman uses for it's views.
The observerCallback gets called whenever the key changes with the arguments newValue, oldValue.
Returns the object observe was called upon.
observe attaches handlers which get called upon change
song = Batman({
length: 340,
bpm: 120
})
song.observe('length', function(newValue, oldValue) {
return log([newValue, oldValue]);
})
song.set('length', 200)
200
logged.last
[200, 340]
song.set('length', 300)
300
logged.last
[300, 200]
Note: observe works excellently on keypaths. If you attach a handler to a "deep" keypath, it will fire any time the value of that keypath changes, which is another way of saying the handler will fire when any segment of the keypath changes, passing in the new value at the end of the keypath.
observe attaches handlers which get called upon change
author = Batman({
name: "Harry"
})
post = Batman({
text: "Hello World!",
author: author
})
post.observe('author.name', function(newName, oldName) {
return log([newName, oldName]);
})
post.set('author', newAuthor = Batman({
name: "James"
}))
logged.last
["James", "Harry"] // The observer fired when the 'author' segment of the keypath changed.
observeAndFire(key, observerCallback) : this
Adds the observerCallback as an observer to key, and fires it immediately. Accepts the exact same arguments and follows the same semantics as Observable::observe, but the observer is fired with the current value of the keypath it observers synchronously during the call to observeAndFire.
During the initial synchronous firing of the callback, the newValue and oldValue arguments will be the same value: the current value of the property. This is because the old value of the property is not cached and therefore unavailable. If your observer needs the old value of the property, you must attach it before the set on the property happens.
observeAndFire calls the observer upon attaching it with the currentValue of the property
song = Batman({
length: 340,
bpm: 120
})
song.observeAndFire('length', function(newValue, oldValue) {
return log([newValue, oldValue]);
})
logged.last
[340, 340]
song.set('length', 300)
300
logged.last
[300, 340]
forget([key [, observerCallback]]) : this
If observerCallback and key are given, that observer is removed from the observers on key. If only a key is given, all observers on that key are removed. If no key is given, all observers on all keys are removed. Accepts keypaths.
Returns the object forget was called upon.
forget removes an observer from a key if the key and the observer are given
song = Batman({
length: 340,
bpm: 120
})
observer = function(newValue, oldValue) {
return log([newValue, oldValue]);
}
song.observe('length', observer)
song.set('length', 200)
200
logged.last
[200, 340]
song.forget('length', observer)
song.set('length', 300)
300
logged.last
[200, 340] // The logged values haven't changed because the observer hasn't fired again.
forget removes all observers from a key if only the key is given
song = Batman({
length: 340,
bpm: 120
})
observerA = (function(newValue, oldValue) {
return log([newValue, oldValue]);
})
observerB = (function(newValue, oldValue) {
return log([newValue, oldValue]);
})
song.observe('length', observerA)
song.observe('length', observerB)
song.set('length', 200)
200
logged.length
2 // Both length observers fired.
song.forget('length')
song.set('length', 300)
300
logged.length
2 // Nothing more has been logged because neither observer fired.
forget removes all observers from all key if no key is given
song = Batman({
length: 340,
bpm: 120
})
observerA = (function(newValue, oldValue) {
return log([newValue, oldValue]);
})
observerB = (function(newValue, oldValue) {
return log([newValue, oldValue]);
})
song.observe('length', observerA)
song.observe('bpm', observerB)
song.set('length', 200)
200
logged.length
1 // The length observer fired.
song.forget()
song.set('length', 300)
300
song.set('bpm', 130)
130
logged.length
1 // Nothing more has been logged because neither observer fired.
getOrSet(keypath, valueFunction) : value
Assigns the keypath to the result of calling the valueFunction if the current value at the keypath is falsey. Returns the value of the property after the operation, be it changed or not. Equivalent to CoffeeScript's ||= operator.
getOrSet doesn't set the property if it exists
song = Batman({
length: 340,
bpm: 120
})
song.getOrSet('length', function() {
return 500;
})
340
song.get('length')
340
getOrSet sets the property if it is falsey
song = Batman({
length: 340,
bpm: 120
})
song.getOrSet('artist', function() {
return "Elvis";
})
"Elvis"
song.get('artist')
"Elvis"
Batman.Object
Batman.Object is the superclass for virtually all objects in a Batman application. Batman.Object mixes in Batman.Observable and Batman.EventEmitter for things like get, set, observe, and fire, and then defines some more useful things for tying everything together.
@accessor([keys...], objectOrFunction)
Accessors are used to create properties on a class, prototype, or instance which can be fetched, set, and unset. These properties can be static, computed as functions of the other properties on the object the accessor belongs to, or properties of any Batman object in the system. accessor is a Batman and old browser friendly version of ES5 Object.defineProperty missing some of the goodness.
The value of custom accessors can be observed just like any property. Accessors also track which other properties they rely on for computation, and recalculate eagerly when those other properties change. This way, when a source value is changed, any dependent accessors will automatically update any bindings to them with a new value. Accessors accomplish this feat by tracking get calls, so do be sure to use get to retrieve properties on Batman Objects inside accessors so those properties can be tracked as dependencies. The property dependencies of an accessor are called "sources" in the Batman world.
Importantly, accessors are also inherited, so accessors defined anywhere in an object's prototype chain will be used. Following this, @accessor is meant to be used during the class definition of a class extending Batman.Object.
Arguments
@accessor can be called with zero, one, or many keys for the accessor to define. This has the following effects:
- zero: create a
defaultAccessor, which will be called when no other properties or accessors on an object match a keypath. This is similar tomethod_missingin Ruby or#doesNotUnderstandin Smalltalk. - one: create a
keyAccessorat the given key, which will only be called when that key is gotten, set, or unset. - many: create
keyAccessorsfor each given key, which will then be called whenever each one of the listed keys is gotten, set, or unset.
@accessor accepts as the last argument either an object with any combination of the get, set, and unset keys defined. Functions which implement the behaviour for those particular actions on the property should reside at these keys. @accessor also accepts a function as the last argument, which is a shorthand for specifying the get implementation for the accessor.
Uses
Accessors are a really useful addition to the world of JavaScript. You can now define transforms on simple properties which will automatically update when the properties they transform change, for example you might want to truncate a potentially long piece of text to display a summary elsewhere, or you might want to capitalize or encodeURIComponent a value before putting it in the view or the current URL.
@accessor can be called on a class to define how a property is calculated
Post = (function(_super) {
__extends(Post, _super);
function Post() {
return Post.__super__.constructor.apply(this, arguments);
}
Post.accessor('summary', function() {
return this.get('body').slice(0, 10) + "...";
});
return Post;
})(Batman.Object)
post = new Post({
body: "Why Batman is Useful: A lengthy post on an important subject"
})
post.get('summary')
"Why Batman..."
You can also use accessors to combine properties; the colloquial fullName example comes to mind, but all sorts of other complex logic can be abstracted away using the accessor pattern.
@accessor can define a transform on several properties
User = (function(_super) {
__extends(User, _super);
function User() {
return User.__super__.constructor.apply(this, arguments);
}
User.accessor('fullName', function() {
return this.get('firstName') + " " + this.get('lastName');
});
return User;
})(Batman.Object)
tim = new User({
firstName: "Tim",
lastName: "Thomas"
})
tim.get('fullName')
"Tim Thomas"
tim.set('firstName', "Timmy")
tim.get('fullName')
"Timmy Thomas"
Accessors can define custom get, set, and unset functions to support each operation on the property:
@accessor can define the get, set, and unset methods for the property
AbsoluteNumber = (function(_super) {
__extends(AbsoluteNumber, _super);
function AbsoluteNumber() {
return AbsoluteNumber.__super__.constructor.apply(this, arguments);
}
AbsoluteNumber.accessor('value', {
get: function() {
return this._value;
},
set: function(_, value) {
return this._value = Math.abs(value);
},
unset: function() {
return delete this._value;
}
});
return AbsoluteNumber;
})(Batman.Object)
number = new AbsoluteNumber({
value: -10
})
number.get('value')
10
Importantly, it is also safe to use branching, loops, or whatever logic you want in accessor bodies:
@accessor can use arbitrary logic to define the value
Player = (function(_super) {
__extends(Player, _super);
function Player() {
return Player.__super__.constructor.apply(this, arguments);
}
Player.accessor('score', function() {
if (this.get('played')) {
return (this.get('goals') * 2) + (this.get('assists') * 1);
} else {
return 0;
}
});
return Player;
})(Batman.Object)
rick = new Player({
played: false,
goals: 0,
assists: 0
})
rick.get('score')
0
rick.set('played', true)
rick.get('score')
0
rick.set('goals', 3)
rick.get('score')
6
rick.set('assists', 1)
rick.get('score')
7
Caveats
Accessors are extremely useful but you must keep these items in mind while programming using them:
- Accessors should be linear, so they can be cached.
Batman automatically memoizes the return value of accessors, and will not re-execute the body until one of the accessor's sources changes. If you need the accessor to recalculate every time the property is gotten, pass false for the cache option in the accessor descriptor object (the last argument to the @accessor function).
@accessor usually caches results
counter = 0
Example = (function(_super) {
__extends(Example, _super);
function Example() {
return Example.__super__.constructor.apply(this, arguments);
}
Example.accessor('cachedCounter', function() {
return ++counter;
});
Example.accessor('notCachedCounter', {
get: function() {
return ++counter;
},
cache: false
});
return Example;
})(Batman.Object)
example = new Example()
example.get('cachedCounter')
1
example.get('cachedCounter')
1
example.get('cachedCounter')
1 // The second and third calls do not execute the function
example.get('notCachedCounter')
2
example.get('notCachedCounter')
3 // Passing cache: false does re-execute the function
example.get('cachedCounter')
1
- Accessors must use
getto access properties they use for computation
Batman tracks an accessor's sources by adding a global hook to all gets done, so if you don't use get to access properties on objects, Batman can't know that that property is a source of the property your accessor defines, so it can't recompute that property when the source property changes. All properties on Batman.Object should be accessed using get and set regardless if the code occurs in an accessor body or not, but it is critical to do so in accessors so the sources of the accessor can be tracked.
- Accessors can create memory leaks or performance bottlenecks
If you return a brand new object by say merging a number of Batman.Sets or doing any sort of major and complete re-computation you run the risk of creating performance problems. This is because accessors can be called frequently and unpredictably, as they are recomputed every time one of their sources changes as well as upon every set. Instead of recomputing expensive things every time the accessor is called, try to use objects which do delta recomputation using observers. Practically this translates to using things like new SetUnion(@get('setA'), @get('setB')) instead of @get('setA').merge(@get('setB')) in an accessor body.
@classAccessor([keys...], objectOrFunction)
classAccessor defines an accessor on the class, in that gets and sets done on the class will use the accessor definition as an implementation. @accessor called on a class will define an accessor for all instances of that class, where as @classAccessor defines accessors on the class object itself. See @accessor for the details surrounding accessors.
@classAccessor defines an accessor on the class
SingletonDooDad = (function(_super) {
__extends(SingletonDooDad, _super);
function SingletonDooDad() {
return SingletonDooDad.__super__.constructor.apply(this, arguments);
}
SingletonDooDad.classAccessor('instance', function() {
return new this();
});
return SingletonDooDad;
})(Batman.Object)
instance = SingletonDooDad.get('instance')
SingletonDooDad.get('instance') === instance
true // A second get returns the same instance
@mixin(objects...) : prototype
@mixin is a handy function for mixing in objects to a class' prototype. @mixin is implemented on top of the Batman level mixin helper, which means that keys from incoming objects will be applied using set, and any initialize functions on the objects will be called with the prototype being mixed into. Returns the prototype being mixed into.
Note: @mixin, similar to @accessor, applies to all instances of a class. If you need to mix in to the class itself, look at classMixin.
note: @mixin, once more similar to @accessor, is intended for use during the class definition of a Batman.Object subclass.
@mixin extends the prototype of a Batman.Object subclass
FishBehaviour = {
canBreathUnderwater: true
}
MammalBehaviour = {
canBreathAboveWater: true
}
Platypus = (function(_super) {
__extends(Platypus, _super);
function Platypus() {
return Platypus.__super__.constructor.apply(this, arguments);
}
Platypus.mixin(FishBehaviour, MammalBehaviour);
return Platypus;
})(Batman.Object)
platypus = new Platypus
platypus.get('canBreathAboveWater')
true
platypus.get('canBreathUnderwater')
true
@classMixin(objects...) : this
@classMixin allows mixing in objects to a class during that class' definition. See @mixin for information about the arguments passed to mixin, but note that @classMixin applies to the class object itself, and @mixin applies to all instances of the class. Returns the class being mixed into.
@classMixin extends the Batman.Object subclass
Singleton = {
initialze: function(subject) {
return subject.accessor('instance', function() {
return new subject;
});
}
}
Highlander = (function(_super) {
__extends(Highlander, _super);
function Highlander() {
return Highlander.__super__.constructor.apply(this, arguments);
}
Highlander.classMixin(Singleton);
return Highlander;
})(Batman.Object)
instance = Highlander.get('instance')
instance === Highlander.get('instance')
true // There can only be one.
@observeAll(key, callback : function) : prototype
@observeAll extends the Batman.Object implementation of Batman.Observable with the ability to observe all instances of the class (and subclasses). Observers attached with @observeAll function exactly as if they were attached to the object directly. Returns the prototype of the class.
Note: @observeAll is intended to be used during the class definition for a Batman.Object subclass, but it can be called after the class has been defined as a function on the class. It supports being called after instances of the class have been instantiated as well.
@observeAll attaches handlers which get called upon change
Song = (function(_super) {
__extends(Song, _super);
function Song() {
return Song.__super__.constructor.apply(this, arguments);
}
Song.observeAll('length', function(newValue, oldValue) {
return log(newValue);
});
return Song;
})(Batman.Object)
song = new Song({
length: 340,
bpm: 120
})
song.set('length', 200)
200
logged.last
200
@observeAll can attach handlers after instance instantiation
Song = (function(_super) {
__extends(Song, _super);
function Song() {
return Song.__super__.constructor.apply(this, arguments);
}
return Song;
})(Batman.Object)
song = new Song({
length: 340,
bpm: 120
})
song.set('length', 360)
360
logged.last
void 0
Song.observeAll('length', function(newValue, oldValue) {
return log(newValue);
})
song.set('length', 200)
200
logged.last
200
constructor(objects...)
To create a new Batman.Object, the Batman.Object constructor can be used, or, the Batman namespace is also a utility function for creating Batman objects. Each object passed in to the constructor will have all its properties applied to the new Batman.Object using get and set, so any custom getters or setters will be respected. Objects passed in last will have precedence over objects passed in first in the event that they share the same keys. The property copy from these objects is shallow.
Batman() function allows for handy creation of Batman.Objects
object = Batman({
foo: 'bar'
})
typeof object.get
'function'
Batman.Object constructor function accepts multiple mixin arguments and later mixins take precedence.
song = Batman({
length: 100,
bpm: 120
}, {
bpm: 130
})
song.get('length')
100
song.get('bpm')
130 // The property from the second object passed to the constructor overwrites that from the first.
toJSON() : object
toJSON returns a vanilla JavaScript object representing this Batman.Object.
toJSON returns a vanilla JS object
object = Batman({
foo: 'bar'
})
object.toJSON()
{
foo: 'bar'
}
hashKey() : string
hashKey returns a unique string representing this particular Batman.Object. No two Batman.Objects will have the same hashKey.
accessor([keys...], objectOrFunction)
accessor defines an accessor on one instance of an object instead of on all instances like the class level @accessor. See @accessor for the details surrounding accessors.
accessor can be called on an instance of Batman.Object to define an accessor just on that instance
Post = (function(_super) {
__extends(Post, _super);
function Post() {
return Post.__super__.constructor.apply(this, arguments);
}
Post.accessor('summary', function() {
return this.get('body').slice(0, 10) + "...";
});
return Post;
})(Batman.Object)
post = new Post({
body: "Why Batman is Useful: A lengthy post on an important subject"
})
post.get('summary')
"Why Batman..."
post.accessor('longSummary', function() {
return this.get('body').slice(0, 20) + "...";
})
post.get('longSummary')
"Why Batman is Useful..."
defining an accessor on an instance does not affect the other instances
Post = (function(_super) {
__extends(Post, _super);
function Post() {
return Post.__super__.constructor.apply(this, arguments);
}
return Post;
})(Batman.Object)
post = new Post({
body: "Why Batman is Useful: A lengthy post on an important subject"
})
otherPost = new Post({
body: "Why State Machines Are Useful: Another lengthy post"
})
post.accessor('longSummary', function() {
return this.get('body').slice(0, 20) + "...";
})
post.get('longSummary')
"Why Batman is Useful..."
otherPost.get('longSummary')
void 0
mixin(objects...) : this
mixin extends the object it's called on with the passed objects using the Batman.mixin helper. Returns the object it's called upon.
Note: Since the Batman.mixin helper is used, mixin functionality like using set to apply properties and calling initialize functions is included in the instance level mixin function.
mixin on an instance applies the keys from the mixed in object to the instance
Snake = (function(_super) {
__extends(Snake, _super);
function Snake() {
return Snake.__super__.constructor.apply(this, arguments);
}
return Snake;
})(Batman.Object)
snake = new Snake()
snake.mixin({
canSlither: true
}, {
canHiss: true
})
snake.get('canSlither')
true
snake.get('canHiss')
true
Batman.SimpleSet
SimpleSet is an implementation of the Set object you might expect to find in lots of other languages standard libraries. Sets are an enumerable but unordered collection of objects. A particular object may either be a member or not be a member of a particular set, but it may not belong to the set twice, as that would imply ordering. In the case of Batman, sets will tell you when items are added or removed through events, sets mix in the Enumerable module for handy iteration and aggregation, and can also generate indexes and sorts via SetIndex and SetSort.
SimpleSet vs Set
SimpleSet and Set are two distinct classes in Batman. SimpleSet implements the set semantics as described above, but it is not a Batman.Object, so properties on it (like it's length or toArray) can not be observed, and thus not bound. Set is a Batman.Object, so it can be observed, and thus plays nice with the accessor system. Use a SimpleSet only when you know nothing will need to be observed on the set you are creating, which usually isn't a valid assumption. If it is in fact correct, consider using a native array as well, because iteration and in fact membership checks will be faster.
constructor(items...)
When creating a SimpleSet, items forming the initial set can be passed as separate arguments to the constructor
new SimpleSet constructor can be called without arguments
set = new Batman.SimpleSet
set.toArray()
[]
new SimpleSet constructor can be passed items to add to the set.
set = new Batman.SimpleSet('a', 'b', 'c')
set.toArray().sort()
['a', 'b', 'c']
length : number
A count of the items in a SimpleSet can be found at it's length property.
has(item) : Boolean
has returns a boolean describing if the given item is a member of the set.
SimpleSet::has indicates if an item is a member of the set or not.
set = new Batman.SimpleSet('a', 'b', 'c')
set.has('a')
true
set.has('d')
false
add(items...) : Array
add adds 0 or more new items to the set. add returns an array of the items which have been newly added to the set, which is to say the intersection of the argument items and the set itself before addition.
SimpleSet::add adds an item to the set
set = new Batman.SimpleSet()
set.has('a')
false
set.add('a')
['a']
set.has('a')
true
SimpleSet::add returns only the new items that weren\'t previously in the set
set = new Batman.SimpleSet('a', 'b')
set.add('b', 'c', 'd').sort()
['c', 'd']
set.toArray().sort()
['a', 'b', 'c', 'd']
remove(items...) : Array
remove removes 0 or more items from the set. remove returns an array of the items which were successfully removed from the set, which is to say the intersection of the argument items and the set itself before removal.
SimpleSet::remove removes an item from the set
set = new Batman.SimpleSet('a')
set.has('a')
true
set.remove('a')
['a']
set.has('a')
false
SimpleSet::remove returns only the new items that were previously in the set
set = new Batman.SimpleSet('a', 'b')
set.remove('b', 'c', 'd').sort()
['b']
set.toArray()
['a']
find(testFunction : function) : [Object]
find returns the first item within the set for which the testFunction called with the item returns true, or undefined if no item passes the test.
Note: find returns the first item the test passes for, but since set iteration has no specified order, no guarantee can be made about which item will be returned if more than one item in the set passes the test. However, set iteration order is stable, so find called on the same set twice should return the same item.
SimpleSet::find returns the first item for which the test function passes
set = new Batman.SimpleSet(1, 2, 3)
set.find(function(x) {
return x % 2 === 0;
})
2
SimpleSet::find returns undefined if no items pass the test function
set = new Batman.SimpleSet(1, 2, 3)
typeof set.find(function(x) {
return x > 5;
})
'undefined'
forEach(iteratorFunction : function[, context: Object])
forEach calls the iteratorFunction with each item in the set, optionally executing the iteratorFunction in the passed context. Returns undefined.
Note: Set iteration order is not defined, which is to say Set client code cannot rely on one item being iterated over before another, regardless of when the Set's items were added. If you need an ordered set, Batman provides SetSort for exactly this, while including the added benefit of observability. If you need a one time ordering of a set, you can get the array representation with toArray and then use vanilla JavaScript sort on that array.
SimpleSet::forEach iterates over each item in the set
sum = 0
set = new Batman.SimpleSet(1, 2, 3)
set.forEach(function(x) {
return sum += x;
})
sum
6
SimpleSet::forEach iterates over each item in the set optionally in the provided context
context = {
sum: 0
}
set = new Batman.SimpleSet(1, 2, 3)
set.forEach(function(x) {
return this.sum += x;
}, context)
context.sum
6
isEmpty() : boolean
isEmpty returns a boolean: true if the set has no items, and false if it has any items.
SimpleSet::isEmpty returns true if the set has no items
set = new Batman.SimpleSet()
set.isEmpty()
true
set.add('a')
set.isEmpty()
false
clear() : Array
clear removes all items from a set. Returns an array of all the items in the set.
Note: Set iteration order is not defined, so the order of the array of items returned by clear is undefined.
SimpleSet::clear empties the set
set = new Batman.SimpleSet('a', 'b', 'c')
set.isEmpty()
false
set.clear().sort()
['a', 'b', 'c']
set.isEmpty()
true
replace(collection : Enumerable) : Array
replace removes all the items in a set and then adds all the items found in another collection. The other collection must have a toArray function which returns an array representation of the collection. Returns the array of items added.
SimpleSet::replace empties the set and then adds items from a different collection
set = new Batman.SimpleSet('a', 'b', 'c')
secondSet = new Batman.SimpleSet('d', 'e', 'f')
set.replace(secondSet).sort()
['d', 'e', 'f']
set.toArray().sort()
['d', 'e', 'f']
toArray() : Array
toArray returns an array representation of the set.
Note: Set iteration order is not defined, so the order in which the set's items appear in the array is not defined. It is however stable, so the order of the items in two successive toArray calls where the set was not modified in between should be the same.
SimpleSet::toArray returns an array representation of the set
set = new Batman.SimpleSet()
set.toArray()
[]
set.add('a', 'b', 'c')
set.toArray().sort()
['a', 'b', 'c']
merge(collections... : Enumerable) : SimpleSet
merge adds all the items in a set and all the items in the passed collections to a new set and returns it. A collection is an object which has a forEach function. It is a non-destructive collection union. The set merge is called on and each collection passed to merge remain unaffected.
SimpleSet::merge returns a new set with the items of the original set and the passed set
abc = new Batman.SimpleSet('a', 'b', 'c')
def = new Batman.SimpleSet('d', 'e', 'f')
Batman.typeOf(set = abc.merge(def))
'Object'
set.toArray().sort()
['a', 'b', 'c', 'd', 'e', 'f']
indexedBy(key : String) : SetIndex
indexedByUnique(key : String) : UniqueSetIndex
sortedBy(key: String) : SetSort
Batman.Set
Set is an observable, Batman.Object wrapper around SimpleSet.
SimpleSet vs Set
SimpleSet and Set are two distinct classes in Batman. SimpleSet implements the basic set semantics, but it is not a Batman.Object, so properties on it (like it's length or toArray) can not be bound. Set is a Batman.Object, so it can be observed, and thus plays nice with the accessor system. Use a SimpleSet only when you know nothing will need to be observed on the set you are creating, which usually isn't a valid assumption. If it is in fact correct, consider using a native array as well, because iteration and in fact membership checks will be faster.
constructor(items...)
When creating a Set, items forming the initial set can be passed as separate arguments to the constructor
new Set constructor can be called without arguments
set = new Batman.Set
set.toArray()
[]
new Set constructor can be passed items to add to the set.
set = new Batman.Set('a', 'b', 'c')
set.toArray().sort()
['a', 'b', 'c']
length : number
A count of the items in a Set can be found at it's length property.
isEmpty : boolean
Observable property for isEmpty()
toArray : Array
Observable property for toArray(). Whenever items are added or removed on the set, the toArray property will change. This is the mechanism by which Batman's view bindings get notified of collection updates.
observers on the toArray property fire when the set changes
set = new Batman.Set('a', 'b', 'c')
set.observe('toArray', function(newArray) {
return log(newArray.sort());
})
set.add('d')
['d']
logged.last
['a', 'b', 'c', 'd']
set.remove('b')
['b']
logged.last
['a', 'c', 'd']
has(item) : Boolean
has returns a boolean describing if the given item is a member of the set.
Note: Using has(item) in an accessor body will register the set has is called upon as a source of the property being calculated. This so that whenever the set changes, the property will be recalculated, because the set may now have or not have the item in question.
Set::has indicates if an item is a member of the set or not.
set = new Batman.Set('a', 'b', 'c')
set.has('a')
true
set.has('d')
false
Set::has registers the set as a source of an accessor
(Team = (function(_super) {
__extends(Team, _super);
function Team() {
this.awards = new Batman.Set();
}
Team.accessor('bestEver?', function() {
return this.get('awards').has('Stanley Cup');
});
return Team;
})(Batman.Object))
team = new Team
team.observeAndFire('bestEver?', function(status) {
return log(status);
})
team.get('awards').add('Eastern Conference Champs')
logged.last
false
team.get('awards').add('Stanley Cup')
logged.last
true
add(items...)
add adds 0 or more new items to the set. add returns an array of the items which have been newly added to the set, which is to say the intersection of the argument items and the set itself before addition.
add fires the itemsWereAdded event with the list of items newly added to the set if that list has length greater than 0. This is to say the event will not be fired if the items passed to add were all already members of the set.
Set::add adds an item to the set
set = new Batman.Set()
set.has('a')
false
set.add('a')
['a']
set.has('a')
true
Set::add returns only the new items that weren\'t previously in the set
set = new Batman.Set('a', 'b')
set.add('b', 'c', 'd').sort()
['c', 'd']
set.toArray().sort()
['a', 'b', 'c', 'd']
Set::add fires the itemsWereAdded event with the items newly added to the set
set = new Batman.Set('a', 'b')
set.on('itemsWereAdded', function() {
var items;
items = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return log(items);
})
set.add('b', 'c', 'd')
logged.last.sort()
['c', 'd']
Set::add does not fire the itemsWereAdded event if the added items were already in the set.
set = new Batman.Set('a', 'b')
set.on('itemsWereAdded', function() {
var items;
items = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return log(items);
})
set.add('a', 'b')
typeof logged.last
'undefined'
remove(items...)
remove removes 0 or more items from the set. remove returns an array of the items which were successfully removed from the set, which is to say the intersection of the argument items and the set itself before removal.
remove fires the itemsWereRemoved event with the list of removed items if that list has length greater than 0. This is to say the event will not be fired if none of the passed items were members of the set.
Set::remove removes an item from the set
set = new Batman.Set('a')
set.has('a')
true
set.remove('a')
['a']
set.has('a')
false
Set::remove returns only the new items that were previously in the set
set = new Batman.Set('a', 'b')
set.remove('b', 'c', 'd').sort()
['b']
set.toArray()
['a']
Set::remove fires the itemsWereRemoved event with the items removed to the set
set = new Batman.Set('a', 'b', 'c')
set.on('itemsWereRemoved', function() {
var items;
items = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return log(items);
})
set.remove('b', 'c')
logged.last.sort()
['b', 'c']
Set::remove does not fire the itemsWereRemoved event if the removed items were not already members of the set.
set = new Batman.Set('a', 'b')
set.on('itemsWereRemoved', function() {
var items;
items = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return log(items);
})
set.remove('c', 'd')
typeof logged.last
'undefined'
find(testFunction : function) : [Object]
find returns the first item within the set for which the testFunction called with the item returns true, or undefined if no item passes the test.
Note: find returns the first item the test passes for, but since set iteration has no specified order, no guarantee can be made about which item will be returned if more than one item in the set passes the test. However, set iteration order is stable, so find called on the same set twice should return the same item.
Set::find returns the first item for which the test function passes
set = new Batman.Set(1, 2, 3)
set.find(function(x) {
return x % 2 === 0;
})
2
Set::find returns undefined if no items pass the test function
set = new Batman.Set(1, 2, 3)
typeof set.find(function(x) {
return x > 5;
})
'undefined'
forEach(iteratorFunction : function[, context: Object])
forEach calls the iteratorFunction with each item in the set, optionally executing the iteratorFunction in the passed context. Returns undefined.
Note: Set iteration order is not defined, which is to say Set client code cannot rely on one item being iterated over before another, regardless of when the Set's items were added. If you need an ordered set, Batman provides SetSort for exactly this, while including the added benefit of observability. If you need a one time ordering of a set, you can get the array representation with toArray and then use vanilla JavaScript sort on that array.
Note: Using forEach() in an accessor body will register the set iterated over as a source of the property being calculated. This so that whenever the set changes, the property will be recalculated. This can be come an issue if you iterate over a set and modify the set's items, as when the property recalculates that modification will potentially happen on item's it's happened on previously.
Set::forEach iterates over each item in the set
sum = 0
set = new Batman.Set(1, 2, 3)
set.forEach(function(x) {
return sum += x;
})
sum
6
Set::forEach iterates over each item in the set optionally in the provided context
context = {
sum: 0
}
set = new Batman.Set(1, 2, 3)
set.forEach(function(x) {
return this.sum += x;
}, context)
context.sum
6
Set::forEach registers the set as a source if called in an accessor body
(Team = (function(_super) {
__extends(Team, _super);
function Team() {
this.players = new Batman.Set();
}
Team.accessor('willWinTheCup?', function() {
var sedinCount;
sedinCount = 0;
this.players.forEach(function(player) {
if (player.split(' ')[1] === 'Sedin') {
return sedinCount++;
}
});
return sedinCount >= 2;
});
return Team;
})(Batman.Object))
team = new Team()
team.observeAndFire('willWinTheCup?', function(status) {
return log(status);
})
team.get('willWinTheCup?')
false
team.get('players').add('Henrik Sedin')
logged.last
false
team.get('players').add('Daniel Sedin')
logged.last
true
isEmpty() : boolean
isEmpty returns a boolean: true if the set has no items, and false if it has any items.
Note: Using isEmpty() in an accessor body will register the set isEmpty is called on as a source of the property being calculated, so that whenever the set changes the property will be recalculated.
Set::isEmpty returns true if the set has no items
set = new Batman.Set()
set.isEmpty()
true
set.add('a')
set.isEmpty()
false
Set::isEmpty registers the set as a source of an accessor
(Team = (function(_super) {
__extends(Team, _super);
function Team() {
this.games = new Batman.Set();
}
Team.accessor('seasonStarted?', function() {
return !this.games.isEmpty();
});
return Team;
})(Batman.Object))
team = new Team
team.get('seasonStarted?')
false
team.get('games').add({
win: true
})
team.get('seasonStarted?')
true
clear() : Array
clear removes all items from a set. Returns an array of all the items in the set.
clear will fire the itemsWereRemoved event once with all the items in the set.
Note: Set iteration order is not defined, so the order of the array of items returned by clear is undefined.
Set::clear empties the set
set = new Batman.Set('a', 'b', 'c')
set.isEmpty()
false
set.clear().sort()
['a', 'b', 'c']
set.isEmpty()
true
Set::clear fires the itemsWereRemoved event with all the items in the set
set = new Batman.Set('a', 'b', 'c')
set.on('itemsWereRemoved', function() {
var items;
items = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return log(items);
})
set.clear()
logged.last.sort()
['a', 'b', 'c']
replace(collection : Enumerable) : Array
replace removes all the items in a set and then adds all the items found in another collection. The other collection must have a toArray function which returns an array representation of the collection. Returns the array of items added.
replace will fire the itemsWereRemoved event once with all the items in the set, and then the itemsWereAdded event once with the items from the incoming collection.
Set::replace empties the set and then adds items from a different collection
set = new Batman.Set('a', 'b', 'c')
secondSet = new Batman.Set('d', 'e', 'f')
set.replace(secondSet).sort()
['d', 'e', 'f']
set.toArray().sort()
['d', 'e', 'f']
Set::replace fires the itemsWereRemoved event with all the items in the set
set = new Batman.Set('a', 'b', 'c')
set.on('itemsWereRemoved', function() {
var items;
items = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return log(items);
})
set.replace(new Batman.SimpleSet())
logged.last.sort()
['a', 'b', 'c']
Set::replace fires the itemsWereAdded event with all the items in the incoming set
set = new Batman.Set()
set.on('itemsWereAdded', function() {
var items;
items = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return log(items);
})
set.replace(new Batman.SimpleSet('a', 'b', 'c'))
logged.last.sort()
['a', 'b', 'c']
toArray() : Array
toArray returns an array representation of the set.
Note: Set iteration order is not defined, so the order in which the set's items appear in the array is not defined. It is however stable, so the order of the items in two successive toArray calls where the set was not modified in between should be the same.
Note: toArray is also an observable property.
Note: Using toArray() in an accessor body will register the set toArray is called on as a source of the property being calculated, so that whenever the set changes the property will be recalculated.
Set::toArray returns an array representation of the set
set = new Batman.Set()
set.toArray()
[]
set.add('a', 'b', 'c')
set.toArray().sort()
['a', 'b', 'c']
merge(collections... : Enumerable) : Set
merge adds all the items in a set and all the items in the passed collections to a new set and returns it. A collection is an object which has a forEach function. It is a non-destructive collection union. The set merge is called on and each collection passed to merge remain unaffected.
Note: Be careful about using merge within accessors. Calling merge in an accessor function body will register the set merge is called upon as a source of the property being calculated, which means each the set changes, the property will be recalculated. This means the merge will occur again each time, and return an entirely new Set instance. If the previously returned Set instance is retained after recalculation, this is a big memory leak. Instead of merging in accessors, try to use a SetUnion or a SetIntersection.
Set::merge returns a new set with the items of the original set and the passed set
abc = new Batman.Set('a', 'b', 'c')
def = new Batman.Set('d', 'e', 'f')
Batman.typeOf(set = abc.merge(def))
'Object'
set.toArray().sort()
['a', 'b', 'c', 'd', 'e', 'f']
indexedBy(key : String) : SetIndex
indexedByUnique(key : String) : UniqueSetIndex
sortedBy(key: String) : SetSort
Batman.App
Batman.App is the central object in any Batman application. It manages the routing table and the current URL parameters, as well as the initial start of the application. It should also be used as a namespace for models and views so that other objects can find them. Batman assumes that there will only ever be one Batman.App in use at once.
Batman.currentApp
A Batman-wide reference to the currently running Batman.App.
@run() : App
App.run is the central entry point for a Batman application. @run takes these steps to initialize a Batman application:
- Instantiate a
Dispatcher, an internal object for mananging action dispatch to controllers. - Instantiate a
Navigator, an internal object for managing the URL via pushState or hashbangs. - Instantiate the
layoutview according to thelayoutproperty on theApp. - Wait for the layout view to fire it's
readyevent. - Start the first action dispatch by telling the
Navigatorto begin monitoring the URL.
Note: @run emits the run class event on the App, but not necessarily as soon as @run is called. Because the layout View renders asynchronously and may need to fetch other components, the run event can and often does fire long after @run is called. If you need to execute code as soon as the App has started running, add a listener to the run event on the App class. If you need to execute code as soon as the layout has rendered, you can use the ready event on the App class.
@run can be called before or on the load DOMEvent of the window. @run will return the App if the commencement was successful and complete, or false if the App must wait for the layout to render or if the App has already run.
starting an application with DOMEvents
window.addEventListener 'load', ->
MyApp.run()
starting an application with jQuery
$ ->
MyApp.run()
@stop() : App
@stop stops the App it's called upon. The URL will stop being monitored and no more actions will be dispatched. In usual Batman development you shouldn't have to call this.
@routes
@routes is a class level property referencing the root level NamedRouteQuery which allows for binding to routes on objects. See data-route bindings for more information.
@controllers
@controllers is a class level property containing the singleton Batman.Controller instances for each subclass in the application. This controllers directory puts these instances at the lowercase name of the controller. For example, the TodosController would be found at controllers.todos. @controllers ideally should never be bound to, but it is very useful for debugging in the console and other such workarounds.
App.controllers references a directory of controller instances
Alfred = (function(_super) {
__extends(Alfred, _super);
function Alfred() {
return Alfred.__super__.constructor.apply(this, arguments);
}
return Alfred;
})(Batman.App)
Alfred.TodosController = (function(_super) {
__extends(TodosController, _super);
function TodosController() {
return TodosController.__super__.constructor.apply(this, arguments);
}
return TodosController;
})(Batman.Controller)
controller = Alfred.get('controllers.todos')
Batman._functionName(controller.constructor)
"TodosController"
@layout
@currentParams
@paramsManager
@paramsPusher
'run' class event
The run class event is fired once the app has run. This indeterminately but often happens before the app's layout has finished rendering.
'ready' class event
The ready class event is fired once the app's layout is rendered.
Batman.App Routing
The Batman routing DSL is similar to Rails 3's routing DSL.
@resources
@member
@collection
@route
@root
Batman.Controller
Batman.Controller is the base class from which all an application's controllers should descend from. Batman.Controllers are responsible for executing actions which fire off requests for model data, render views, or redirect to other actions in response to navigation state changes.
Controller Directory
Batman.Controllers are singletons which means each controller in your application is instantiated exactly once. The instance of each controller is available on the class at the @sharedContoller property, or within a ControllerDirectory on the Application class. See Batman.Application.controllers.
Actions
Each Batman.Controller should have a number of instance level functions which can fetch the data needed and often render a view (or views) to display that data. These functions can be declared in typical CoffeeScript fashion like so:
class Alfred.TodosController
index: (params) ->
show: (params) ->
Each action function receives the parameters from the dispatcher which are pulled out of the navigated-to URL. This includes both named route parameters (/:foo style) as well as arbitrary query paramters (?foo=bar style).
@beforeFilter(options : [String|Object], filter : [String|Function])
@beforeFilter allows the declaration of a function to be executed before the body of an action during action execution. @beforeFilter accepts either a string or options object representing which action(s) to execute the filter before, and then a string naming a function or function proper to execute.
@beforeFilter allows declaration of filters to execute before an action
TestController = (function(_super) {
__extends(TestController, _super);
function TestController() {
return TestController.__super__.constructor.apply(this, arguments);
}
TestController.beforeFilter('index', function() {
return log('before!');
});
TestController.prototype.index = function() {
log('action!');
return this.render(false);
};
return TestController;
})(Batman.Controller)
controller = TestController.get('sharedController')
controller.dispatch('index')
logged[0]
'before!'
logged[1]
'action!'
@beforeFilter allows declaration of named filters to execute before an action
TodoController = (function(_super) {
__extends(TodoController, _super);
function TodoController() {
return TodoController.__super__.constructor.apply(this, arguments);
}
TodoController.beforeFilter('show', 'fetchTodo');
TodoController.prototype.fetchTodo = function() {
return this.set('todo', {
isDone: true
});
};
TodoController.prototype.show = function() {
return this.render(false);
};
return TodoController;
})(Batman.Controller)
controller = TodoController.get('sharedController')
controller.dispatch('show')
controller.get('todo')
{
isDone: true
}
@beforeFilter allows whitelisting or blacklisting filters to execute before an action
TodoController = (function(_super) {
__extends(TodoController, _super);
function TodoController() {
return TodoController.__super__.constructor.apply(this, arguments);
}
TodoController.beforeFilter({
only: ['show', 'edit']
}, 'fetchTodo');
TodoController.beforeFilter({
except: ['index']
}, 'prepareNewTodo');
TodoController.prototype.fetchTodo = function() {
return this.set('todo', {
isDone: true
});
};
TodoController.prototype.prepareNewTodo = function() {
return this.set('newTodo', {
isDone: false
});
};
TodoController.prototype.index = function() {
return this.render(false);
};
TodoController.prototype.show = function() {
return this.render(false);
};
return TodoController;
})(Batman.Controller)
controller = TodoController.get('sharedController')
controller.dispatch('show')
controller.get('todo')
{
isDone: true
}
controller.get('newTodo')
{
isDone: false
}
@afterFilter
Batman.Model
Batman.Model is responsible for representing data in your application, and for providing a fluid interface to moving in to and out of your backend.
Note: This documentation uses the term model to refer to the class level Model or Model subclass, and the term record to refer to one instance of a Model or Model subclass.
The Identity Map
Batman uses an identity map when fetching and storing records to do it's best to only ever represent a backend record with exactly one client side record. This means that if you use Model.find twice to fetch a record with the same ID, you will get back the same (===) instance in each callback. This is useful because it means that any state the instance might be in is available and preserved no matter which piece of code asked for it, and so that bindings to the instance update no matter which piece of code actually updates the model.
Practically, the identity map is an implementation detail on Batman's end and developers shouldn't have to actually interact with it, but the knowledge that you have the "one true instance" is helpful when reasoning about code and bindings.
Subclassing
Models in your applications should be subclasses of Batman.Model, or subclasses of subclasses, and so on. Extending Batman.Model will give your domain class all the functionality. You can also subclass your own subclasses and things like encoders, validations, and storage adapters will be inherited by sub-subclasses.
Storage Adapters
Batman.Model alone only defines the logic surrounding loading and saving, but not the actual mechanism for doing so. This is left to a Batman.StorageAdaper subclass, 4 of which are included with Batman or in extras:
Batman.LocalStoragefor storing data in the browsers'localStorage, if available.Batman.SessionStoragefor storing data in the browser'ssessionStorage, if available.Batman.RestStoragefor using HTTP GET, POST, PUT, and DELETE to store data in a backend.Batman.RailsStoragewhich extendsBatman.RestStoragewith some handy Rails specific functionality like parsing out validation errors.
@primaryKey : string
primaryKey is a class level configuration option to change which key Batman uses as the primary key. Change the option using set, like so:
primary key can be set using @set
Shop = (function(_super) {
__extends(Shop, _super);
function Shop() {
return Shop.__super__.constructor.apply(this, arguments);
}
Shop.set('primaryKey', 'shop_id');
return Shop;
})(Batman.Model)
Shop.get('primaryKey')
'shop_id'
The primaryKey is what Batman uses to compare instances to see if they represent the same domain-level object: if two records have the same value at the key specified by primaryKey, only one will be in the identity map. The key specified by primaryKey is also used by the associations system when determining if a record is related to another record, and by the remote storage adapters to generate URLs for records.
Note_: The default primaryKey is 'id'.
@storageKey : string
storageKey is a class level option which gives the storage adapters something to interpolate into their specific key generation schemes. In the case of LocalStorage or SessionStorage adapters, the storageKey defines what namespace to store this record under in the localStorage or sessionStorage host objects, and with the case of the RestStorage family of adapters, the storageKey assists in URL generation. See the documentation for the storage adapter of your choice for more information.
The default storageKey is null.
@persist(mechanism : StorageAdapter) : StorageAdapter
@persist is how a Model subclass is told to persist itself by means of a StorageAdapter. @persist accepts either a StorageAdapter class or instance and will return either the instantiated class or the instance passed to it for further modification.
models can be told to persist via a storage adapter
Shop = (function(_super) {
__extends(Shop, _super);
function Shop() {
return Shop.__super__.constructor.apply(this, arguments);
}
Shop.persist(TestStorageAdapter);
return Shop;
})(Batman.Model)
record = new Shop
record.hasStorage()
true
@persist returns the instantiated storage adapter
adapter = false
Shop = (function(_super) {
__extends(Shop, _super);
function Shop() {
return Shop.__super__.constructor.apply(this, arguments);
}
adapter = Shop.persist(TestStorageAdapter);
return Shop;
})(Batman.Model)
adapter instanceof Batman.StorageAdapter
true
@persist accepts already instantiated storage adapters
adapter = new Batman.StorageAdapter
adapter.someHandyConfigurationOption = true
Shop = (function(_super) {
__extends(Shop, _super);
function Shop() {
return Shop.__super__.constructor.apply(this, arguments);
}
Shop.persist(adapter);
return Shop;
})(Batman.Model)
record = new Shop
record.hasStorage()
true
@encode(keys...[, encoderObject : [Object|Function]])
@encode specifies a list of keys a model should expect from and send back to a storage adapter, and any transforms to apply to those attributes as they enter and exit the world of Batman in the optional encoderObject.
The encoderObject should have an encode and/or a decode key which point to functions. The functions accept the "raw" data (the Batman land value in the case of encode, and the backend land value in the case of decode), and should return the data suitable for the other side of the link. The functions should have the following signatures:
encoderObject = {
encode: (value, key, builtJSON, record) ->
decode: (value, key, incomingJSON, outgoingObject, record) ->
}
By default these functions are the identity functions. They apply no transformation. The arguments for encode functions are as follows:
valueis the client side value of thekeyon therecordkeyis the key which thevalueis stored under on therecord. This is useful when passing the sameencoderObjectwhich needs to pivot on what key is being encoded to different calls toencode.builtJSONis the object which is modified by each encoder which will eventually be returned bytoJSON. To send the server the encoded value under a different key than thekey, modify this object by putting the value under the desired key, and returnundefined.recordis the record on whichtoJSONhas been called.
For decode functions:
valueis the server side value of thekeywhich will end up on therecord.keyis the key which thevalueis stored under the incoming JSON.incomingJSONis the JSON which is being decoded into therecord. This can be used to create compound key decoders.outgoingObjectis the object which is built up by the decoders and thenmixin'd to the record.recordis the record on whichfromJSONhas been called.
The encode and decode keys can also be false to avoid the default identity function encoder or decoder from being used.
Note: Batman.Model subclasses have no encoders by default, except for one which automatically decodes the primaryKey of the model, which is usually id. To get any data into or out of your model, you must white-list the keys you expect from the server or storage attribute.
@encode accepts a list of keys which are used during decoding
Shop = (function(_super) {
__extends(Shop, _super);
function Shop() {
return Shop.__super__.constructor.apply(this, arguments);
}
Shop.encode('name', 'url', 'email', 'country');
return Shop;
})(Batman.Model)
json = {
name: "Snowdevil",
url: "snowdevil.ca"
}
record = new Shop()
record.fromJSON(json)
record.get('name')
"Snowdevil"
@encode accepts a list of keys which are used during encoding
Shop = (function(_super) {
__extends(Shop, _super);
function Shop() {
return Shop.__super__.constructor.apply(this, arguments);
}
Shop.encode('name', 'url', 'email', 'country');
return Shop;
})(Batman.Model)
record = new Shop({
name: "Snowdevil",
url: "snowdevil.ca"
})
record.toJSON()
{
name: "Snowdevil",
url: "snowdevil.ca"
}
@encode accepts custom encoders
Shop = (function(_super) {
__extends(Shop, _super);
function Shop() {
return Shop.__super__.constructor.apply(this, arguments);
}
Shop.encode('name', {
encode: function(name) {
return name.toUpperCase();
}
});
return Shop;
})(Batman.Model)
record = new Shop({
name: "Snowdevil"
})
record.toJSON()
{
name: "SNOWDEVIL"
}
@encode accepts custom decoders
Shop = (function(_super) {
__extends(Shop, _super);
function Shop() {
return Shop.__super__.constructor.apply(this, arguments);
}
Shop.encode('name', {
decode: function(name) {
return name.replace('_', ' ');
}
});
return Shop;
})(Batman.Model)
record = new Shop()
record.fromJSON({
name: "Snow_devil"
})
record.get('name')
"Snow devil"
@encode can be passed an encoderObject with false to prevent the default encoder or decoder
Shop = (function(_super) {
__extends(Shop, _super);
function Shop() {
return Shop.__super__.constructor.apply(this, arguments);
}
Shop.encode('name', {
encode: false,
decode: function(x) {
return x;
}
});
Shop.encode('url');
return Shop;
})(Batman.Model)
record = new Shop()
record.fromJSON({
name: "Snowdevil",
url: "snowdevil.ca"
})
record.get('name')
'Snowdevil'
record.get('url')
"snowdevil.ca"
record.toJSON()
{
url: "snowdevil.ca"
} // The name key is absent because of encode: false
Some more handy examples:
@encode can be used to turn comma separated values into arrays
Post = (function(_super) {
__extends(Post, _super);
function Post() {
return Post.__super__.constructor.apply(this, arguments);
}
Post.encode('tags', {
decode: function(string) {
return string.split(', ');
},
encode: function(array) {
return array.join(', ');
}
});
return Post;
})(Batman.Model)
record = new Post()
record.fromJSON({
tags: 'new, hot, cool'
})
record.get('tags')
['new', 'hot', 'cool']
record.toJSON()
{
tags: 'new, hot, cool'
}
@encode can be used to turn arrays into sets
Post = (function(_super) {
__extends(Post, _super);
function Post() {
return Post.__super__.constructor.apply(this, arguments);
}
Post.encode('tags', {
decode: function(array) {
return (function(func, args, ctor) {
ctor.prototype = func.prototype;
var child = new ctor, result = func.apply(child, args), t = typeof result;
return t == "object" || t == "function" ? result || child : child;
})(Batman.Set, array, function(){});
},
encode: function(set) {
return set.toArray();
}
});
return Post;
})(Batman.Model)
record = new Post()
record.fromJSON({
tags: ['new', 'hot', 'cool']
})
record.get('tags') instanceof Batman.Set
true
record.toJSON()
{
tags: ['new', 'hot', 'cool']
}
@validate(keys...[, options : [Object|Function]])
Validations allow a model to be marked as valid or invalid based on a set of programmatic rules. By validating a model's data before it gets to the server we can provide immediate feedback to the user about what they have entered and forgo waiting on a round trip to the server. validate allows the attachment of validations to the model on particular keys, where the validation is either a built in one (invoked by use of options to pass to them) or a custom one (invoked by use of a custom function as the second argument).
Note: Validation in Batman is always asynchronous, despite the fact that none of the validations may use an asynchronous operation to check for validity. This is so that the API is consistent regardless of the validations used.
Built in validators are attached by calling @validate with options designating how to calculate the validity of the key:
@validate accepts options to check for validity
Post = (function(_super) {
__extends(Post, _super);
function Post() {
return Post.__super__.constructor.apply(this, arguments);
}
Post.validate('title', 'body', {
presence: true
});
return Post;
})(Batman.Model)
The built in validation options are listed below:
presence : boolean: Assert that the string value is existant (not undefined nor null) and has length greather than 0.numeric : true: Assert that the value can be is or can be coerced into a number usingparseFloat.minLength : number: Assert that the value'slengthproperty is greater than the given number.maxLength : number: Assert that the value'slengthproperty is less than the given number.length : number: Assert that the value'slengthproperty is exactly the given number.lengthWithin : [number, number]orlengthIn : [number, number]: Assert that the value'slengthproperty is within the ranger specified by the given array of two numbers, where the first number is the lower bound and the second number is the upper bound.
Custom validators should have the signature (errors, record, key, callback). The arguments are as follows:
errors: anErrorsSetinstance which expects to haveaddcalled on it to add errors to the modelrecord: the record being validatedkey: the key to which the validation has been attachedcallback: a function to call once validation has been complete. Calling this function is ++mandatory++.
See Model::validate for information on how to get a particular record's validity.
@loaded : Set
The loaded set is available on every model class and holds every model instance seen by the system in order to function as an identity map. Successful loading or saving individual records or batches of records will result in those records being added to the loaded set. Destroying instances will remove records from the identity set.
the loaded set stores all records seen
Post = (function(_super) {
__extends(Post, _super);
function Post() {
return Post.__super__.constructor.apply(this, arguments);
}
Post.persist(TestStorageAdapter);
Post.encode('name');
return Post;
})(Batman.Model)
Post.get('loaded') instanceof Batman.Set
true
Post.get('loaded.length')
0
post = new Post()
post.save()
Post.get('loaded.length')
1
the loaded adds new records caused by loads and removes records caused by destroys
Post = (function(_super) {
__extends(Post, _super);
function Post() {
return Post.__super__.constructor.apply(this, arguments);
}
Post.encode('name');
return Post;
})(Batman.Model)
adapter = new TestStorageAdapter(Post)
adapter.storage = {
'posts1': {
name: "One",
id: 1
},
'posts2': {
name: "Two",
id: 2
}
}
Post.persist(adapter)
Post.load()
Post.get('loaded.length')
2
post = false
Post.find(1, function(err, result) {
return post = result;
})
post.destroy()
Post.get('loaded.length')
1
@all : Set
The all set is an alias to the loaded set with an added implicit load on the model. Model.get('all') will synchronously return the loaded set and trigger an implicit asynchronous call of Model.load() without options to load a batch of records, which will eventually populate the loaded set with the records returned by the server. Note that all is relative only to the client: it completely depends on the storage adapter in use and any backends which they may contact to determine what comes back during a Model.load.
all is useful for listing every instance of a model in a view, and since the loaded set will change when the load returns, it can be safely bound to.
the all set asynchronously fetches records when gotten
Post = (function(_super) {
__extends(Post, _super);
function Post() {
return Post.__super__.constructor.apply(this, arguments);
}
Post.encode('name');
return Post;
})(Batman.Model)
adapter = new AsyncTestStorageAdapter(Post)
adapter.storage = {
'posts1': {
name: "One",
id: 1
},
'posts2': {
name: "Two",
id: 2
}
}
Post.persist(adapter)
Post.get('all.length')
0 // The synchronously returned set is empty
...
Post.get('all.length')
2 // After the async load the set is populated
@clear() : Set
Model.clear() empties the identity map. This is useful for tests and other unnatural situations where records new to the system are guaranteed to be as such.
@find(id, callback : Function) : Model
Model.find() asks a model to call a given callback with the record with the given id.
@load(options = {}, callback)
@load calls back with an error and some models
Post = (function(_super) {
__extends(Post, _super);
function Post() {
return Post.__super__.constructor.apply(this, arguments);
}
Post.persist(TestStorageAdapter);
return Post;
})(Batman.Model)
posts = false
Post.load(function(err, result) {
return posts = result;
})
...
posts.length
0
@create(attributes = {}, callback) : Model
@findOrCreate(attributes = {}, callback) : Model
id : value
dirtyKeys : Set
errors : ErrorsSet
constructor(idOrAttributes = {}) : Model
isNew() : boolean
updateAttributes(attributes) : Model
toString() : string
toJSON() : Object
fromJSON() : Model
toParam() : value
state() : string
hasStorage() : boolean
load(options = {}, callback)
save(options = {}, callback)
destroy(options = {}, callback)
validate(callback)
Batman.ValidationError
Batman.ErrorsSet
Batman.View
Batman View Bindings
Batman's view bindings are how data gets show and collected from the user. They center on the notion of "bindings": that the view representation and the JavaScript land value are always guaranteed to be in sync, such that when one changes, the other will reflect it.
How to use bindings
Bindings are declared as attributes on nodes under the data namespace. In practice, it looks like this:
<div data-bind="name"></div>
This instantiates a binding on the div node which will update the node's innerHTML with the value found at the name keypath in the current context. Whenever the name key changes, the div's innerHTML will update to the new value.
Nodes can have multiple bindings like this:
<p data-bind="body" data-showif="isPublished"></p>
or attribute bindings like this:
<p data-bind-id="currentID"></p>
and bindings can be on inputs which the user can change, like this:
<input type="text" data-bind="title"></input>
When the title property changes on the JavaScript object this input is bound to, the input's value will be updated. When the user types into the input (and change or keyup events are triggered), the title property in JavaScript land will be updated.
Binding Keypaths
A +keypath+ is the value of the HTML attribute a binding references. Importantly, keypaths can have multiple segments:
<p data-bind="order.customer.name"></p>
The keypath in the above HTML is order.customer.name. When you create a binding to a keypath like this (with dot separating segments), the binding will update the HTML value when +any+ of those segments change. In the above example, this means the p tag's innerHTML will be updated when:
- the order changes,
- the order's customer changes,
- or the order's customer's name changes.
This is important because it means you can rely on a binding to "just work" when anything it depends on changes. If say you had a <select> on the page which changed the order's customer property, bindings which bind to order.customer.name will update each time you change that select to reflect the new customer's name.
Binding Contexts
All bindings render in a context. Binding contexts, known internally to Batman as RenderContexts, are objects which emulate the notion of variable scope in JavaScript code. When a controller action renders, it passes a context to the view consisting of itself, the App, and an object with a window key pointing to the host window object.
Keypath Filters
Bindings can bind to filtered keypaths:
<p data-bind="post.body | truncate 100"></p>
The above <p> will have 100 characters worth of the post's body. Whenever the post's body changes, it will be retruncated and the <p>'s innerHTML will be updated.
Filter chains can be arbitrarily long:
<span data-bind="knight.title | prepend 'Sir' | append ', the honourable'."></span>
and filter chains can use other keypaths as arguments to the filters:
<span data-bind="person.name | prepend ' ' | prepend person.title"></span>
The above <span>'s innerHTML will be updated whenever the person's name, or the person's title changes.
Two Way Bindings and Filters
Note that filtered keypaths can not propagate DOM land changes because values can't always be "unfiltered". For example, if we bind an input to the truncated version of a string:
<input data-bind="post.body | truncate 100"></input>
The <input>'s value can be updated when the post.body property changes but if a user types into this input field, they will edit the truncated body. If Batman updated the post.body property with the contents of the input, all characters which had been truncated will be lost to the nether. To avoid this loss of information and inconsistency, bindings to filtered keypaths will +only update from JavaScript land to HTML+, and never vice versa.
Keypath Literals
Keypaths also support a select few literals within them. Numbers, strings, and booleans can be passed as arguments to filters or used as the actual value of the keypath.
The following are all valid albeit contrived bindings:
<!-- String literal used as an argument -->
<p data-bind="body | append ' ... '"></p>
<!-- Boolean literal used as an argument -->
<p data-showif="shouldShow | default true"></p>
<!-- Number literal used as an argument -->
<p data-bind="body | truncate 100"></p>
<!-- String literal used as the value -->
<p data-bind="'Hardcoded'"></p>
<!-- Boolean literal used as the value -->
<p data-showif="true"></p>
data-bind
data-bind creates a two way binding between a property on a Batman.Object and an HTML element. Bindings created via data-bind will update the HTML element with the value of the JS land property as soon as they are created and each time the property changes after, and if the HTML element can be observed for changes, it will update the JS land property with the value from the HTML.
data-bind will change its behaviour depending on what kind of tag it is attached to:
<input type="checkbox">: the binding will edit thecheckedproperty of the checkbox and populate the keypath with a boolean.<input type="text">and similar,<textarea>: the binding will edit thevalueproperty of the input and populate the keypath with the string found atvalue.<input type="file">: the binding will +not+ edit thevalueproperty of the input, but it will update the keypath with a hostFileobject or objects if the node has themultipleattribute.<select>: the binding will edit theselectedproperty of each<option>tag within the<select>match the property at the keypath. If the<select>has the multiple attribute, the value at the keypath can be an array of selected<option>values. You can also usedata-bind-selectedbindings on the individual options to toggle option selectedness.- All other tags: the binding will edit the
innerHTMLproperty of the tag and will not populate the keypath.
data-bind can also be used to bind an attribute of a node to a JavaScript property. Since attributes can't be observed for changes, this is a one way binding which will never update the JavaScript land property. Specify which attribute to bind using the "double dash" syntax like so: data-bind-attribute="some.keypath". For example, to bind the placeholder attribute of an input, use data-bind-placeholder.
<input type="text" data-bind-placeholder="'Specify a subtitle for product ' | append product.name">
Note: data-bind will not update a JavaScript property if filters are used in the keypath.
data-source
data-source creates a one way binding which propagates only changes from JavaScript land to the DOM, and never vice versa. data-source has the same semantics with regards to how it operates on different tags as data-bind, but it will only ever update the DOM and never the JavaScript land property.
For example, the HTML below will never update the title property on the product, even if the user changes it. Each time the title attribute changes from a set in JavaScript land, the value of the input will be updated to the new value of title, erasing any potential changes that have been made to the value of the input by the user.
<input type="text" data-source="product.title">
Note: data-source-attribute is equivalent to data-bind-attribute, since the former is defined as never making JS land changes, and the latter is unable to.
data-target
data-target creates a one way binding which propagates only changes from the DOM to JavaScript land, and never vice versa. data-target has the same semantics with regards to how it operates on different tags as data-bind, but it will never update the DOM even if the JavaScript land value changes.
Note: data-target-attribute is unavailable, because DOM changes to node attributes can't be monitored.
data-showif / data-hideif
data-showif and data-hideif bind to keypaths and show or hide the node they appear on based on the truthiness of the result. data-showif will show a node if the given keypath evaluates to something truthy, and data-hideif will leave a node visible until it's given keypath becomes truthy, at which point the node will be hidden. data-showif and data-hideif show and hide nodes by adding display: none !important; to the node's style attribute.
For example, the if the HTML below were rendered where the keypath product.published evaluated to true, the <button> will be visible.
<button data-showif="product.published">Unpublish Product</button>
This is the Batman equivalent of a templating language's if construct, and where else branches are implemented using the opposite binding.
<button data-showif="product.published">Unpublish Product</button>
<button data-hideif="product.published">Publish Product</button>
data-addclass / data-removeclass
data-addclass and data-removeclass bindings can be used to conditionally add or remove a class from a node based on a boolean keypath. Specify the class to add using the "double dash" syntax; for example,data-addclass-big="some.keypath" on a node will add the "big" class to that node's classes if some.keypath is truthy. data-removeclass will remove a class (usually one which is present in the HTML) if the keypath passed to it is truthy.
The outer span in the HTML below will have an "error" class when the product.errors.length keypath evaluates to anything other than 0, since 0 is falsy and other numbers are truthy.
<span data-addclass-error="product.errors.length">This product has <span data-bind="product.errors.length"></span> errors.</button>
data-foreach
data-foreach is used to loop over an iterable object in Batman views. data-foreach duplicates the node it occurs on for each item in the collection found at the keypath given to it, and renders each duplicated node with that node's object from the collection by putting it in the context under a name passed to it using the "double dash" syntax.
The <option> node below will be duplicated for each item in the Set at the products keypath.
<select>
<option data-foreach-product="products" data-bind="product.name"></option>
</select>
Batman will execute the data-foreach binding before the data-bind on the <option> node, which means that the data-bind will be processed for each duplicated node with each separate Product in the products Set in scope for each separate node. If there were say 3 Products in the products set, the HTML would look similar to this once rendered:
<select>
<option data-bind="product.name">Product A</option>
<option data-bind="product.name">Product B</option>
<option data-bind="product.name">Product C</option>
<!-- end products -->
</select>
data-foreach can be used to iterate over Batman.Sets, and most often should be, because it observes any Sets and will update the DOM with new nodes if items are added to the set, or remove nodes from the DOM if their corresponding nodes are removed from the set. data-foreach is, like each other binding, keypath aware, such that if the Set instance at the keypath changes, or any previous segment of the keypath changes, data-foreach will remove all the nodes currently in the DOM, and add new nodes for each new item in the incoming Set.
Note: data-foreach expects to find an iterable object at the keypath given to it, and will emit a warning if it finds undefined.
data-formfor
data-formfor creates a special addition to the context stack to represent a object under edit within a form. Usually this object is a model. Using the double dash syntax the name for the model to reside under can be specified.
Automatic Validation Display
data-formfor also has some handy functionality for displaying the result of validating the object under edit in the form. This will only be enabled if the object has an errors Set, which Batman.Models certainly do.
If a tag matching the relative selector .errors is found, will populate this element with a list of the errors found during validation on the object. The selector for the errors container can be changed by adding a data-errors-list attribute with the value of the selector to the form with the data-formfor binding on it, or editing Batman.DOM.FormBinding::defaultErrorsListSelector.
If value bindings are made using data-bind to attributes on the model within the form, automatic data-addclass-error bindings will be added to the elements on which the data-bind occurs to add the "error" class when the model has errors on the attribute which data-bind binds to.
In the HTML below, an automatic data-addclass-error will be added to the <input> which activates when the product model has validation errors on the name attribute.
<form data-formfor-product="currentProduct">
<input type="text" data-bind="product.name"></input>
</form>
The class which gets automatically added to inputs binding to invalid attributes can be customized by editing Batman.DOM.FormBinding::errorClass.
data-context
data-context bindings add the object found at the key to the context stack, optionally under a key using the double dash syntax.
For example, if a product object exists in the current context, the data-context binding below will expose its attributes at the root level of the context:
<div data-context="product">
<span data-bind="name"></span>
<span data-bind="cost"></span>
</div>
Contexts added to the stack can also be scoped under a key using data-context-:
<div data-context-currentProduct="product">
<span data-bind="currentProduct"></span>
<span data-bind="currentProduct"></span>
</div>
This is a useful mechanism for passing local variables to partial views.
data-event
data-event bindings add DOM event listeners to the nodes they exist on which call the function found at the passed keypath. data-event bindings use the double dash syntax to specify the name of the event to listen for.
In the HTML below, if the keypath controller.nextAction resolves to a function, that function will be executed each time the <button> element is clicked.
<button data-event-click="controller.nextAction"></button>
Functions which data-event calls will be passed the node and the DOMEvent object: (node, event) ->.
data-event supports the following types of events formally and should "do the right thing" when attached to elements which fire these events:
- click
- doubleclick
- change
- submit
If the event name used doesn't match the above events, the event name used will just fall through and be passed to window.addEventListener.
data-route
data-route bindings are used to dispatch a new controller action upon the clicking of the node they bind to. data-route expects to find either a string or a NamedRouteQuery at the keypath passed to it. With this route, it will add an event handler to the click action of the element which dispatches the route and prevents the default action of the DOMEvent. data-route will also populate the href attribute if it occurs on an <a> tag so that other functons like "Copy Link Address" and Alt+Click continue to work on the link.
The first way to use data-route are by passing it a string, which can be built using filters or an accessor, but the preferred way is to use the NamedRouteQuery. These objects are generated for you by starting keypaths at the App.routes property. All Batman.Apps have a routes property which holds a nested list of all the routes, which you descend into by passing various key segments and objects. Since the App object is present in the default context stack, data-route keypaths can just start with routes like so.
For example, assume the following routes definition in the current Batman.App:
class Alfred extends Batman.App
@resources 'todos'
This means that routes like /todos and /todos/:id exist. To route to the collection action, use the plural name of the resource:
<a data-route="routes.todos"></a>
To route to an individual todo things get a bit more complicated. If we have a Todo model with ID# 42 in the context as todo, use the get filter shorthand on the NamedRouteQuery returned by routes.todos to generate a member route:
<a data-route="routes.todos[todo]"></a>
Underneath this is doing Alfred.get('routes.todos').get(todo); that is to say the todo object is being passed as a key to the NamedRouteQuery, which knows how to generate a member route when given a record. The above HTML when rendered will look like this:
<a data-route="routes.todos[todo]" href="/todos/42"></a>
This syntax can be extended to nested routes. If we have nested routes, we can use chained gets to generated nested routes
class Tracker extends Batman.App
@resources 'villians', ->
@resources 'crimes'
Routes for collection and member crimes should look like /villians/:villian_id/crimes and /villians/:villian_id/crimes/:id respectively. Assuming the presence of a villian and a crime in the context, chained gets on NamedRouteQuerys achieve this:
<!-- Collection of crimes for a particular villian -->
<a data-route="routes.villians[villian].crimes"></a>
<!-- One crime of a particular villian -->
<a data-route="routes.villians[villian].crimes[crime]"></a>
Note: data-route bindings route only to internal dispatch, and not external links. Use a regular <a> tag to link away from the application.
data-view
data-view bindings attach custom Batman.View instances or instantiate custom View subclasses to / on a node. data-view expects either a Batman.View instance or subclass at the keypath passed to it. If an instance is passed, it will set the node property of the view to the node the data-view occurs on. If a class is passed, that class will be instantiated with the context the data-view binding executed in and with the node it occurred upon. See Batman.View for more information on custom Views and their uses.
Note: data-view bindings will bind to the passed keypath until it exists, that is to say until the value of it is not undefined. After the View has been set up, the data-view binding will remove itself and stop observing the keypath.
data-partial
data-partial pulls in a partial template and renders it in the current context of the node the data-partial occurs in. data-partial expects the name of the view to render in the value of the HTML attribute. Warning: This value is not a keypath. The HTML attribute's value is interpreted as a string, and the template which resides at that view path will be rendered.
If we have this HTML at views/villains/_stub.html in our app:
<span data-bind="villian.name"></span>
and in views/villians/show.html we have this HTML:
<h1>A villian!</h1>
<div data-partial="villians/_stub"></div>
the contents of the stub partial will be inserted and rendered in the <div> above.
data-mixin
data-defineview
data-renderif
data-yield
data-contentfor
data-replace
Batman View Filters
raw(value) : string
The raw filter renders the value unescaped.
<span data-bind="someHTMLyString | raw"></span>
get(value, key) : value
value[key] : value
Shorthand for the get filter.