Reiterate
If you’re familiar with Ruby and Rails, you probably know all about Symbol#to_proc and Methodphitamine. I wanted some similar goodness for Prototype, so I could stop writing
var value = radioGroup.find(function(radio) {
return radio.checked;
}).value;
var divs = someDivs.findAll(function(div) {
return div.hasClassName('myClass') && div.visible();
});
and instead write
var value = radioGroup.find('checked').value;
var divs = someDivs.findAll({hasClassName: 'myClass', visible: true});
So I went about writing it. It started off with a simple patch over at Rails Trac that extended the idea behind Enumerable#pluck to lots of other methods, but then I got carried away. You can now use strings, arrays and hashes instead of iterator functions to make your code more readable.
Download
Download Reiterate 1.3.1. The download contains the source, a packed version and a gzipped version (just 867 bytes!).
Documentation
Reiterate affects the following Enumerable methods: all, any, collect, detect, findAll, max, min, partition, reject, sortBy, map, find, select, filter, every, and some. Instead of passing functions to them you can now use strings, arrays and objects/hashes.
Strings. Strings are fairly intuitive to use. They work just like they do with pluck but with some extra features:
- You can use them to fetch the return values of methods, not just properties:
['FOO', 'BAR', 'BAZ'].map('toLowerCase')
// -> ['foo', 'bar', 'baz']
[[2,8,7], [6,2,6], [1,7,9]].sortBy('join')
// -> [[1, 7, 9], [2, 8, 7], [6, 2, 6]]
['FOO', 'BAR', 'BAZ'].map('toLowerCase.toArray')
// -> [['f', 'o', 'o'], ['b', 'a', 'r'], ['b', 'a', 'z']]
['FOO', 'BAR', 'BAZ'].map('toArray.length')
// -> [3, 3, 3]
Arrays. When using an array, the first member acts as the method name and the rest of the array as arguments. That’s pretty much all there is to it:
var divs = someDivs.findAll(['hasClassName', 'myClass']);
// is the same as
var divs = someDivs.findAll(function(div) {
return div.hasClassName('myClass');
});
// Sorting by third letter
['apples', 'oranges', 'plums'].sortBy(['replace', /^(..)(.)/, '$2$1'])
// -> ["oranges", "apples", "plums"]
Hashes. Although this might look like it’s done by messing with Object.prototype, it isn’t. Hashes can be used in a similar way to arrays, and some would argue they are neater:
var divs = someDivs.findAll({hasClassName: 'myClass'});
is equivalent to the first Array example above. Hashes come into their own when you use more than one key/value pair. If each key is a method name, the method is executed within its parent object using the corresponding value as arguments. If it’s a property, then its value is compared to the value you supply. Some examples will make this clearer:
var divs = arr.findAll({hasClassName: 'myClass', tagName: 'DIV'});
// is the same as
var divs.findAll(function(div) {
return div.hasClassName('myClass') && div.tagName == 'DIV';
});
var divs = arr.findAll({hasClassName: 'myClass', visible: true});
// is the same as
var divs.findAll(function(div) {
return div.hasClassName('myClass') && div.visible(true);
});
That true value is not compared to the return value of visible, it’s just there because you need a value of some sort in a hash pair. The ordering of keys is not necessarily the same as the order you specify them in when the function is executed.
If a function takes several arguments, you need to surround them in array braces:
someStrings.sortBy({substr: [2, 4]})
Reiterate gets really useful when you’re chaining methods together. Let’s say you want to find all the background images in use in a document:
var bgs = $$('*').collect({getStyle:
'backgroundImage'}).reject({match: 'none'});
// is the same as...
var bgs = $$('*').collect(function(node) {
return node.getStyle('backgroundImage');
}).reject(function(path) {
return path.match('none');
});
(Yes, you could use invoke for the first function — the point is to illustrate how this works, and to show that collect can be made to behave like its Ruby counterpart.)
Binary operators. These are things like + and ||; operators which sit between two variables. Ruby lets you treat some of these as instance methods, and Reiterate (1.1 onwards) allows this too. Supported operators are +, -, *, /, %, <, <=, >, >=, ==, !=, ===, !==, &&, &, ||, |, typeof, instanceof. Some examples:
// Count members within a range
numbers.findAll({'>': 4, '<=': 27}).length
// Remove items of a specific type
collection.reject(['instanceof', String])
// Assign default values to a collection
[27, 0, 'prototype', '', true, false].map({'||': 'foo'})
// -> [27, "foo", "prototype", "foo", true, "foo"]
You can use either Arrays or Hashes with these methods, and you must supply one argument for each operator used. Also note that this is not done using eval, it’s done using a collection of functions that can be applied to objects so that operators effectively become methods, e.g.
Function.Operators['+'] = function(x) { return this + x; }
Counting. Reiterate 1.2 adds a count method, that acts as a shorthand for findAll().length. You can use Strings, Arrays, Hashes and regular old functions with it:
someDivs.count('visible')
someDivs.count({hasClassName: 'myClass'})
someDivs.count(function(div) {
return div.visible() &&
div.getStyle('backgroundImage') == 'picture.png';
})
Feedback
Any questions? Suggestions? Bugs? Generous monetary donations? Drop a comment down the bottom there. I’ve tested this against a development copy of Prototype 1.6.0 and a production site using 1.5.0. It works its magic by ‘intercepting’ method calls rather than overwriting originals, so the exact details of the methods affected shouldn’t matter too much. Fingers crossed.
I’d quite like to see this go into the core, so if you like it then feel free to swing by my ticket and give me a +1 in the comments.