mobservable
Advanced tools
Comparing version 0.0.6 to 0.1.0
declare module "mobservable" { | ||
interface Lambda { | ||
(): void; | ||
(): void; | ||
} | ||
interface IProperty<T, S> { | ||
(): T; | ||
(value: T): S; | ||
subscribe(callback: (newValue: T, oldValue: T) => void): Lambda; | ||
interface IObservableValue<T, S> { | ||
(): T; | ||
(value: T): S; | ||
subscribe(callback: (newValue: T, oldValue: T) => void): Lambda; | ||
} | ||
function property<T, S>(value?: T | {(): T;}, scope?: S): IProperty<T, S>; | ||
function guard<T>(func: () => T, onInvalidate: Lambda): [T, Lambda]; | ||
function batch(action: Lambda): void; | ||
function onReady(listener: Lambda): Lambda; | ||
function onceReady(listener: Lambda): void; | ||
function defineProperty<T>(object: Object, name: string, initialValue?: T): void; | ||
interface MobservableStatic { | ||
<T,S>(value?:T|{():T}, scope?:S):IObservableValue<T,S>; | ||
value<T,S>(value?:T|{():T}, scope?:S):IObservableValue<T,S>; | ||
watch<T>(func:()=>T, onInvalidate:Lambda):[T,Lambda]; | ||
array<T>(values?:T[]): IObservableArray<T>; | ||
batch(action:Lambda); | ||
onReady(listener:Lambda):Lambda; | ||
onceReady(listener:Lambda); | ||
defineProperty<T>(object:Object, name:string, initialValue?:T); | ||
} | ||
interface IObservableArray<T> extends Array<T> { | ||
[n: number]: T; | ||
length: number; | ||
spliceWithArray(index:number, deleteCount?:number, newItems?:T[]):T[]; | ||
observe(listener:()=>void, fireImmediately:boolean):Lambda; | ||
clear(): T[]; | ||
replace(newItems:T[]); | ||
values(): T[]; | ||
} | ||
export = MobservableStatic; | ||
} |
@@ -11,19 +11,31 @@ /// <reference path="./typings/node-0.10.d.ts" /> | ||
export interface Lambda { | ||
interface Lambda { | ||
():void; | ||
} | ||
export interface IProperty<T,S> { | ||
interface IObservableValue<T,S> { | ||
():T; | ||
(value:T):S; | ||
subscribe(callback:(newValue:T, oldValue:T)=>void):Lambda; | ||
observe(callback:(newValue:T, oldValue:T)=>void):Lambda; | ||
} | ||
export function property<T,S>(value?:T|{():T}, scope?:S):IProperty<T,S> { | ||
var prop:Property<T,S> = null; | ||
interface MobservableStatic { | ||
<T,S>(value?:T|{():T}, scope?:S):IObservableValue<T,S>; | ||
value<T,S>(value?:T|{():T}, scope?:S):IObservableValue<T,S>; | ||
watch<T>(func:()=>T, onInvalidate:Lambda):[T,Lambda]; | ||
array<T>(values?:T[]): ObservableArray<T>; | ||
batch(action:Lambda); | ||
onReady(listener:Lambda):Lambda; | ||
onceReady(listener:Lambda); | ||
defineProperty<T>(object:Object, name:string, initialValue?:T); | ||
} | ||
function observableValue<T,S>(value?:T|{():T}, scope?:S):IObservableValue<T,S> { | ||
var prop:ObservableValue<T,S> = null; | ||
if (typeof value === "function") | ||
prop = new ComputedProperty(<()=>T>value, scope); | ||
prop = new ComputedObservable(<()=>T>value, scope); | ||
else | ||
prop = new Property(<T>value, scope); | ||
prop = new ObservableValue(<T>value, scope); | ||
@@ -36,10 +48,16 @@ var propFunc = function(value?:T):T|S { | ||
}; | ||
(<any>propFunc).subscribe = prop.subscribe.bind(prop); | ||
(<any>propFunc).observe = prop.observe.bind(prop); | ||
(<any>propFunc).prop = prop; | ||
(<any>propFunc).toString = function() { return prop.toString(); }; | ||
return <IProperty<T,S>> propFunc; | ||
return <IObservableValue<T,S>> propFunc; | ||
} | ||
export function guard<T>(func:()=>T, onInvalidate:Lambda):[T,Lambda] { | ||
var mobservableStatic:MobservableStatic = <MobservableStatic> function<T,S>(value?:T|{():T}, scope?:S):IObservableValue<T,S> { | ||
return observableValue(value,scope); | ||
}; | ||
mobservableStatic.value = observableValue; | ||
mobservableStatic.watch = function watch<T>(func:()=>T, onInvalidate:Lambda):[T,Lambda] { | ||
var dnode = new DNode(); | ||
@@ -62,16 +80,20 @@ var retVal:T; | ||
export function batch(action:Lambda) { | ||
mobservableStatic.array = function array<T>(values?:T[]): ObservableArray<T> { | ||
return new ObservableArray(values); | ||
} | ||
mobservableStatic.batch = function batch(action:Lambda) { | ||
Scheduler.batch(action); | ||
} | ||
export function onReady(listener:Lambda):Lambda { | ||
mobservableStatic.onReady = function onReady(listener:Lambda):Lambda { | ||
return Scheduler.onReady(listener); | ||
} | ||
export function onceReady(listener:Lambda) { | ||
mobservableStatic.onceReady = function onceReady(listener:Lambda) { | ||
Scheduler.onceReady(listener); | ||
} | ||
export function defineProperty<T>(object:Object, name:string, initialValue?:T) { | ||
var _property = property(initialValue, object); | ||
mobservableStatic.defineProperty = function defineProperty<T>(object:Object, name:string, initialValue?:T) { | ||
var _property = mobservableStatic.value(initialValue, object); | ||
Object.defineProperty(object, name, { | ||
@@ -89,3 +111,3 @@ get: function() { | ||
class Property<T,S> { | ||
class ObservableValue<T,S> { | ||
protected events = new events.EventEmitter(); | ||
@@ -101,2 +123,3 @@ protected dependencyState:DNode = new DNode(); | ||
var oldValue = this._value; | ||
// Optimization: intruce a state that signals ready without an initial dirty, for non-computed values | ||
this.dependencyState.markStale(); | ||
@@ -116,3 +139,3 @@ this._value = value; | ||
subscribe(listener:(newValue:T, oldValue:T)=>void, fireImmediately=false):Lambda { | ||
observe(listener:(newValue:T, oldValue:T)=>void, fireImmediately=false):Lambda { | ||
var current = this.get(); // make sure the values are initialized | ||
@@ -129,7 +152,7 @@ if (fireImmediately) | ||
toString() { | ||
return `Property[${this._value}]`; | ||
return `Observable[${this._value}]`; | ||
} | ||
} | ||
class ComputedProperty<U,S> extends Property<U,S> { | ||
class ComputedObservable<U,S> extends ObservableValue<U,S> { | ||
private initialized = false; | ||
@@ -140,3 +163,3 @@ | ||
if (!func) | ||
throw new Error("ComputedProperty requires a function"); | ||
throw new Error("ComputedObservable requires a function"); | ||
@@ -158,3 +181,3 @@ this.dependencyState.compute = this.compute.bind(this); | ||
set(_:U):S { | ||
throw new Error("Computed cannot retrieve a new value!"); | ||
throw new Error("ComputedObservable cannot retrieve a new value!"); | ||
} | ||
@@ -177,6 +200,227 @@ | ||
toString() { | ||
return `ComputedProperty[${this.func.toString()}]`; | ||
return `ComputedObservable[${this.func.toString()}]`; | ||
} | ||
} | ||
/* | ||
TODO: mention clearly that ObservableArray is not sparse, that is, | ||
no wild index assignments with index >(!) length are allowed (that is, won't be observed) | ||
*/ | ||
class ObservableArray<T> implements Array<T> { | ||
[n: number]: T; | ||
length: number; | ||
private _supressLengthNotification: boolean; | ||
private _values: T[]; | ||
private dependencyState:DNode; | ||
private events; | ||
constructor(initialValues?:T[]) { | ||
// make for .. in / Object.keys behave like an array: | ||
Object.defineProperty(this, "length", { | ||
enumerable: false, | ||
get: function() { | ||
this.dependencyState.notifyObserved(); | ||
return this._values.length; | ||
}, | ||
set: function(newLength) { | ||
var currentLength = this._values.length; | ||
if (this._supressLengthNotification === true || newLength != currentLength) // distinguish between internal and external updates | ||
return; | ||
// grow | ||
if (newLength > currentLength) | ||
this.spliceWithArray(currentLength, 0, new Array<T>(newLength - currentLength)); | ||
// shrink | ||
else if (newLength < currentLength) | ||
this.splice(newLength -1, currentLength - newLength); | ||
this.notifyObserved(); | ||
} | ||
}); | ||
Object.defineProperty(this, "dependencyState", { enumerable: false, value: new DNode() }); | ||
Object.defineProperty(this, "_values", { enumerable: false, value: [] }); | ||
Object.defineProperty(this, "events", { enumerable: false, value: new events.EventEmitter() }); | ||
if (initialValues && initialValues.length) | ||
this.spliceWithArray(0, 0, initialValues); | ||
else | ||
this.createNewStubEntry(0); | ||
} | ||
// and adds / removes the necessary numeric properties to this object | ||
// does not alter this._values itself | ||
private updateLength(oldLength:number, delta:number) { | ||
if (delta < 0) { | ||
for(var i = oldLength - 1 - delta; i < oldLength; i++) | ||
delete this[i]; | ||
} | ||
else if (delta > 0) { | ||
for (var i = 0; i < delta; i++) | ||
this.createNewEntry(oldLength + i); | ||
} | ||
else | ||
return; | ||
this.createNewStubEntry(oldLength + delta); | ||
} | ||
private createNewEntry(index: number) { | ||
Object.defineProperty(this, "" + index, { | ||
enumerable: true, | ||
configurable: true, | ||
set: (value) => { | ||
if (this._values[index] !== value) { | ||
this._values[index] = value; | ||
this.notifyChildUpdate(index); | ||
} | ||
}, | ||
get: () => { | ||
this.dependencyState.notifyObserved(); | ||
return this._values[index]; | ||
} | ||
}) | ||
} | ||
private createNewStubEntry(index: number) { | ||
Object.defineProperty(this, "" + index, { | ||
enumerable: false, | ||
configurable: true, | ||
set: (value) => this.push(value), | ||
get: () => undefined | ||
}); | ||
} | ||
spliceWithArray(index:number, deleteCount?:number, newItems?:T[]):T[] { | ||
var length = this._values.length; | ||
// yay, splice can deal with strange indexes | ||
if (index > length) | ||
index = length; | ||
else if (index < 0) | ||
index = Math.max(0, length - index); | ||
// too few arguments? | ||
if (index === undefined) | ||
return; | ||
if (deleteCount === undefined) | ||
deleteCount = length - index; | ||
if (newItems === undefined) | ||
newItems = []; | ||
var lengthDelta = newItems.length - deleteCount; | ||
var res:T[] = Array.prototype.splice.apply(this._values, [<any>index, deleteCount].concat(newItems)); | ||
this.updateLength(length, lengthDelta); // create or remove new entries | ||
this.notifySplice(index, res, newItems); | ||
return res; | ||
} | ||
private notifyChildUpdate(index:number) { | ||
this.notifyChanged(); | ||
// TODO: update Array.observe listeners | ||
} | ||
private notifySplice(index:number, deleted:T[], added:T[]) { | ||
this.notifyChanged(); | ||
// TODO: update Array.observe listeners | ||
} | ||
private notifyChanged() { | ||
this.dependencyState.markStale(); | ||
this.dependencyState.markReady(true); | ||
this.events.emit('change'); | ||
} | ||
// TODO: eS7 event params | ||
observe(listener:()=>void, fireImmediately=false):Lambda { | ||
if (fireImmediately) | ||
listener(); | ||
this.events.addListener('change', listener); | ||
return () => { | ||
this.events.removeListener('change', listener); | ||
}; | ||
} | ||
clear(): T[] { | ||
return this.splice(0); | ||
} | ||
replace(newItems:T[]) { | ||
return this.spliceWithArray(0, this._values.length, newItems); | ||
} | ||
values(): T[] { | ||
return this.slice(0); | ||
} | ||
/* | ||
ES7 goodies | ||
*/ | ||
// TODO: observe(callaback) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/observe | ||
// https://github.com/arv/ecmascript-object-observe | ||
// TODO: unobserve(callback) | ||
/* | ||
functions that do alter the internal structure of the array, from lib.es6.d.ts | ||
*/ | ||
splice(index:number, deleteCount?:number, ...newItems:T[]):T[] { | ||
return this.spliceWithArray(index, deleteCount, newItems); | ||
} | ||
push(...items: T[]): number { | ||
// don't use the property internally | ||
this.spliceWithArray(this._values.length, 0, items); | ||
return this._values.length; | ||
} | ||
pop(): T { | ||
return this.splice(this._values.length, 1)[0]; | ||
} | ||
shift(): T { | ||
return this.splice(0, 1)[0] | ||
} | ||
unshift(...items: T[]): number { | ||
this.spliceWithArray(0, 0, items); | ||
return this._values.length; | ||
} | ||
/* | ||
functions that do not alter the array, from lib.es6.d.ts | ||
*/ | ||
toString():string { return this.wrapReadFunction<string>("toString", arguments); } | ||
toLocaleString():string { return this.wrapReadFunction<string>("toLocaleString", arguments); } | ||
concat<U extends T[]>(...items: U[]): T[] { return this.wrapReadFunction<T[]>("concat", arguments); } | ||
join(separator?: string): string { return this.wrapReadFunction<string>("join", arguments); } | ||
reverse():T[] { return this.wrapReadFunction<T[]>("reverse", arguments); } | ||
slice(start?: number, end?: number): T[] { return this.wrapReadFunction<T[]>("slice", arguments); } | ||
sort(compareFn?: (a: T, b: T) => number): T[] { return this.wrapReadFunction<T[]>("sort", arguments); } | ||
indexOf(searchElement: T, fromIndex?: number): number { return this.wrapReadFunction<number>("indexOf", arguments); } | ||
lastIndexOf(searchElement: T, fromIndex?: number): number { return this.wrapReadFunction<number>("lastIndexOf", arguments); } | ||
every(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean { return this.wrapReadFunction<boolean>("every", arguments); } | ||
some(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean { return this.wrapReadFunction<boolean>("some", arguments); } | ||
forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void { return this.wrapReadFunction<void>("forEach", arguments); } | ||
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[] { return this.wrapReadFunction<U[]>("map", arguments); } | ||
filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T[] { return this.wrapReadFunction<T[]>("filter", arguments); } | ||
reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U { return this.wrapReadFunction<U>("reduce", arguments); } | ||
reduceRight<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U { return this.wrapReadFunction<U>("reduceRight", arguments); } | ||
private wrapReadFunction<U>(funcName:string, args:IArguments):U { | ||
var baseFunc = Array.prototype[funcName]; | ||
// generate a new function that wraps arround the Array.prototype, and replace our own definition | ||
ObservableArray.prototype[funcName] = function() { | ||
this.dependencyState.notifyObserved(); | ||
return baseFunc.apply(this._values, arguments); | ||
} | ||
return this[funcName].apply(this, args); | ||
} | ||
} | ||
//TODO: trick type system | ||
//ObservableArray.prototype = []; // makes, observableArray instanceof Array === true, but not typeof or Array.isArray.. | ||
//y.__proto__ = Array.prototype | ||
//x.prototype.toString = function(){ return "[object Array]" } | ||
//even monky patch Array.isArray? | ||
enum DNodeState { | ||
@@ -201,3 +445,2 @@ STALE, // One or more depencies have changed, current value is stale | ||
addObserver(node:DNode) { | ||
// optimization: replace with length, see: https://jsperf.com/array-push-vs-unshift-vs-direct-assignment/2 | ||
this.observers[this.observers.length] = node; | ||
@@ -261,2 +504,3 @@ } | ||
// optimization: replace this check with an 'unstableDependenciesCounter'. | ||
areAllDependenciesAreStable() { | ||
@@ -330,9 +574,13 @@ var obs = this.observing, l = obs.length; | ||
this.observing = DNode.trackingStack.pop(); | ||
if (this.hasObservingChanged()) { | ||
// optimization, smart compare two lists before removing / deleting / finding cycles | ||
for(var l = this.prevObserving.length, i=0; i<l; i++) | ||
this.prevObserving[i].removeObserver(this); | ||
for(var l = this.observing.length, i=0; i<l; i++) | ||
this.observing[i].addObserver(this); | ||
this.findCycle(this); | ||
var changes = quickDiff(this.observing, this.prevObserving); | ||
var added = changes[0]; | ||
var removed = changes[1]; | ||
for(var i = 0, l = removed.length; i < l; i++) | ||
removed[i].removeObserver(this); | ||
for(var i = 0, l = added.length; i < l; i++) { | ||
added[i].addObserver(this); | ||
added[i].findCycle(this); | ||
} | ||
@@ -345,4 +593,10 @@ } | ||
var ts = DNode.trackingStack, l = ts.length; | ||
if (l) | ||
ts[l-1][ts[l-1].length] = this; | ||
if (l) { | ||
var cs = ts[l -1], csl = cs.length; | ||
// this last item added check is an optimization especially for array loops, | ||
// because an array.length read with subsequent reads from the array | ||
// might trigger many observed events, while just checking the last added item is cheap | ||
if (cs[csl -1] !== this && cs[csl -2] !== this) | ||
cs[csl] = this; | ||
} | ||
} | ||
@@ -427,1 +681,75 @@ | ||
} | ||
/** | ||
* Given a new and an old list, tries to determine which items are added or removed | ||
* in linear time. The algorithm is heuristic and does not give the optimal results in all cases. | ||
* (For example, [a,b] -> [b, a] yiels [[b,a],[a,b]]) | ||
* its basic assumptions is that the difference between base and current are a few splices. | ||
* | ||
* returns a tuple<addedItems, removedItems> | ||
* @type {T[]} | ||
*/ | ||
function quickDiff<T>(current:T[], base:T[]):[T[],T[]] { | ||
if (!base.length) | ||
return [current, []]; | ||
if (!current.length) | ||
return [[], base]; | ||
var added:T[] = []; | ||
var removed:T[] = []; | ||
var currentIndex = 0, | ||
currentSearch = 0, | ||
currentLength = current.length, | ||
currentExhausted = false, | ||
baseIndex = 0, | ||
baseSearch = 0, | ||
baseLength = base.length, | ||
isSearching = false, | ||
baseExhausted = false; | ||
while (!baseExhausted && !currentExhausted) { | ||
if (!isSearching) { | ||
// within rang and still the same | ||
if (currentIndex < currentLength && baseIndex < baseLength && current[currentIndex] === base[baseIndex]) { | ||
currentIndex++; | ||
baseIndex++; | ||
// early exit; ends where equal | ||
if (currentIndex === currentLength && baseIndex === baseLength) | ||
return [added, removed]; | ||
continue; | ||
} | ||
currentSearch = currentIndex; | ||
baseSearch = baseIndex; | ||
isSearching = true; | ||
} | ||
baseSearch += 1; | ||
currentSearch += 1; | ||
if (baseSearch >= baseLength) | ||
baseExhausted = true; | ||
if (currentSearch >= currentLength) | ||
currentExhausted = true; | ||
if (!currentExhausted && current[currentSearch] === base[baseIndex]) { | ||
// items where added | ||
added.push.apply(added, current.slice(currentIndex, currentSearch)); | ||
currentIndex = currentSearch +1; | ||
baseIndex ++; | ||
isSearching = false; | ||
} | ||
else if (!baseExhausted && base[baseSearch] === current[currentIndex]) { | ||
// items where removed | ||
removed.push.apply(removed, base.slice(baseIndex, baseSearch)); | ||
baseIndex = baseSearch +1; | ||
currentIndex ++; | ||
isSearching = false; | ||
} | ||
} | ||
added.push.apply(added, current.slice(currentIndex)); | ||
removed.push.apply(removed, base.slice(baseIndex)); | ||
return [added, removed]; | ||
} | ||
(<any>mobservableStatic).quickDiff = quickDiff; // For testing purposes only | ||
export = mobservableStatic; |
{ | ||
"name": "mobservable", | ||
"version": "0.0.6", | ||
"version": "0.1.0", | ||
"description": "Changes are coming! Small library for creating observable properties en functions", | ||
"main": "mobservable.js", | ||
"main": "dist/mobservable.js", | ||
"scripts": { | ||
"test": "node_modules/.bin/nodeunit test/", | ||
"prepublish": "node_modules/typescript-require/node_modules/typescript/bin/tsc -m commonjs -t ES5 mobservable.ts" | ||
"test": "grunt test", | ||
"prepublish": "grunt build" | ||
}, | ||
@@ -21,6 +21,8 @@ "repository": { | ||
"devDependencies": { | ||
"nodeunit": "^0.9.1", | ||
"grunt": "^0.4.5", | ||
"grunt-contrib-nodeunit": "^0.4.1", | ||
"grunt-ts": "^4.0.1", | ||
"nscript": "^0.1.5", | ||
"typescript-require": "^0.2.8" | ||
"typescript": "^1.4.1" | ||
} | ||
} |
116
README.md
@@ -5,39 +5,73 @@ # MOBservable | ||
MOBservable is simple observable implementation, based on the ideas of observables in bigger frameworks like `knockout`, `ember` etc, but this one does not have 'strings attached'. Furthermore it should fit well in any typescript project. | ||
MOBservable is light-weight stand alone observable implementation, based on the ideas of observables in bigger frameworks like `knockout`, `ember`, but this time without 'strings attached'. Furthermore it should fit well in any typescript project. | ||
# Properties | ||
# Observable values | ||
The `mobservable.property` method takes a value or function and creates an observable value from it. | ||
This way properties that listen that observe each other can be created. A quick example: | ||
The `mobservable.value(valueToObserve)` method (or just its shorthand: `mobservable(valueToObserve)`) takes a value or function and creates an observable value from it. A quick example: | ||
```typescript | ||
import mobservable = require('mobservable'); | ||
var property = mobservable.property; | ||
var vat = property(0.20); | ||
var vat = mobservable(0.20); | ||
var order = {}; | ||
order.price = property(10), | ||
order.priceWithVat = property(() => order.price() * (1+vat())); | ||
order.price = mobservable(10), | ||
order.priceWithVat = mobservable(() => order.price() * (1 + vat())); | ||
order.priceWithVat.subscribe((price) => console.log("New price: " + price)); | ||
order.priceWithVat.observe((price) => console.log("New price: " + price)); | ||
order.price(20); | ||
// Prints: New price: 24 | ||
order.price(10); | ||
// Prints: New price: 10 | ||
vat(0.10); | ||
// Prints: New price: 22 | ||
``` | ||
### mobservable.property(value, scope?) | ||
## mobservable.value(value, scope?):IObservableValue | ||
Constructs a new `Property`, value can either be a string, number, boolean or function that takes no arguments and returns a value. In the body of the function, references to other properties will be tracked, and on change, the function will be re-evaluated. The returned value is an `IProperty` function/object. | ||
Constructs a new observable value. The value can be everything that is not a function, or a function that takes no arguments and returns a value. In the body of the function, references to other properties will be tracked, and on change, the function will be re-evaluated. The returned value is an `IProperty` function/object. Passing an array or object into the `value` method will only observe the reference, not the contents of the objects itself. To observe the contents of an array, use `mobservable.array`, to observe the contents of an object, just make sure its (relevant) properties are observable values themselves. | ||
Optionally accepts a scope parameter, which will be returned by the setter for chaining, and which will used as scope for calculated properties. | ||
The method optionally accepts a scope parameter, which will be returned by the setter for chaining, and which will be used as scope for calculated properties, for example: | ||
### mobservable.defineProperty(object, name, value) | ||
```javascript | ||
var value = mobservable.value; | ||
function OrderLine(price, amount) { | ||
this.price = value(price); | ||
this.amount = value(amount); | ||
this.total = value(function() { | ||
return this.price() * this.amount(); | ||
}, this) | ||
} | ||
``` | ||
## mobservable.array(initialValues?):ObservableArray | ||
**Note: ES5 environments only** | ||
Constructs an array like, observable structure. An observable array is a thin abstraction over native arrays that adds observable properties. The only noticable difference between built-in arrays is that these arrays cannot be sparse, that is, values assigned to an index larger than `length` are not oberved (nor any other property that is assigned to a non-numeric index). In practice, this should harldy be an issue. Example: | ||
```javascript | ||
var numbers = mobservable.array([1,2,3]); | ||
var sum = mobservable.value(function() { | ||
return numbers.reduce(function(a, b) { return a + b }, 0); | ||
}); | ||
sum.observe(function(s) { console.log(s); }); | ||
numbers[3] = 4; | ||
// prints 10 | ||
numbers.push(5,6); | ||
// prints 21 | ||
numbers.unshift(10) | ||
// prints 31 | ||
``` | ||
## mobservable.defineProperty(object, name, value) | ||
**Note: ES5 environments only** | ||
Defines a property using ES5 getters and setters. This is useful in constructor functions, and allows for direct assignment / reading from observables: | ||
```javascript | ||
var vat = property(0.2); | ||
var vat = mobservable.value(0.2); | ||
var Order = function() { | ||
@@ -71,15 +105,15 @@ mobservable.defineProperty(this, 'price', 20); | ||
### mobservable.guard(func, onInvalidate) | ||
## mobservable.watch(func, onInvalidate) | ||
`guard` invokes `func` and returns a tuple consisting of the return value of `func` and an unsubscriber. `guard` will track which observables `func` was observing, but it will *not* recalculate `func` if necessary, instead, it will fire the `onInvalidate` callback to notify that the output of `func` can no longer be trusted. | ||
`watch` invokes `func` and returns a tuple consisting of the return value of `func` and an unsubscriber. `watch` will track which observables `func` was observing, but it will *not* recalculate `func` if necessary, instead, it will fire the `onInvalidate` callback to notify that the output of `func` can no longer be trusted. | ||
The `onInvalidate` function will be called only once, after that, the guard has finished. To abort a guard, use the returned unsubscriber. | ||
The `onInvalidate` function will be called only once, after that, the watch has finished. To abort a watch, use the returned unsubscriber. | ||
Guard is useful in functions where you want to have `func` observable, but func is actually invoked as side effect or part of a bigger change flow or where unnecessary recalculations of `func` or either pointless or expensive. | ||
`Watch` is useful in functions where you want to have a function that responds to change, but where the function is actually invoked as side effect or part of a bigger change flow or where unnecessary recalculations of `func` or either pointless or expensive, for example in React component render methods | ||
### mobservable.batch(workerFunction) | ||
## mobservable.batch(workerFunction) | ||
Batch postpones the updates of computed properties until the (synchronous) `workerFunction` has completed. This is useful if you want to apply a bunch of different updates throughout your model before needing the updated computed values, for example while refreshing a value from the database. | ||
### mobservable.onReady(listener) / mobservable.onceReady(listener) | ||
## mobservable.onReady(listener) / mobservable.onceReady(listener) | ||
@@ -90,14 +124,42 @@ The listener is invoked each time the complete model has become stable. The listener is always invoked asynchronously, so that even without `batch` the listener is only invoked after a bunch of changes have been applied | ||
### IProperty() | ||
## `IObservableValue` objects | ||
Returns the current value of the property | ||
### IObservableValue() | ||
### IProperty(value) | ||
If an IObservableValue object is called without arguments, the current value of the observer is returned | ||
Sets a new value to the property. Returns the scope with which this property was created for chaining. | ||
### IObservableValue(newValue) | ||
### IProperty.subscribe(listener,fireImmediately=false) | ||
If an IObservable object is called with arguments, the current value is updated. All current observers will be updated as well. | ||
### IObservableValue.observe(listener,fireImmediately=false) | ||
Registers a new listener to change events. Listener should be a function, its first argument will be the new value, and second argument the old value. | ||
Returns a function that upon invocation unsubscribes the listener from the property. | ||
## `ObservableArray` | ||
An `ObservableArray` is an array-like structure with all the typical behavior of arrays, so you can freely assign new values to (non-sparse) indexes, alter the length, call array functions like `map`, `filter`, `shift` etc. etc. All the ES5 features are in there. Additionally available methods: | ||
### ObservableArray.clear() | ||
Removes all elements from the array and returns the removed elements. Shorthand for `ObservableArray.splice(0)` | ||
### ObservableArray.replace(newItemsArray) | ||
Replaces all the items in the array with `newItemsArray`, and returns the old items. | ||
### ObservableArray.spliceWithArray(index, deleteCount, newItemsArray) | ||
Similar to `Array.splice`, but instead of accepting a variable amount of arguments, the third argument should be an array containing the new arguments. | ||
### ObservableArray.observe(callback) | ||
Register a callback that will be triggered every time the array is altered. A method to unregister the callback is returned. | ||
In the near feature this will adhere to the ES7 specs for Array.observe so this class can be used as ES7 array shim. | ||
### ObservableArray.values() | ||
Returns all the values of this ObservableArray as native, non-observable, javascript array. The returned array is a shallow copy. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
114991
8
2540
162
5