koel
is a javascript library in two parts.
-
The first part is a tiny, efficient rewrite of http://knockoutjs.com/.
-
The second is a rewrite of the
el()
function used in FastMail.
ko is a tiny data binding library. It lets you make two things:
-
Observables, which wrap a value (such as a string or a number).
var fruitOfTheDay = ko('banana');
-
To read from an observable, call it like you would a function:
fruitOfTheDay(); // => 'banana'
-
To write to an observable, use
.assign()
.fruitOfTheDay.assign('melon');
-
-
Computeds, which wrap a function.
var lunch = ko(function() { return soupOfTheDay() + ' followed by ' + fruitOfTheDay(); });
ko does dependency detection: while the function is executing, it remembers every observable which the computed reads from. It sets up a subscription to each dependency, and will re-compute the function when any of them change.
The return value of the function is used as the new value. Any subscribers to the computed will be notified of the change.
The function should not return an Observable.
Computeds are a kind of observable: you read from them in the same way. You may read from one computed inside another.
You can't assign to a computed.
That's about it; observables and computeds make up the core of ko.
Observables also support the following:
-
.subscribe(function() { … } [, callNow_]_)
Explicitly subscribe to the observable.
fruitOfTheDay.subscribe(function(fruit) { document.title = fruit; });
Every time the observable's value changes, the callback will be invoked with the new value. This is useful when you want to run a function every time a value changes, but unlike a computed, the function doesn't produce a result.
The return value of the function is ignored.
subscribe() takes an optional second argument callNow, which defaults to true. If it's true, it will run the callback immediately.
-
.compute(function(value) { … })
Convenience method. The following are identical:
observable.compute(f)
computed(function() { var value = observable(); return f(value); });
-
.destroy()
Remove all subscribers from the observable.
If it's a computed, this will also remove the related subscriptions from each of our dependencies.
You can extend koel with new types of observable, if you need to send more specific updates than assignment. This is the mechanism used by observableArray.
-
.subscribe({ name: function() { … }, … })
Handle an event with the given name. It takes a dict, so you can pass multiple event handlers.
emit() pops its name argument and calls each handler with the remaining arguments. Handlers are called in the order they were defined.
-
.emit(name [, arguments… ])
Emit an event describing a change to this observable.
All events change the observable's value, so using .subscribe()
will get you
all kinds of changes. If you want to handle only direct assignment events --
that is, calls to .assign() -- listen for the 'assign'
event.
There are also the following methods on ko
itself:
-
ko.observable()
-- in case you really want an observable function. -
ko.computed()
-- will fail if its argument isn't a function. -
ko.subscribe(v, function(value) { … })
-- subscribe to v if its an observable, otherwise just pass it straight to the callback.More efficient than the equivalent
ko.observable(v).subscribe(function(value) { … })
, since it doesn't create an observable if it doesn't need to. -
ko.isObservable(v)
-- is v an observable? -
ko.plugin()
-- a simple way of extending theko()
function.
el is a helper function for creating DOM elements.
For background, read the blog post about Sugared DOM, which this function is based on.
el takes three arguments: selector
, attrs
, and children
. Either or both
of the last two arguments may be omitted.
-
selector
looks like a CSS selector. It consists of a tag name, followed by#the-id
and then many.class-names
. Any of them may be omitted; the default tag isdiv
.Examples:
el('span'); // => <span /> el('.hat-shop'); // => <div class="hat-shop" /> el(''); // => <div /> el('div#main.blue.very-big'); // => <div id="main" class="blue very-big" />
-
attrs
is a dictionary of attributes. These will be set as attributes on the resulting DOM element.Example:
el('a', { href: 'http://google.com', target: '_blank', }, "follow this link");
The following property aliases are supported:
class
className
defaultValue
for
html
text
value
All escaping is handled by the browser.
If a value is an observable, el will automatically subscribe to it, updating the attribute whenever the observable changes.
If you give an observable to the value property, el will set up appropriate
change
event listeners. This is handy for elements likeinput
andselect
. (This doesn't work for computeds, since they can't be assigned.)To bind event handlers, use special
on_«event»
attributes:el('button', { on_click: function(event) { // do stuff }, }, "click me");
-
children
is a string or an array.If it's a string, it will be used instead of the
textContent
property. (You may not set the text both ways on the same element.)Otherwise, each element of the array is either a string or a DOM element. Strings will be converted into text nodes.
children may be an observable.
Examples:
el('h1', "Hi there!"); // => <h1>Hi there!</h1> var score = ko(6); el('p', ['You have ', el('span', score), ' new messages!']); // => <p>You have <span>6</span> new messages!</p>
Final example, using observable arrays:
var cheeses = ko(['cheddar', 'stilton', 'brie']); el('ul', cheeses); cheeses.push('camembert');
koel has a bonus third part: observable arrays.
Plain observable arrays wouldn't be that useful, since you wouldn't be able to tell what changed.
var animals = ko([
'cow',
'sheep',
'horse',
]);
function addPillow() {
animals.assign(animals.concat(['pillow']));
}
animals.subscribe(function(newArray) {
/*
* we have the new array -- but what changed?!
*/
});
addPillow();
ko includes an array plugin which uses observable's event-emitter system to give more useful updates. el uses it in order to do efficient DOM updates.
Observable arrays have the following wrapper interface:
insert(index, item)
-- insert before item at indexreplace(index, item)
-- similar toanimals()[index] = item
remove(index)
-- remove item at indexpush(item)
-- add item to end of arraypop()
→ remove last itemshift()
→ remove first item
You can get more interesting updates by listening for the following events:
animals.subscribe({
assign: function(newArray) { … },
replace: function(index, item) { … },
insert: function(index, item) { … },
remove: function(index) { … },
});
Using animals.subscribe(function() { … })
would give you updates anytime the
array changes for any reason. The assign handler will only fire when the
entire array is replaced by calling .assign().
There's also a bonus third ko
concept, in addition to observables and
computeds: deriveds.
It's useful to be able to call map
or filter
on an array, but recomputing it over the whole array wouldn't be terribly efficient:
el('ul.favourite-animals', ko(function() {
return animals().map(function(name) {
return el('li', name);
});
});
So the wrapped interface also includes the following methods, which return a derived array.
map(function() { … })
filter(function() { … })
You can't modify a derived.
Example:
el('ul.favourite-animals', animals.map(function(name) {
return el('li', name);
});
Any replace/insert/remove changes are propogated to the derived, being careful to only recompute items if their dependencies have changed. So you get the minimal possible change to the derived array. Since el supports this too, koel gives you efficient DOM updates for lists.
Here are some questions. No-one's asked them yet, because koel's just a quick library I needed for a project. But the answers may be helpful.
This is considered an array update:
array.replace(1, 'four');
This is not:
array()[1].fooBar = 'six';
An array tracks the objects in it, not their state. It's just a list of pointers, if you like.
If you want to track element properties, make them into observables.
Don't do that; you'll get confused.
You could do this instead:
array = ko(["cow", "sheep", "horse"]);
array.update(1, "elephant");
// -elsewhere in your code-
array.on('replace', function(index, item) {
// . . .
});
Or you could have an array of objects, where some of the object's properties might be observable.
Object.observe does sound cool. But right now, only Chrome supports it.
And I prefer observables, since they're an object in their own right ("first-class"). Object.observe only lets you subscribe to property updates on an object.
Probably not.
Nope. Use a proper framework instead, such as Overture.
var A = ko(5);
var filter = ko(function(){
var divisor = A();
return function(element){
return element % divisor === 0;
};
});
var list = ko(function(){ return [A()]; });
var filteredList = ko(function(){ var l = list().filter(filter()); console.log(l); return l;});
A.assign(7)
Shut up, Dan.
MIT. (It's small enough that you could always rewrite it yourself anyway.)