Batman.Set is the array-like enumerable of batman.js. It offers observable properties (which are automatically tracked by @accessor) and useful change events.
In batman.js, you need observable data structures everywhere. Batman.Set is the observable, array-like enumerable that the framework uses internally, and you can use it too! Besides Batman.Set, batman.js provides some other classes to help you get things done:
Batman.SetIndex(created withindexedBy) groups a Set’s items by a property valueBatman.UniqueSetIndex(created withindexedByUnique) looks up items by unique valueBatman.SetSort(created withsortedBy) returns a sorted proxy of the Set- Binary set operations create unions, intersections and complements of sets.
Batman.Set
Batman.Set implements the set pattern. It is a collection of distinct objects, meaning that there can be no duplicates (unlike an array). Features of Batman.Set include:
- Enumeration (
Batman.Setmixes inBatman.Enumerable) - Guaranteed unique contents (a
Batman.Setwon’t allow duplicates, even if you calladdtwice.) - Observable
- Sorting and searching, with internal caching
- Extensible with CoffeeScript
extendfor making custom sets
You can create a Batman.Set by passing n items to the constructor:
set = new Batman.Set(1,2,3,4)
set.get('length') # => 4
You can add and remove with the add and remove functions, which also take any number of items:
addedItems = set.add(5, 6)
removedItems = set.remove(1)
set.get('length') # => 5
If you try to add the same (===) item twice, it won’t be added:
addedItems = set.add(5)
set.get('length') # => 5
addedItems # => []
If you try to remove an item that isn’t in the set, nothing will happen:
removedItems = set.remove(100)
set.get('length') # => 5
removedItems # => []
Observing Batman.Set
Calling these functions inside an accessor function will cause the accessor to track the Batman.Set:
atfindmergeforEach(and any otherBatman.Enumablefunction, since they callforEachunder the hood)toArrayisEmptyhas
So will getting these accessors:
firstlastisEmptytoArraylength
For example, all these accessors will be recalculated when students changes:
class Classroom extends Batman.Object
@accessor 'students', -> new Batman.Set
@accessor 'size', -> @get('students.length')
@accessor 'hasStudents', ->
@get('students.isEmpty') # or @get('students').isEmpty()
@accessor 'numberOfPassingStudents', ->
# ::count calls forEach in Batman.Enumerable:
@get('students').count (s) -> s.get('grade') > 1.0
size, hasStudents, and numberOfPassingStudents all register students as a source. (See the docs or this blog post for more information about batman.js automatic source tracking.)
Besides automatic source tracking in accessors, you can observe these properties with observe.
itemsWereAdded/itemsWereRemoved
A set notifies its subscribers by firing:
itemsWereAddedwhen items are added to the setitemsWereRemovedwhen items are removed from the set
Each event is fired with the items that were added and removed.
You can handle these events with on:
set.on 'itemsWereAdded', (addedItems) ->
alert "There were #{addedItems.length} new items!"
set.on 'itemsWereRemoved', (removedItems) ->
alert "Say goodbye to #{removedItems.length} items!"
The event _may be fired with the internally-determined indexes of the items. This is used internally by batman.js but isn’t implemented in all cases._
These functions cause items to be added or removed:
addremovereplaceclearinsert
Under the hood, batman.js depends on these events to keep data-foreach bindings up to date.
Set Indexes
Set indexes are batman.js’s way of searching sets. Batman.js caches these indexes and updates them whenever items are added or removed from the base Batman.Set. This way, you can be sure than any indexes you use will be automatically updated when the set is changed.
Consider the vegetables set:
vegetables = new Batman.Set
{name: "Tomato", color: "red"}
{name: "Cucumber", color: "green"}
{name: "Radish", color: "red"}
{name: "Eggplant", color: "aubergine"}
Batman.SetIndex
A Batman.SetIndex groups the base Batman.Set by a property of its members. For example, we can group vegetables by color:
vegetablesByColor = vegetables.indexedBy('color')
Then, to get vegetables of a certain color, you get the color from the set index:
redVegetables = vegetablesByColor.get('red') # returns a Batman.Set
redVegtables.toArray()
# => [{name: "Tomato", color: "red"}, {name: "Radish", color: "red"}]
(Batman.SetIndex::get is an example of the “default accessor as method_missing” pattern.)
The resulting set is just like any other Batman.Set, so you can observe it, pass it to view bindings, etc.
If you get a value that doesn’t exist, you get an empty Batman.Set. However, if a matching item is added to the base set, the index will be updated and the derived set will have the matching item added to it. For example, the yellow vegetables set is empty at first:
yellowVegetables = vegetablesByColor.get('yellow')
yellowVegetables.get('length') # => 0
But if you add a vegetable with color: "yellow",
vegetables.add({name: "Butternut Squash", color: "yellow"})
it will be immediately added to the derived set:
yellowVegetables.get('first') # => {name: "Butternut Squash", color: "yellow"}
Batman.UniqueSetIndex
A Batman.UniqueSetIndex doesn’t return a set of matching items, it returns the first matching item. This is useful when you know that the values of a property will be unique (For example, batman.js uses MyModel.get('loaded.indexedBy.id') to update records from JSON by ID).
For example, our vegetables all have unique names:
tomato = vegetables.indexedByUnique("name").get("Tomato")
Using indexedByUnique in an accessor makes the Batman.UniqueSetIndex a source for that accessor. So when the unique set index’s value changes, the accessor will be recalculated.
This can be demonstrated by extending our vegetables example a little bit. Imagine a garden which should know what vegetables are growing in it. Since it’s essentially a group of vegetables, let’s extend Batman.Set:
class Garden extends Batman.Set
In our app, we want to display red/green for which vegetables are in a garden. For example, hasTomato:
class Garden extends Batman.Set
@accessor 'hasTomato', ->
@indexedByUnique('name').get("Tomato")?
Now, a Garden will return true for hasTomato as soon as a tomato is added:
myGarden = new Garden
{name: "Spinach", color: "green"}
{name: "Corn", color: "yellow"}
myGarden.get('hasTomato') # => false
myGarden.add({name: "Tomato", color: "red"})
myGarden.get('hasTomato') # => true
SetSort
A Batman.SetSort behaves just like a Batman.Set, except that its members are ordered by a given property. If an item is added to the base set, it is also added to the set sort (in its proper place, of course).
Given these vegetables:
vegetables = new Batman.Set
{name: "Tomato", color: "red"}
{name: "Cucumber", color: "green"}
{name: "Radish", color: "red"}
{name: "Eggplant", color: "aubergine"}
We can easily sort them by name:
vegetables.sortedBy("name") # => Batman.SetSort
vegetables.sortedBy("name").mapToProperty("name")
# => ["Cucumber", "Eggplant", "Tomato", "Radish"]
They can also be sorted in reverse order:
vegetables.sortedBy("name", "desc").mapToProperty("name")
# => ["Radish", "Tomato", "Eggplant", "Cucumber"]
Or, to sort descending by an accessor:
vegetables.get('sortedByDescending.name').mapToProperty("name")
# => ["Radish", "Tomato", "Eggplant", "Cucumber"]
Set Caching
You don’t have to worry about calling indexedBy or sortedBy repeatedly. Under the hood, batman.js caches them on their base sets, so it doesn’t recalculate the indexes and sorts every time.
Union, Intersection, Complement
Batman.BinarySetOperations are objects that track two sets and contain the resulting elements from their operations. There are three implemented subclasses of Batman.BinarySetOperation:
Batman.SetUnioncontains all members from both sets, without duplicates.Batman.SetIntersectioncontains members which are present in the first set and present in the second set.Batman.SetComplementcontains members which are in the first set but not present in the second set.
Take note: constructors for binary set operations will fail if either argument is null, so be sure to check for that when you’re building them!