Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
360 lines
11 KiB
JavaScript
Executable file
360 lines
11 KiB
JavaScript
Executable file
/**
|
|
* Observable provides ways of defining properties that the value changes of
|
|
* them can be observed. In addition to normal properties, it allows to
|
|
* create read-only properties and dependency properties.
|
|
*
|
|
* Observable creation:
|
|
* There are two ways of defining an Observable: object literal or extending
|
|
* from Observable. Object literal is useful when defining a singleton or you
|
|
* want to create an observable easily.
|
|
*
|
|
* @example
|
|
* var observable = Observable({
|
|
* prop: 10,
|
|
* func: function() {}
|
|
* });
|
|
*
|
|
* Extending from Observable allow you to define a class of Observable. The
|
|
* advantage of this compared to object literal is that the accessers are
|
|
* shared across all instances of the class. The syntax compatible with the
|
|
* javascript prototype definition.
|
|
*
|
|
* @example
|
|
* var ExtendedObservable = Module.create(function ExtendedObservable() {
|
|
* this.super(Observable).call(this);
|
|
* }).extend(Observable);
|
|
* Observable.defineObservableProperty(ExtendedObservable.prototype, 'prop',
|
|
* {
|
|
* value: 10
|
|
* });
|
|
* ExtendedObservable.prototype.func = function() {};
|
|
*
|
|
* // Extend from NewObservable
|
|
* var ExtendedObservable2 = Module.create(function() {
|
|
* // constructor
|
|
* }).extend(ExtendedObservable);
|
|
* Observable.defineObservableProperty(
|
|
* ExtendedObservable2.prototype, 'prop2',
|
|
* {
|
|
* value: 20
|
|
* });
|
|
*
|
|
* var observable = ExtendedObservable2();
|
|
* console.log(observable.prop); // 10
|
|
* console.log(observable.prop2); // 20
|
|
*
|
|
* Defining a read-only property:
|
|
* This is only supported when extending from Observable. When you define a
|
|
* read-only property, an internal property with a '_' prefix is defined at
|
|
* the same time so you can still change the value inside the observable.
|
|
*
|
|
* @example
|
|
* Observable.defineObservableProperty(ExtendedObservable.prototype, 'prop',
|
|
* {
|
|
* readonly: true,
|
|
* value: 10
|
|
* });
|
|
* ExtendedObservable.prototype.inc = function() {
|
|
* this._prop = 100;
|
|
* };
|
|
* var observable = new ExtendedObservable();
|
|
* observable.prop = 100; // throws an exception
|
|
* observable.inc(); // observers on "prop" are called
|
|
*
|
|
* Defining a dependency property:
|
|
* This is only supported when extending from Observable. You provide a list
|
|
* of the dependent properties and when each of them changes, the observsers
|
|
* on the defined property are called. Dependency properties are read-only and
|
|
* their values are determined by the specified getter.
|
|
*
|
|
* @example
|
|
* Observable.defineObservableProperty(ExtendedObservable.prototype, 'prop',
|
|
* {
|
|
* value: 10
|
|
* });
|
|
* Observable.defineObservableProperty(
|
|
* ExtendedObservable.prototype, 'prop2',
|
|
* {
|
|
* value: 20
|
|
* });
|
|
* Observable.defineObservableProperty(
|
|
* ExtendedObservable.prototype, 'dependencyProp',
|
|
* {
|
|
* dependency: ['prop', 'prop2'],
|
|
* get: function() {
|
|
* return this.prop + this.prop2;
|
|
* }
|
|
* });
|
|
* var observable = new ExtendedObservable();
|
|
* observable.prop = 100; // observers on "dependencyProp" are called
|
|
* observable.prop2 = 200; // observers on "dependencyProp" are called
|
|
* // observable.dependencyProp is 300.
|
|
*
|
|
* @module modules/mvvm/observable
|
|
*/
|
|
define(function(require) {
|
|
'use strict';
|
|
|
|
var Module = require('modules/base/module');
|
|
var DependencyGraph = require('modules/base/dependency_graph');
|
|
|
|
var OP_PREFIX = (name) => { return '$OP_' + name; };
|
|
|
|
/**
|
|
* @class Observable
|
|
* @requires module:modules/base/module
|
|
* @requires module:modules/base/dependency_graph
|
|
* @returns {Observable}
|
|
*/
|
|
var Observable = Module.create(function Observable(object) {
|
|
this._observers = {};
|
|
if (object) {
|
|
this._initWithObject(object);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Initialize the observable with a prototype object. This is not required
|
|
* for objects that are defined using prototype as everything should be
|
|
* defined via Observable.defineObservableProperty explicitly.
|
|
*
|
|
* @access private
|
|
* @memberOf Observable.prototype
|
|
* @param {Object} object
|
|
*/
|
|
Observable.prototype._initWithObject = function o_init(object) {
|
|
for (var name in object) {
|
|
// If name is a function, simply add it to the observable.
|
|
if (typeof object[name] === 'function') {
|
|
this[name] = object[name];
|
|
} else {
|
|
_defineObservableProperty(this, name, {
|
|
value: object[name]
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Notify the value change of a property.
|
|
*
|
|
* @access private
|
|
* @memberOf Observable.prototype
|
|
* @param {String} name
|
|
* @param {Object} newValue
|
|
* @param {Object} oldValue
|
|
*/
|
|
Observable.prototype._notify = function o__notify(name, newValue, oldValue) {
|
|
var observers = this._observers[name];
|
|
if (observers) {
|
|
observers.forEach(function(observer) {
|
|
observer(newValue, oldValue);
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove an observer from a property.
|
|
*
|
|
* @access private
|
|
* @memberOf Observable.prototype
|
|
* @param {Function} observer
|
|
* @param {String} name
|
|
*/
|
|
Observable.prototype._removeObserver =
|
|
function o__removeObserver(observer, name) {
|
|
// arguments in reverse order to support .bind(observer) for the
|
|
// unbind from all case
|
|
var observers = this._observers[name];
|
|
if (observers) {
|
|
var index = observers.indexOf(observer);
|
|
if (index >= 0) {
|
|
observers.splice(index, 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Observe a property with an observer. The observer is called when the
|
|
* property changes.
|
|
*
|
|
* @access public
|
|
* @memberOf Observable.prototype
|
|
* @param {String} name
|
|
* @param {Function} observer
|
|
*/
|
|
Observable.prototype.observe = function o_observe(name, observer) {
|
|
if (typeof observer !== 'function') {
|
|
return;
|
|
}
|
|
(this._observers[name] = this._observers[name] || []).push(observer);
|
|
};
|
|
|
|
/**
|
|
* Unobserve a property
|
|
*
|
|
* @access public
|
|
* @memberOf Observable.prototype
|
|
* @param {String} name
|
|
* @param {Function} observer
|
|
*/
|
|
Observable.prototype.unobserve = function o_unobserve(name, observer) {
|
|
if (typeof name === 'function') {
|
|
// (observer) -- remove from every key in _observers
|
|
Object.keys(this._observers).forEach(
|
|
this._removeObserver.bind(this, name));
|
|
} else {
|
|
if (observer) {
|
|
// (prop, observer) -- remove observer from the specific prop
|
|
this._removeObserver(observer, name);
|
|
} else if (name in this._observers) {
|
|
// (prop) -- otherwise remove all observers for property
|
|
this._observers[name] = [];
|
|
}
|
|
}
|
|
};
|
|
|
|
// Static functions
|
|
var _dependencyGraphs = new Map();
|
|
/**
|
|
* Each module should have its own dependency graph that decides what
|
|
* observers to called when a property changes. The function returns the
|
|
* dependency graph of a module. It creates one if the map does not exist.
|
|
*
|
|
* @param {Object} modulePrototype
|
|
*/
|
|
function _getDependencyGraph(modulePrototype) {
|
|
var dependencyGraph = _dependencyGraphs.get(modulePrototype);
|
|
if (!dependencyGraph) {
|
|
// register a new dependency graph of the module based on the existing
|
|
// dependency graph on the module.
|
|
dependencyGraph = DependencyGraph(modulePrototype._dependencyGraph);
|
|
modulePrototype._dependencyGraph = dependencyGraph;
|
|
_dependencyGraphs.set(modulePrototype, dependencyGraph);
|
|
}
|
|
return dependencyGraph;
|
|
}
|
|
|
|
/**
|
|
* The function helps query the values of all dependent properties of a
|
|
* specified property.
|
|
*
|
|
* @param {Observable} observable
|
|
* @param {String} sourceProperty
|
|
* The source property name.
|
|
*/
|
|
function _getAllDependentValues(observable, sourceProperty) {
|
|
var dependentList = observable._dependencyGraph &&
|
|
observable._dependencyGraph.getAllDependent(sourceProperty);
|
|
if (dependentList && dependentList.length) {
|
|
return dependentList.map((name) => {
|
|
return {
|
|
name: name,
|
|
value: observable[name]
|
|
};
|
|
});
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function _getterTemplate(name, defaultValue) {
|
|
return function() {
|
|
var value = this[OP_PREFIX(name)];
|
|
if (typeof value === 'undefined') {
|
|
value = this[OP_PREFIX(name)] = defaultValue;
|
|
}
|
|
return value;
|
|
};
|
|
}
|
|
|
|
function _setterTemplate(name) {
|
|
return function(value) {
|
|
var oldValue = this[name];
|
|
if (oldValue !== value) {
|
|
// cache the old values of all dependent
|
|
var dependentValues = _getAllDependentValues(this, name);
|
|
// change the value
|
|
this[OP_PREFIX(name)] = value;
|
|
// notify the changes
|
|
this._notify(name, value, oldValue);
|
|
if (dependentValues) {
|
|
dependentValues.forEach((obj) => {
|
|
this._notify(obj.name, this[obj.name], obj.value);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function _defineObservablePropertyCore(object, name, options) {
|
|
var dependency = options && options.dependency;
|
|
|
|
// Update dependency information
|
|
if (dependency) {
|
|
var dependencyGraph = _getDependencyGraph(object);
|
|
dependency.forEach((dependentName) => {
|
|
// name depends on dependentName
|
|
dependencyGraph.addDependency(name, dependentName);
|
|
});
|
|
}
|
|
|
|
Object.defineProperty(object, name, {
|
|
enumerable: true,
|
|
get: options.get,
|
|
set: options.set
|
|
});
|
|
}
|
|
|
|
function _defineObservableProperty(object, name, options) {
|
|
if (options && options.readonly) {
|
|
var internalName = '_' + name;
|
|
_defineObservablePropertyCore(object, name, {
|
|
dependency: [internalName],
|
|
get: function() {
|
|
return this[internalName];
|
|
}
|
|
});
|
|
_defineObservablePropertyCore(object, internalName, {
|
|
get: _getterTemplate(internalName, options && options.value),
|
|
set: _setterTemplate(internalName)
|
|
});
|
|
} else if (options && options.dependency && options.dependency.length) {
|
|
if (typeof options.get !== 'function') {
|
|
throw new Error('Observable: getter of ' + name + ' is invalid');
|
|
}
|
|
_defineObservablePropertyCore(object, name, {
|
|
dependency: options.dependency,
|
|
get: options.get
|
|
});
|
|
} else {
|
|
_defineObservablePropertyCore(object, name, {
|
|
get: _getterTemplate(name, options && options.value),
|
|
set: _setterTemplate(name)
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Observe a property with an observer. The observer is called when the
|
|
* property changes.
|
|
*
|
|
* @access public
|
|
* @memberOf Observable
|
|
* @param {Object} object
|
|
* @param {String} name
|
|
* @param {Object} options
|
|
* @param {Boolean} options.readonly
|
|
* Indicating if the property is read-only.
|
|
* @param {Array.<String>} options.dependency
|
|
* List of the dependent properties.
|
|
* @param {Function} options.get
|
|
* Getter of the property.
|
|
*/
|
|
Object.defineProperty(Observable, 'defineObservableProperty', {
|
|
get: function() {
|
|
return _defineObservableProperty;
|
|
}
|
|
});
|
|
return Observable;
|
|
});
|