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 ES5Object.defineProperty
.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 be sure to useget
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 extendingBatman.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_missing
in Ruby or#doesNotUnderstand
in Smalltalk. - one: create a
keyAccessor
at the given key, which will only be called when that key is gotten, set, or unset. - many: create
keyAccessors
for 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 theget
,set
, andunset
keys defined, or a function. 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 theget
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.test '@accessor can be called on a class to define how a property is calculated', -> class Post extends Batman.Object @accessor 'summary', -> @get('body').slice(0, 10) + "..." post = new Post(body: "Why Batman is Useful: A lengthy post on an important subject") equal 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.test '@accessor can define a transform on several properties', -> class User extends Batman.Object @accessor 'fullName', -> @get('firstName') + " " + @get('lastName') tim = new User(firstName: "Tim", lastName: "Thomas") equal tim.get('fullName'), "Tim Thomas" tim.set('firstName', "Timmy") equal tim.get('fullName'), "Timmy Thomas"
Accessors can define custom
get
,set
, andunset
functions to support each operation on the property:test '@accessor can define the get, set, and unset methods for the property', -> class AbsoluteNumber extends Batman.Object @accessor 'value', get: -> @_value set: (_, value) -> @_value = Math.abs(value) unset: -> delete @_value number = new AbsoluteNumber(value: -10) equal number.get('value'), 10
Importantly, it is also safe to use branching, loops, or whatever logic you want in accessor bodies:
test '@accessor can use arbitrary logic to define the value', -> class Player extends Batman.Object @accessor 'score', -> if @get('played') (@get('goals') * 2) + (@get('assists') * 1) else 0 rick = new Player(played: false, goals: 0, assists: 0) equal rick.get('score'), 0 rick.set('played', true) equal rick.get('score'), 0 rick.set('goals', 3) equal rick.get('score'), 6 rick.set('assists', 1) equal rick.get('score'), 7
Caveats
Accessors are extremely useful, but keep these items in mind when using them:
- Accessors should be pure functions so they are predictable and 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 thecache
option in the accessor descriptor object (the last argument to the@accessor
function).test "@accessor usually caches results", -> counter = 0 class Example extends Batman.Object @accessor 'cachedCounter', -> ++counter @accessor 'notCachedCounter', get: -> ++counter cache: false example = new Example() equal example.get('cachedCounter'), 1 equal example.get('cachedCounter'), 1 equal example.get('cachedCounter'), 1, "The second and third calls do not execute the function" equal example.get('notCachedCounter'), 2 equal example.get('notCachedCounter'), 3, "Passing cache: false does re-execute the function" equal example.get('cachedCounter'), 1
- Accessors must use
get
to access properties they use for computation
Batman tracks an accessor's sources by adding a global hook to all
get
s done, so if you don't useget
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 onBatman.Object
should be accessed usingget
andset
whether or not the code occurs in an accessor body, 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, say by merging a number of
Batman.Set
s 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 and for every call toset
. Instead of recomputing expensive things every time the accessor is called, try to use objects which do smart re-computation using observers. Practically, this translates to using things likenew SetUnion(@get('setA'), @get('setB'))
instead of@get('setA').merge(@get('setB'))
in an accessor body, sinceSetUnion
will observe its constituents and update itself when they change, instead of themerge
resulting in the accessor recomputing every timesetA
orsetB
changed. - zero: create a
-
@classAccessor([keys...], objectOrFunction)
classAccessor
defines an accessor on the class:get
s andset
s done to 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, whereas@classAccessor
defines accessors on the class object itself. See@accessor
for the details surrounding accessors.test '@classAccessor defines an accessor on the class', -> class SingletonDooDad extends Batman.Object @classAccessor 'instance', -> new @() instance = SingletonDooDad.get('instance') # "classAccessor defines accessors for gets done on the class its self" ok SingletonDooDad.get('instance') == instance # "A second get returns the same instance"
-
@mixin(objects...) : prototype
@mixin
is a handy function for mixing inobject
s to a class' prototype.@mixin
is implemented on top of the Batman levelmixin
helper, which means that keys from incomingobjects
will be applied usingset
, and anyinitialize
functions on theobjects
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 atclassMixin
.@mixin
is intended for use during the class definition of aBatman.Object
subclass.test '@mixin extends the prototype of a Batman.Object subclass', -> FishBehaviour = {canBreathUnderwater: true} MammalBehaviour = {canBreathAboveWater: true} class Platypus extends Batman.Object @mixin FishBehaviour, MammalBehaviour platypus = new Platypus ok platypus.get('canBreathAboveWater') ok platypus.get('canBreathUnderwater')
-
@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.test '@classMixin extends the Batman.Object subclass', -> Singleton = initialze: (subject) -> subject.accessor 'instance', -> new subject class Highlander extends Batman.Object @classMixin Singleton instance = Highlander.get('instance') ok instance == Highlander.get('instance'), "There can only be one."
-
@observeAll(key, callback : function) : prototype
@observeAll
extends theBatman.Object
implementation ofBatman.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 aBatman.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.test "@observeAll attaches handlers which get called upon change", -> results = [] class Song extends Batman.Object @observeAll 'length', (newValue, oldValue) -> results.push newValue song = new Song({length: 340, bpm: 120}) equal song.set('length', 200), 200 deepEqual results[1], 200 test "@observeAll can attach handlers after instance instantiation", -> results = [] class Song extends Batman.Object song = new Song({length: 340, bpm: 120}) equal song.set('length', 360), 360 deepEqual results[0], undefined Song.observeAll 'length', (newValue, oldValue) -> results.push newValue equal song.set('length', 200), 200 deepEqual results[0], 200
-
constructor(objects...)
To create a new
Batman.Object
, theBatman.Object
constructor can be used, or, theBatman
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 newBatman.Object
usingget
andset
, 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 propertycopy
from these objects is shallow.test 'Batman() function allows for handy creation of Batman.Objects', -> object = Batman(foo: 'bar') equal typeof object.get, 'function' test 'Batman.Object constructor function accepts multiple mixin arguments and later mixins take precedence.', -> song = Batman({length: 100, bpm: 120}, {bpm: 130}) equal song.get('length'), 100 equal 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 thisBatman.Object
.test 'toJSON returns a vanilla JS object', -> object = Batman(foo: 'bar') deepEqual object.toJSON(), {foo: 'bar'}
-
hashKey() : string
hashKey
returns a unique string identifying this particularBatman.Object
. No twoBatman.Object
s will have the samehashKey
. Feel free to override the implmentation of this function on your objects if you have a better hashing scheme for a domain object of yours. -
batchAccessorChanges(key, wrappedFunction) : string
Prevents accessor from being recalculated while the specified function is called. Only after
wrappedFunction
is complete will the accessor be recomputed. Returns the result ofwrappedFunction
.This can be useful when making multiple changes, and only want a single change event fired after the modifications are in place.
-
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.test 'accessor can be called on an instance of Batman.Object to define an accessor just on that instance', -> class Post extends Batman.Object @accessor 'summary', -> @get('body').slice(0, 10) + "..." post = new Post(body: "Why Batman is Useful: A lengthy post on an important subject") equal post.get('summary'), "Why Batman..." post.accessor('longSummary', -> @get('body').slice(0, 20) + "...") # "Instance level accessor defines accessors just for that instance" equal post.get('longSummary'), "Why Batman is Useful..." test 'defining an accessor on an instance does not affect the other instances', -> class Post extends 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', -> @get('body').slice(0, 20) + "..." equal post.get('longSummary'), "Why Batman is Useful..." equal otherPost.get('longSummary'), undefined
-
mixin(objects...) : this
mixin
extends the object it's called on with the passedobjects
using theBatman.mixin
helper. Returns the object it's called upon.Note: Since the
Batman.mixin
helper is used, mixin functionality like usingset
to apply properties and callinginitialize
functions is included in the instance levelmixin
function.test 'mixin on an instance applies the keys from the mixed in object to the instance', -> class Snake extends Batman.Object snake = new Snake() snake.mixin {canSlither: true}, {canHiss: true} ok snake.get('canSlither') ok snake.get('canHiss')
-
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. AllBatman.Object
s, their subclasses and instances are observable by default. -
isObservable[= true] : boolean
isObservable
will returntrue
when the current object is able to be observed, orfalse
if it is not. -
get(keypath: string) : value
Retrieves the value at a
key
on an object. Accepts keypaths.Note:
get
must be used for property access on any object inBatman
'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 useget
instead of the regular.
for property access.test "get retrieves properties on Batman objects", -> song = new Batman.Object({length: 340, bpm: 120}) equal song.get("length"), 340 equal song.get("bpm"), 120 test "get retrieves properties on nested Batman objects using keypaths", -> post = new Batman.Object text: "Hello World!" author: new Batman.Object name: "Harry" equal post.get("author.name"), "Harry" test "get retrieves properties on Batman objects when . property access doesn't", -> song = new Batman.Model({length: 340, bpm: 120}) equal typeof song.length, "undefined" equal song.get("length"), 340
-
set(keypath: string, newValue) : newValue
Stores the
value
at akey
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 theBatman
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
andset
's return value are not guaranteed to be identical.test "set stores properties on batman objects.", -> song = new Batman.Object({length: 340, bpm: 120}) equal song.get("length"), 340 equal song.set("length", 1000), 1000 equal song.get("length"), 1000 test "set stores properties on nested Batman objects using keypaths", -> author = new Batman.Object name: "Harry" post = new Batman.Object text: "Hello World!" author: author equal post.set("author.name", "Nick"), "Nick" equal author.get("name"), "Nick", "The value was set on the nested object." test "set is incompatible with '.' property mutation", -> song = new Batman.Model({length: 340, bpm: 120}) equal song.get("length"), 340 equal song.length = 1000, 1000 equal song.get("length"), 340, "The song length reported by Batman is unchanged because set wasn't used to change the value."
-
unset(keypath: string) : value
Removes the value at the given
keypath
, leaving itundefined
. Accepts keypaths. Returns the value the property had before unsetting.unset
is roughly equivalent toset(keypath, undefined)
, however, custom properties can define a nonstandardunset
function, so it is best to useunset
instead ofset(keypath, undefined)
wherever possible.test "unset removes the property on Batman objects", -> song = new Batman.Object({length: 340, bpm: 120}) equal song.get("length"), 340 equal song.unset("length"), 340 equal song.get("length"), undefined, "The value is unset." test "unset removes the property at a keypath", -> author = new Batman.Object name: "Harry" post = new Batman.Object text: "Hello World!" author: author equal post.unset("author.name"), "Harry" equal author.get("name"), undefined, "The value was unset on the nested object."
-
getOrSet(keypath: string, valueFunction: Function) : value
Assigns the
keypath
to the result of callingvalueFunction
ifkeypath
is currently falsey. Returns the value of the property after the operation, whether it has changed or not. Equivalent to CoffeeScript's||=
operator.test "getOrSet doesn't set the property if it exists", -> song = new Batman.Object({length: 340, bpm: 120}) equal song.getOrSet("length", -> 500), 340 equal song.get("length"), 340 test "getOrSet sets the property if it is falsey", -> song = new Batman.Object({length: 340, bpm: 120}) equal song.getOrSet("artist", -> "Elvis"), "Elvis" equal song.get("artist"), "Elvis"
-
observe(key: string, observerCallback: Function) : this
Adds a handler to call when the value of the property at the
key
changes uponset
ting. Accepts keypaths.observe
is the very core of Batman's usefulness. As long asset
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 its views.The
observerCallback
gets called with the argumentsnewValue, oldValue
, whenever thekey
changes.Returns the object
observe
was called upon.test "observe attaches handlers which get called upon change", -> result = null song = new Batman.Object({length: 340, bpm: 120}) song.observe "length", (newValue, oldValue) -> result = [newValue, oldValue] equal song.set("length", 200), 200 deepEqual result, [200, 340] equal song.set("length", 300), 300 deepEqual result, [300, 200]
Note:
observe
works excellently on keypaths. If you attach a handler to a "deep" keypath, it will fire when any segment of the keypath changes, passing in the new value at the end of the keypath.test "observe attaches handlers which get called upon change", -> result = null author = new Batman.Object name: "Harry" post = new Batman.Object text: "Hello World!" author: author post.observe "author.name", (newName, oldName) -> result = [newName, oldName] newAuthor = new Batman.Object({name: "James"}) post.set "author", newAuthor deepEqual result, ["James", "Harry"], "The observer fired when the 'author' segment of the keypath changed."
-
observeAndFire(key: string, observerCallback: Function) : this
Adds the
observerCallback
as an observer tokey
, and fires it immediately. Accepts the exact same arguments and follows the same semantics asObservable::observe
, but the observer is fired with the current value of the keypath it observes synchronously during the call toobserveAndFire
.Note: During the initial synchronous firing of the
callback
, thenewValue
andoldValue
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 theset
on the property happens.test "observeAndFire calls the observer upon attaching it with the currentValue of the property", -> result = null song = new Batman.Object({length: 340, bpm: 120}) song.observeAndFire "length", (newValue, oldValue) -> result = [newValue, oldValue] deepEqual result, [340, 340] equal song.set("length", 300), 300 deepEqual result, [300, 340]
-
observeOnce(key: string, observerCallback: Function)
Behaves the same way as
Observable::observe
, except that onceobserverCallback
has been executed for the first time, it will remove itself as an observer tokey
.test "observeOnce only calls observerCallback when key is modified for the first time", -> result = null song = new Batman.Object({length: 340, bpm: 120}) song.observeOnce "length", (newValue, oldValue) -> result = [newValue, oldValue] equal song.set("length", 200), 200 deepEqual result, [200, 340] equal song.set("length", 300), 300 deepEqual result, [200, 340], "The observer was not fired for the second update"
-
forget([key: string[, observerCallback: Function]]) : this
If
observerCallback
andkey
are given, that observer is removed from the observers onkey
. If only akey
is given, all observers on that key are removed. If nokey
is given, all observers on all keys are removed. Accepts keypaths.Returns the object on which
forget
was called.test "forget removes an observer from a key if the key and the observer are given", -> result = null song = new Batman.Object({length: 340, bpm: 120}) observer = (newValue, oldValue) -> result = [newValue, oldValue] song.observe "length", observer equal song.set("length", 200), 200 deepEqual result, [200, 340] song.forget "length", observer equal song.set("length", 300), 300 deepEqual result, [200, 340], "The logged values haven't changed because the observer hasn't fired again." test "forget removes all observers from a key if only the key is given", -> results = [] song = new Batman.Object({length: 340, bpm: 120}) observerA = ((newValue, oldValue) -> results.push [newValue, oldValue]) observerB = ((newValue, oldValue) -> results.push [newValue, oldValue]) song.observe "length", observerA song.observe "length", observerB equal song.set("length", 200), 200 equal results.length, 2, "Both length observers fired." song.forget("length") equal song.set("length", 300), 300 equal results.length, 2, "Nothing more has been added because neither observer fired." test "forget removes all observers from all keys if no key is given", -> results = [] song = new Batman.Object({length: 340, bpm: 120}) observerA = ((newValue, oldValue) -> results.push [newValue, oldValue]) observerB = ((newValue, oldValue) -> results.push [newValue, oldValue]) song.observe "length", observerA song.observe "bpm", observerB equal song.set("length", 200), 200 equal results.length, 1, "The length observer fired." song.forget() equal song.set("length", 300), 300 equal song.set("bpm", 130), 130 equal results.length, 1, "Nothing more has been logged because neither observer fired."
-
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. AllBatman.Object
s, their subclasses and instances areEventEmitter
s by default. -
true(keys... : [string|Array], handler : Function)
Attaches a function
handler
to each event in the providedkeys
collection. This function will be executed when one of the specified events is fired.test 'event handlers are added with `on`', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter ok dynamite.on 'detonate', -> results.push 'detonated' dynamite.fire 'detonate' equal results[0], 'detonated'
-
false(keys... : [string|Array], handler : Function)
Removes the
handler
function from the events specified inkeys
. Ifhandler
is not provided, all handlers will be removed from the specified event keys.test 'event handlers are removed with off', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter handler = -> results.push 'This should not fire' dynamite.on 'detonate', handler dynamite.off 'detonate', handler dynamite.fire 'detonate' deepEqual results, [] test 'If no `handler` is provided, off will remove all handlers from the specified events', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter handler = -> results.push 'This should not fire' anotherHandler = -> results.push 'Neither should this' dynamite.on 'detonate', handler dynamite.on 'detonate', anotherHandler dynamite.off 'detonate' dynamite.fire 'detonate' deepEqual results, []
-
fire(key : string, arguments... : Array)
Calls all previously attached handlers on the event with name
key
. All handlers will receive the passedarguments
.Note: Calling
fire
doesn't guarantee the event will fire since firing can be prevented withprevent
.test 'event handlers are fired', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.on 'detonate', (noise) -> results.push "detonated with noise #{noise}" dynamite.fire 'detonate', "BOOM!" equal results[0], "detonated with noise BOOM!"
-
hasEvent(key : string) : boolean
Asks if the
EventEmitter
has an event with the givenkey
.test 'events can be tested for presence', -> dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.on 'detonate', -> log "detonated" ok dynamite.hasEvent('detonate') equal dynamite.hasEvent('click'), false
-
once(key : string, handler : Function)
Allows the specified handler to be fired only once before it is removed
test 'handlers added using `once` are removed after they are fired', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.once 'detonate', -> results.push 'BOOM!' ok dynamite.hasEvent('detonate') dynamite.fire 'detonate' equal results[0], 'BOOM!' dynamite.fire 'detonate' equal results[1], undefined
-
prevent(key : string) : EventEmitter
Prevents the event with name
key
from firing, even iffire
is called. This is useful if you need to guarantee a precondition has been fulfilled before allowing event handlers to execute. Returns the event emitting object.Undo event prevention with
allow
orallowAndFire
.Note:
prevent
can be called more than once to effectively "nest" preventions.allow
orallowAndFire
must be called the same number of times or more for events to fire once more.test 'events can be prevented', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.prevent('detonate') dynamite.on 'detonate', -> results.push "This shouldn't fire" dynamite.fire('detonate') equal results[0], undefined, "The event handler wasn't fired." test 'prevent returns the event emitter', -> dynamite = Batman.mixin {}, Batman.EventEmitter equal dynamite, dynamite.prevent('detonate')
-
allow(key : string) : EventEmitter
Allows the event with name
key
to fire, afterprevent
had been called.allow
will not fire the event when called, regardless of whether 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
orallowAndFire
must be called the same number of times or more for events to fire once more.test 'events can be allowed after prevention', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.prevent('detonate') dynamite.on 'detonate', -> results.push "This will only fire once" dynamite.fire('detonate') equal results.length, 0, "The event handler wasn't fired." dynamite.allow('detonate') dynamite.fire('detonate') equal results.length, 1, "The event handler was fired." test 'events must be allowed the same number of times they have been prevented', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.prevent('detonate') dynamite.prevent('detonate') dynamite.on 'detonate', -> results.push "This will only fire once" dynamite.fire('detonate') equal results.length, 0, "The event handler wasn't fired, the prevent count is at 2." dynamite.allow('detonate') dynamite.fire('detonate') equal results.length, 0, "The event handler still wasn't fired, but the prevent count is now at 1." dynamite.allow('detonate') dynamite.fire('detonate') equal results.length, 1, "The event handler was fired." test 'allow returns the event emitter', -> dynamite = Batman.mixin {}, Batman.EventEmitter equal dynamite, dynamite.allow('detonate')
-
allowAndFire(key : string)
Allows the event with name
key
to fire once more, and tries to fire it.allowAndFire
may fail to fire the event ifprevent
has been called more times for this event thanallow
orallowAndFire
have.test 'events can be allowed and fired after prevention', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.on 'detonate', -> results.push "This will only fire once" dynamite.prevent('detonate') dynamite.fire('detonate') equal results.length, 0, "The event handler wasn't fired." dynamite.allowAndFire('detonate') equal results.length, 1, "The event handler was fired." test 'events must be allowed and fired the same number of times they have been prevented', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.on 'detonate', -> results.push "This will only fire once" dynamite.prevent('detonate') dynamite.prevent('detonate') dynamite.allowAndFire('detonate') equal results.length, 0, "The event handler wasn't fired." dynamite.allowAndFire('detonate') equal results.length, 1, "The event handler was fired."
-
isPrevented(key : string) : boolean
Asks if the specified event is currently being prevented from firing
test 'isPrevented is true after prevent is called', -> dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.on 'detonate', -> results.push "This will only fire once" dynamite.prevent('detonate') equal dynamite.isPrevented('detonate'), true test 'isPrevented is false if all prevents have been nullified using `allow`', -> dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.on 'detonate', -> results.push "This will only fire once" dynamite.prevent('detonate') equal dynamite.isPrevented('detonate'), true dynamite.allow('detonate') equal dynamite.isPrevented('detonate'), false
-
mutate(wrappedFunction : Function)
Prevents change events from firing while the specified function is called. Only after
wrappedFunction
is complete will thechange
event be fired. Returns the result ofwrappedFunction
.This can be useful when making multiple changes, and only want a single change event fired after the modifications are in place.
test 'mutate fires a single change event, regardless of the logic in wrappedFunction', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.on 'change', -> results.push 'Change event was fired' mutateFunction = -> dynamite.fire('change') dynamite.fire('change') dynamite.mutate(mutateFunction) equal results.length, 1 test 'mutate returns the result of wrappedFunction', -> results = [] dynamite = Batman.mixin {}, Batman.EventEmitter dynamite.on 'change', -> results.push 'Change event was fired' mutateFunction = -> 'BOOM!' mutateResult = dynamite.mutate(mutateFunction) equal mutateResult, 'BOOM!'
-
mutation(wrappedFunction : Function)
A helper method that returns a function that will call
wrappedFunction
and fires the change event when complete (if it is present).Note: the returned function does not block the change event from firing due to the logic in
wrappedFunction
. To ignore/block change events, useprevent('change')
.test 'mutation returns a function that wraps the provided wrappedFunction', -> class Person extends Batman.Model @resourceName: 'person' @encode 'name' @persist TestStorageAdapter, storage: [] transform: @mutation -> @name = 'Batman' results = [] verifyTransformation = -> equal @name, 'Batman' person = Person.findOrCreate({name: 'Bruce Wayne'}) person.on 'change', verifyTransformation person.transform()
-
isEventEmitter
Always true. Useful for testing whether a specific object instance uses the EventEmitter mixin.