/** * 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.} 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; });