Skip to content

Commit

Permalink
Factored out ObservableObjectAdministration class
Browse files Browse the repository at this point in the history
  • Loading branch information
mweststrate committed Feb 13, 2016
1 parent 63e433b commit 56c86e3
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 91 deletions.
6 changes: 3 additions & 3 deletions src/api/extendobservable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ValueMode} from "../types/modifiers";
import {ObservableMap} from "../types/observablemap";
import {ObservableObject} from "../types/observableobject";
import {asObservableObject, setObservableObjectProperty} from "../types/observableobject";

/**
* Extends an object with reactive capabilities.
Expand All @@ -23,9 +23,9 @@ export function extendObservable<A extends Object, B extends Object>(target: A,


export function extendObservableHelper(target, properties, mode: ValueMode, name: string): Object {
const meta = ObservableObject.asReactive(target, name, mode);
const adm = asObservableObject(target, name, mode);
for (let key in properties) if (properties.hasOwnProperty(key)) {
meta.set(key, properties[key]);
setObservableObjectProperty(adm, key, properties[key]);
}
return target;
}
6 changes: 0 additions & 6 deletions src/api/extras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@
* (c) 2015 - Michel Weststrate
* https://github.com/mweststrate/mobservable
*/
import {Atom} from "../core/atom";
import {ObservableArray} from "../types/observablearray";
import {Reaction} from "../core/reaction";
import {ComputedValue} from "../core/computedvalue";
import {IDepTreeNode} from "../core/observable";
import {ObservableObject} from "../types/observableobject";
import {ObservableMap} from "../types/observablemap";
import {SimpleEventEmitter} from "../utils/simpleeventemitter";
import {once, unique, Lambda} from "../utils/utils";
import {globalState} from "../core/globalstate";
Expand Down
6 changes: 3 additions & 3 deletions src/api/isobservable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ObservableArray} from "../types/observablearray";
import {ObservableMap} from "../types/observablemap";
import {ObservableObject} from "../types/observableobject";
import {isObservableObject, IObservableObjectAdministration} from "../types/observableobject";
import {Atom} from "../core/atom";
import {ComputedValue} from "../core/computedvalue";
import {Reaction} from "../core/reaction";
Expand All @@ -16,8 +16,8 @@ export function isObservable(value, property?: string): boolean {
if (property !== undefined) {
if (value instanceof ObservableMap || value instanceof ObservableArray)
throw new Error("[mobservable.isObservable] isObservable(object, propertyName) is not supported for arrays and maps. Use map.has or array.length instead.");
else if (value.$mobservable instanceof ObservableObject) {
const o = <ObservableObject>value.$mobservable;
else if (isObservableObject(value)) {
const o = <IObservableObjectAdministration> value.$mobservable;
return o.values && !!o.values[property];
}
return false;
Expand Down
4 changes: 2 additions & 2 deletions src/api/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ export function observable(v: any, keyOrScope?: string | any) {
switch (sourceType) {
case ValueType.Reference:
case ValueType.ComplexObject:
return new ObservableValue(value, mode, null);
return new ObservableValue(value, mode, undefined);
case ValueType.ComplexFunction:
throw new Error("[mobservable.observable] To be able to make a function reactive it should not have arguments. If you need an observable reference to a function, use `observable(asReference(f))`");
case ValueType.ViewFunction:
return new ComputedValue(value, keyOrScope, value.name, mode === ValueMode.Structure);
case ValueType.Array:
case ValueType.PlainObject:
return makeChildObservable(value, mode, null);
return makeChildObservable(value, mode, undefined);
}
throw "Illegal State";
}
Expand Down
6 changes: 3 additions & 3 deletions src/api/observabledecorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ValueMode, asReference} from "../types/modifiers";
import {allowStateChanges} from "../api/extras";
import {ObservableObject} from "../types/observableobject";
import {asObservableObject, setObservableObjectProperty} from "../types/observableobject";

/**
* ES6 / Typescript decorator which can to make class properties and getter functions reactive.
Expand Down Expand Up @@ -54,14 +54,14 @@ export function observableDecorator(target: Object, key: string, baseDescriptor:
// the getter might create a reactive property lazily, so this might even happen during a view.
// TODO: eliminate non-strict; creating observables during views is allowed, just don't use set.
allowStateChanges(true, () => {
ObservableObject.asReactive(this, null, ValueMode.Recursive).set(key, baseValue);
setObservableObjectProperty(asObservableObject(this, null, ValueMode.Recursive), key, baseValue);
});
return this[key];
};
descriptor.set = isDecoratingGetter
? () => {throw new Error(`[DerivedValue '${key}'] View functions do not accept new values`); }
: function(value) {
ObservableObject.asReactive(this, null, ValueMode.Recursive).set(key, typeof value === "function" ? asReference(value) : value);
setObservableObjectProperty(asObservableObject(this, null, ValueMode.Recursive), key, typeof value === "function" ? asReference(value) : value);
}
;
if (!baseDescriptor) {
Expand Down
12 changes: 6 additions & 6 deletions src/api/observe.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {IObservableArray, IArrayChange, IArraySplice, isObservableArray} from "../types/observablearray";
import {ObservableMap, IObservableMapChange, isObservableMap} from "../types/observablemap";
import {IObjectChange, isObservableObject} from "../types/observableobject";
import {observable, IObservableValue} from "./observable";
import {IObjectChange, isObservableObject, observeObservableObject} from "../types/observableobject";
import {IObservableValue, observable} from "./observable";
import {ComputedValue} from "../core/computedvalue";
import {ObservableValue} from "../types/observablevalue";
import {Lambda, isPlainObject} from "../utils/utils";
Expand All @@ -23,7 +23,7 @@ export function observe(thing, property?, listener?): Lambda {
if (isObservableArray(thing))
return thing.observe(listener);
if (isObservableMap(thing)) {
if (property) {
if (property !== undefined) {
if (!thing._has(property))
throw new Error("[mobservable.observe] the provided observable map has no key with name: " + property);
return observe(thing._data[property], listener);
Expand All @@ -32,19 +32,19 @@ export function observe(thing, property?, listener?): Lambda {
}
}
if (isObservableObject(thing)) {
if (property) {
if (property !== undefined) {
if (!isObservable(thing, property))
throw new Error("[mobservable.observe] the provided object has no observable property with name: " + property);
return observe(thing.$mobservable.values[property], listener);
}
return thing.$mobservable.observe(listener);
return observeObservableObject(thing, listener);
}
if (thing instanceof ObservableValue || thing instanceof ComputedValue)
return observeObservableValue(thing, listener, fireImmediately);
if (thing.$mobservable instanceof ObservableValue || thing.$mobservable instanceof ComputedValue)
return observeObservableValue(thing.$mobservable, listener, fireImmediately);
if (isPlainObject(thing))
return (<any>observable(thing)).$mobservable.observe(listener);
return observeObservableObject(<any> observable(<Object> thing), listener);
throw new Error("[mobservable.observe] first argument should be an observable array, observable map, observable object, observable value, computed value or plain object.");
}

Expand Down
4 changes: 1 addition & 3 deletions src/types/observablemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ export interface IKeyValueMap<V> {

export type IMapEntries<V> = [string, V][]

export interface IObservableMapChange<T> extends IObjectChange<T, ObservableMap<T>> {

}
export interface IObservableMapChange<T> extends IObjectChange<T, ObservableMap<T>> { }

export class ObservableMap<V> {
$mobservable = {};
Expand Down
148 changes: 83 additions & 65 deletions src/types/observableobject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {ComputedValue} from "../core/computedvalue";
import {ValueMode, AsStructure} from "./modifiers";
import {Lambda} from "../utils/utils";
import {SimpleEventEmitter} from "../utils/simpleeventemitter";
import {getNextId} from "../core/globalstate";

export interface IObjectChange<T, R> {
name: string;
Expand All @@ -16,82 +17,99 @@ export interface IObjectChange<T, R> {
oldValue?: T;
}

// responsible for the administration of objects that have become reactive
export class ObservableObject {
values: {[key: string]: ObservableValue<any>|ComputedValue<any>} = {};
private _events = new SimpleEventEmitter();
const ObservableObjectMarker = {};

constructor(private target, private name: string, private mode: ValueMode) {
if (target.$mobservable)
throw new Error("Illegal state: already an reactive object");
export interface IObservableObjectAdministration {
type: Object;
target: any;
name: string;
mode: ValueMode;
values: {[key: string]: ObservableValue<any>|ComputedValue<any>};
_events: SimpleEventEmitter;
}

Object.defineProperty(target, "$mobservable", {
enumerable: false,
configurable: false,
value: this
});
}
export interface IIsObservableObject {
$mobservable: IObservableObjectAdministration;
}

static asReactive(target, name: string, mode: ValueMode):ObservableObject {
if (target.$mobservable)
return target.$mobservable;
return new ObservableObject(target, name, mode);
export function asObservableObject(target, name: string = "" /* TODO: "ObservableObject" + getNextId() */, mode: ValueMode = ValueMode.Recursive): IObservableObjectAdministration {
if (target.$mobservable) {
if (target.$mobservable.type !== ObservableObjectMarker)
throw new Error("The given object is observable but not an observable object");
return target.$mobservable;
}
const adm: IObservableObjectAdministration = {
type: ObservableObjectMarker,
values: {},
_events: new SimpleEventEmitter(), // TODO create lazy, rename
target, name, mode
};
Object.defineProperty(target, "$mobservable", {
enumerable: false,
configurable: false,
writable: false,
value: adm
});
return adm;
}

set(propName, value) {
if (this.values[propName])
this.target[propName] = value; // the property setter will make 'value' reactive if needed.
else
this.defineReactiveProperty(propName, value);
}
export function setObservableObjectProperty(adm: IObservableObjectAdministration, propName: string, value) {
if (adm.values[propName])
adm.target[propName] = value; // the property setter will make 'value' reactive if needed.
else
defineObservableProperty(adm, propName, value);
}

private defineReactiveProperty(propName, value) {
let observable: ComputedValue<any>|ObservableValue<any>;
let name = `${this.name || ""}.${propName}`;
function defineObservableProperty(adm: IObservableObjectAdministration, propName: string, value) {
let observable: ComputedValue<any>|ObservableValue<any>;
let name = `${adm.name}.${propName}`;

if (typeof value === "function" && value.length === 0)
observable = new ComputedValue(value, this.target, name, false);
else if (value instanceof AsStructure && typeof value.value === "function" && value.value.length === 0)
observable = new ComputedValue(value.value, this.target, name, true);
else
observable = new ObservableValue(value, this.mode, name);
if (typeof value === "function" && value.length === 0)
observable = new ComputedValue(value, adm.target, name, false);
else if (value instanceof AsStructure && typeof value.value === "function" && value.value.length === 0)
observable = new ComputedValue(value.value, adm.target, name, true);
else
observable = new ObservableValue(value, adm.mode, name);

this.values[propName] = observable;
Object.defineProperty(this.target, propName, {
configurable: true,
enumerable: observable instanceof ObservableValue,
get: function() {
return this.$mobservable ? this.$mobservable.values[propName].get() : undefined;
},
set: function(newValue) {
const oldValue = this.$mobservable.values[propName].get();
this.$mobservable.values[propName].set(newValue);
this.$mobservable._events.emit(<IObjectChange<any, any>> {
type: "update",
object: this,
name: propName,
oldValue
});
}
});
adm.values[propName] = observable;
Object.defineProperty(adm.target, propName, {
configurable: true,
enumerable: observable instanceof ObservableValue,
get: function() {
// TODO: why the ternary if?
return this.$mobservable ? this.$mobservable.values[propName].get() : undefined;
},
set: function(newValue) {
const self = <IObservableObjectAdministration> this.$mobservable; // TODO: faster to just use adm?
const oldValue = self.values[propName].get();
self.values[propName].set(newValue);
self._events.emit(<IObjectChange<any, any>> {
type: "update",
object: this,
name: propName,
oldValue
});
}
});

this._events.emit(<IObjectChange<any, any>> {
type: "add",
object: this.target,
name: propName
});
}
adm._events.emit(<IObjectChange<any, any>> {
type: "add",
object: adm.target,
name: propName
});
}

/**
* Observes this object. Triggers for the events 'add', 'update' and 'delete'.
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe
* for callback details
*/
observe(callback: (changes: IObjectChange<any, any>) => void): Lambda {
return this._events.on(callback);
}
/**
* Observes this object. Triggers for the events 'add', 'update' and 'delete'.
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe
* for callback details
*/
export function observeObservableObject(object: IIsObservableObject, callback: (changes: IObjectChange<any, any>) => void): Lambda {
if (!isObservableObject(object))
throw new Error("Expected observable object");
return object.$mobservable._events.on(callback);
}

export function isObservableObject(thing): boolean {
return thing && typeof thing === "object" && thing.$mobservable instanceof ObservableObject;
return thing && thing.$mobservable && thing.$mobservable.type === ObservableObjectMarker;
}

0 comments on commit 56c86e3

Please sign in to comment.