View on GitHub

obs

observable properties done right

Download this project as a .zip file Download this project as a tar.gz file

Synopsis

obs is a powerful implementation of observable properties that can be used on both the client-side and the server-side.

Together with rivets.js it can serve as a lightweight alternative to Knockout.js.

license - MIT Flattr this

browser support

Build Status Coverage Status Dependencies

NPM status

Install

Node.js

With NPM

npm install obs

From source

git clone https://github.com/pluma/obs.git
cd obs
npm install
make && make dist

Browser

With component

component install pluma/obs

Learn more about component.

With a CommonJS module loader

Download the latest minified CommonJS release and add it to your project.

Make sure you also have a compatible copy of sublish.

Learn more about CommonJS modules.

With an AMD module loader

Download the latest minified AMD release and add it to your project.

Make sure you also have a compatible copy of sublish.

Learn more about AMD modules.

As standalone bundle

Get the latest distribution bundle (~4.8 kB minified or ~1.4 kB gzipped, includes sublish 2.0.0) and download it to your project.

<script src="/your/js/path/obs.all.min.js"></script>

This makes the obs module available in the global namespace.

If you are already using sublish in your project, you can download the latest minified standalone release (~4.0 kB minified or ~1.2 kB gzipped) instead.

Basic usage example with node.js

var obs = require('obs');
var x = obs.prop(2),
    y = obs.prop(5),
    sum = obs.computed(function() {
        return x() + y();
    }, [x, y]),
    product = obs.computed(function() {
        return x() * y();
    }, [x, y]);

console.log('sum is currently ' + sum());
// 'sum is currently 7'
console.log('product is currently ' + product());
// 'product is currently 10'

sum.subscribe(function(value, old) {
    console.log('sum is now ' + value + ' (was: ' + old + ')');
});
product.subscribe(function(value, old) {
    console.log('product is now ' + value + ' (was: ' + old + ')');
});

x(3);
// 'sum is now 8 (was: 7)'
// 'product is now 15 (was: 10)'
console.log('sum is currently ' + sum());
// 'sum is currently 8'
y(8);
// 'sum is now 11 (was: 8)'
// 'product is now 24 (was: 15)'

Example with writable computed observables

var obs = require('obs');
var firstname = obs.prop('John'),
    lastname = obs.prop('Doe'),
    fullname = obs.computed({
        compute: function() {
            return firstname() + ' ' + lastname();
        },
        write: function(value) {
            var tokens = (value || '').split(' ');
            firstname(tokens[0]);
            lastname(tokens.slice(1).join(' '));
        },
        watch: [firstname, lastname]
    });
console.log(fullname()); // John Doe
fullname('Konrad von Zuse');
console.log(firstname()); // Konrad
console.log(lastname()); // von Zuse

Client-side example with rivets.js data-binding

Try it on jsfiddle.

HTML

<div id="view">
    <label>
        Your name:
        <input data-rv-value="user.name"/>
    </label>
    <div data-rv-bgcolor="color">
        Hello <span data-rv-text="user.name"></span>!<br>
        The current UNIX time is: <span data-rv-text="now"></span>
    </div>
</div>

CSS

#view {
    font: 16px Verdana, Arial, sans-serif;
}
#view div {
    padding: 10px;
}

JavaScript

Utilities

var colors = [
    'rgba(255,0,0,0.5)', 'rgba(255,255,0,0.5)',
    'rgba(0,255,0,0.5)', 'rgba(0,255,255,0.5)',
    'rgba(0,0,255,0.5)', 'rgba(255,0,255,0.5)'
];
function resolveKeypath(obj, keypath) {
    keypath.split('.').forEach(function(key) {
        if (key) {
            obj = obj[key];
        }
    });
    return obj;
}

Rivets.js adapter and configuration

rivets.configure({
    prefix: 'rv',
    adapter: {
        subscribe: function(obj, keypath, callback) {
            resolveKeypath(obj, keypath).subscribe(callback);
        },
        unsubscribe: function(obj, keypath, callback) {
            resolveKeypath(obj, keypath).unsubscribe(callback);
        },
        read: function(obj, keypath) {
            return resolveKeypath(obj, keypath)();
        },
        publish: function(obj, keypath, value) {
            resolveKeypath(obj, keypath)(value);
        }
    }
});

rivets.binders.bgcolor = function(el, value) {
    el.style.backgroundColor = value;
};

ViewModel (using obs.js)

var viewModel = {
    now: obs.prop(+new Date()),
    color: obs.prop(colors[0]),
    user: {
        name: obs.prop('User')
    }
};

var view = rivets.bind($('#view'), viewModel);

setInterval(function() {
    viewModel.now(+new Date());
    viewModel.color(colors[
        Math.floor(Math.random() * colors.length)
    ]);
}, 3000);

API

obs: Abstract observables

This provides the base functionality for observables. You probably want to use obs.prop and obs.computed instead of calling obs directly.

obs(options):Function

Creates an observable.

options.context (optional)

The context the observable's read and write functions will be executed in.

Defaults to the observable instance if not explicitly set.

options.read:Function and options.write:Function (optional)

The functions to be called when the observable is read from or written to.

An error will be raised if the observable is read from but no read function was defined, or if it is written to and no write function was defined.

options.watched:Array (optional)

An array of objects this observable subscribes to. If the value is not an array, it will be wrapped in one. Each object should have a subscribe method and (optionally) an unsubscribe method.

options.onNotify:Function (optional)

Function to be called when an object the observable is watching changes. Defaults to the observable's notify method, effectively turning the observable into a relay.

observable()

Calls the observable's read function with its context.

observable(value)

Calls the observable's write function with its context and the given value.

observable.subscribe(callback:Function)

Adds the given callback function to this observable's list of subscribers.

The callback will be called with the observable's new and old value as its arguments whenever the observable's value is updated (even if the new value is equal to the old value).

observable.unsubscribe(callback:Function):Boolean

Removes the given callback function from this observable's list of subscribers. The callback will no longer be called when the observable's value changes.

Returns false if the callback could not be found in the list of subscribers or true otherwise.

NOTE: Remember to use the exact function that was passed to observable.subscribe.

observable.peek()

Returns the observable's current value without invoking its read function.

observable.commit()

Sets the observable's initial value to its current value and clears its dirty flag.

observable.reset()

Resets the observable to its initial value (or undefined), then calls notify().

observable.notify()

Updates the observable's dirty flag, then notifies all subscribers with the its current and previous value.

observable.watch(dependencies…)

Adds the given dependencies to this observable. Each dependency should have a subscribe and unsubscribe method. Whenever one of the dependencies changes, this observable's onNotify function will be called.

observable.unwatch(dependencies…)

Removes the given dependencies by calling their unsubscribe methods. The observable will no longer be notified when their values change.

observable.dismiss()

Removes all of the observable's dependencies. Equivalent to calling observable.unwatch for each dependency.

obs.fn

An object containing attributes that will be applied to new observables.

obs.prop: Observable properties

This provides a simple wrapper around obs useful for observables that should just act as a single value storage.

obs.prop([initialValue])

Creates an observable property (optionally initialized with the given value).

obs.prop#()

Returns the property's current value.

obs.prop#(newValue)

Sets the property's current value to newValue and notifies all subscribers.

obs.computed: Computed observables

This provides a simple wrapper around obs to create observables that depend on other observables and have values that should be computed dynamically, e.g. composite values of other observables.

obs.computed(compute:Function, [write:Function], [watch:Array])

Creates a computed observable observable. The observable's value will be set to the return value of the given function compute and updated whenever any of the watch functions changes.

If write is passed, that function will be used when the computed observable is passed a value.

The list of watch functions can be an array containing any kind of object that supports the subscribe and (optionally) unsubscribe methods (e.g. an instance of sublish.PubSub).

obs.computed(options)

Creates a computed observable property with the given options.

options.compute:Function (optional)

The function this computed observable will use to generate its value. If this option is not provided, the observable will be write-only.

options.write:Function (optional)

The function this computed observable will use when it is passed a value. If this option is not provided, the observable will be read-only.

options.watch:Array (optional)

See above. This option has no effect if no read function is provided. If a single object is passed instead of an array, the object will automatically be wrapped in an array.

options.context (optional)

The context the compute and write functions will be executed in. Defaults to the computed observable itself.

computedObservable()

Returns the computed property's current value. For lazy computed observables, this will trigger the function evaluation and notify any subscribers.

obs.computed.lazy(compute:Function, [write:Function], [watched:Array])

See obs.computed(…). The created observable will only call its compute function to update its value when it is explicitly read from.

obs.computed.lazy(options)

See obs.computed(options) and above.

License

The MIT/Expat license. For more information, see http://pluma.mit-license.org/ or the accompanying LICENSE file.