diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..9160059 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +service_name: travis-ci diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..717d3de --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/test/reports diff --git a/.istanbul.yml b/.istanbul.yml new file mode 100644 index 0000000..66b1d55 --- /dev/null +++ b/.istanbul.yml @@ -0,0 +1,6 @@ +verbose: false +reporting: + print: summary + reports: + - lcov + dir: ./test/reports/coverage diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..c58d0fb --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +/test/reports diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7f4d319 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: node_js +node_js: + - "0.11" + - "0.10" + +script: make test-ci diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67362ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 David Rekow. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..121621f --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +# panoptic makefile +# author: David Rekow +# copyright: David Rekow 2014 + + +SHELL := /bin/bash + +# vars +THIS_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +.PHONY: build clean distclean test test-ci + +all: build + +build: test + @echo "Building panoptic..." + @mkdir -p $(THIS_DIR)/dist + + @echo "Compiling dist package with closure compiler..." + @-java -jar $(THIS_DIR)/node_modules/closure-compiler-stream/lib/compiler.jar \ + --debug false \ + --warning_level QUIET \ + --summary_detail_level 1 \ + --js $(THIS_DIR)/index.js \ + --language_in ECMASCRIPT5 \ + --formatting PRETTY_PRINT \ + --compilation_level WHITESPACE_ONLY \ + --js_output_file $(THIS_DIR)/dist/panoptic.js \ + --common_js_entry_module $(THIS_DIR)/index.js \ + --output_wrapper '(function () {%output%}).call(this);' + + @echo "Minifying dist package with closure compiler..." + @-java -jar $(THIS_DIR)/node_modules/closure-compiler-stream/lib/compiler.jar \ + --debug false \ + --warning_level QUIET \ + --summary_detail_level 1 \ + --js $(THIS_DIR)/index.js \ + --language_in ECMASCRIPT5 \ + --compilation_level ADVANCED_OPTIMIZATIONS \ + --common_js_entry_module $(THIS_DIR)/index.js \ + --js_output_file $(THIS_DIR)/dist/panoptic.min.js \ + --output_wrapper '(function () {%output%}).call(this);' \ + --use_types_for_optimization + + @echo "Build complete." + +clean: + @echo "Cleaning built files..." + @-rm -rf $(THIS_DIR)/dist + + @echo "Cleaning test reports..." + @-rm -rf $(THIS_DIR)/test/reports + +distclean: clean + @echo "Cleaning downloaded dependencies..." + @-rm -rf $(THIS_DIR)/node_modules + +test: $(THIS_DIR)/node_modules + @echo "Running panoptic package tests..." + @multi="xunit=test/reports/xunit.xml spec=-" \ + $(THIS_DIR)/node_modules/.bin/istanbul cover $(THIS_DIR)/node_modules/.bin/_mocha -- -R mocha-multi + +test-ci: test + @echo "Reporting coverage to coveralls..." + @cat $(THIS_DIR)/test/reports/coverage/lcov.info | $(THIS_DIR)/node_modules/.bin/coveralls + +$(THIS_DIR)/node_modules: + @echo "Installing NPM build dependencies..." + @npm install diff --git a/README.md b/README.md new file mode 100644 index 0000000..505de07 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +#panoptic +simple object keypath observers. + +[![Build Status](https://travis-ci.org/davidrekow/panoptic.svg?branch=master)](https://travis-ci.org/davidrekow/panoptic) [![Coverage Status](https://coveralls.io/repos/davidrekow/panoptic/badge.png?branch=master)](https://coveralls.io/r/davidrekow/panoptic?branch=master) + +##Installation +###node +Add to your `dependencies` in `package.json`: +```javascript + ... + "dependencies": { + "panoptic": "~0.0.1", + ... + }, + ... +``` +or install directly: +```sh +npm install --save panoptic +``` +###browser +Include either of the bundles in the `dist/` folder of the [repo](https://github.com/davidrekow/panoptic), +depending on whether or not you'd like it minified. + +##Usage +Import the `panoptic` module: +```javascript +var panoptic = require('panoptic'); +``` +then call with an object you want to observe: +```javascript +var data = { + name: 'Cool Person', + age: 25 +}; + +var observable = panoptic(data); +``` +###getting +```javascript +value = observable.get('key'); +value = observable.key; +``` +Retrieve the value for a particular key via getter or object property access. +If you're not sure whether the key exists before accessing, use `get()` to +avoid a TypeError when retrieving deeply nested values: +```javascript +value = observable.get('a.b.c'); // if a or b doesn't exist, returns null +value = observable.a.b.c; // if a or b doesn't exist, throws TypeError +``` +###setting +```javascript +observable.set('key', value); +observable.key = value; +``` +Set the value of a key in the same way. If setting a deeply nested key and you +don't know whether intermediate objects exist, use `set()` and they will be +created (think `mkdir -p`): +```javascript +observable.set('a.b.c', value); // if a or b doesn't exist they will be created +observable.a.b.c = value; // if a or b doesn't exist, throws TypeError +``` +###watching +```javascript +observable.watch('a.b.c', function (newValue) { + var value = this.get('c'); // 'this' is the object the value is retrieved from + value === newValue; // watcher receives the new value after it's set +}); +``` +###unwatching +```javascript +var watcher = function (newValue) { ... }; + +observable.watch('key', watcher); +observable.unwatch('key', watcher); // if watcher is passed, only it gets removed +observable.unwatch('key'); // if no watcher is passed, all get removed +``` +That's it! + +##FAQ +###why panoptic? +it's super lightweight, matches whatever data access syntax you're currently +using, and uses a simple but powerful sparse-tree data structure to avoid the +overhead of propagation and digest cycles when dispatching change events. + +###why `panoptic`? +panopticon (the less morbid connotations), also *pan* (all) + *opt* (option?) + +Find a bug? Please [file an issue](https://github.com/davidrekow/panoptic/issues)! diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..92b26af --- /dev/null +++ b/bower.json @@ -0,0 +1,39 @@ +{ + "name": "panoptic", + "description": "see all, do all. simple object keypath observers.", + "version": "0.0.1", + "license": "MIT", + "authors": [{ + "name": "David Rekow", + "email": "d@davidrekow.com", + "url": "http://davidrekow.com" + }], + "keywords": [ + "panoptic", + "observe", + "observer", + "observers", + "observe object", + "watch", + "watcher", + "watchers", + "watch object", + "watch property", + "data", + "bind", + "databind", + "data bind", + "databinding", + "data binding", + "property binding", + "bind data", + "bind property" + ], + "homepage": "https://github.com/davidrekow/panoptic", + "repository": { + "type": "git", + "url": "https://github.com/davidrekow/panoptic.git" + }, + "main": "index.js", + "ignore": ["test/reports"] +} \ No newline at end of file diff --git a/dist/panoptic.js b/dist/panoptic.js new file mode 100644 index 0000000..5e787df --- /dev/null +++ b/dist/panoptic.js @@ -0,0 +1,106 @@ +(function () {/* + David Rekow 2014 +*/ +function Observable(data, root, path) { + var k; + if (root) { + Observable.setRoot(this, root, path); + } + this._prop = {}; + this._cb = {}; + for (k in data) { + if (data.hasOwnProperty(k) && typeof data[k] !== "function") { + this.bind(k, data[k]); + } + } +} +Observable.prototype = {get:function(key) { + return Observable.resolve(this, key); +}, set:function(key, value) { + Observable.resolve(this, key, value); +}, watch:function(key, observer) { + if (this._root) { + return this._root.watch(this._path + "." + key, observer); + } + if (!this._cb[key]) { + this._cb[key] = []; + } + this._cb[key].push(observer); +}, unwatch:function(key, observer) { + var i; + if (this._root) { + return this._root.unwatch(this._path + "." + key, observer); + } + if (!this._cb[key]) { + return; + } + if (!observer) { + this._cb[key] = []; + return; + } + i = this._cb[key].indexOf(observer); + if (i > -1) { + this._cb[key].splice(i, 1); + } +}, toJSON:function() { + return this._prop; +}, _root:null, _path:null, constructor:Observable, emit:function(key, value, observed) { + var observers; + if (this._root) { + return this._root.emit(this._path + "." + key, value, observed); + } + observers = this._cb[key]; + if (!observers) { + return; + } + observers.forEach(function(observer) { + observer.call(observed, value); + }); +}, bind:function(key, value) { + Object.defineProperty(this, key, {enumerable:true, get:function() { + return this._prop[key]; + }, set:function(value) { + this._prop[key] = value; + this.emit(key, value, this); + }}); + this.set(key, value); +}}; +Observable.resolve = function(observed, key, value) { + var path = key.split("."), pathname = path.pop(), fullpath = observed._path || "", root = observed._root || observed, _path, i; + for (i = 0;i < path.length;i++) { + _path = path[i]; + fullpath += "." + _path; + if (!observed.hasOwnProperty(_path)) { + if (value === undefined || value === null) { + return null; + } + observed.bind(_path, new Observable(null, root, fullpath)); + } + observed = observed[_path]; + } + fullpath += fullpath ? "." + pathname : pathname; + if (value === undefined) { + return observed[pathname]; + } + if (typeof value === "object" && !(value instanceof Observable || value instanceof Array)) { + value = new Observable(value, root, fullpath); + } + if (!observed.hasOwnProperty(pathname)) { + observed.bind(pathname); + } + observed[pathname] = value; +}; +Observable.setRoot = function(observed, root, path) { + Object.defineProperty(observed, "_root", {value:root}); + Object.defineProperty(observed, "_path", {value:path}); +}; +this.panoptic = function(data) { + return new Observable(data); +}; +if (typeof module !== "undefined" && typeof module["exports"] === "object") { + module.exports = this.panoptic; +} +if (typeof define === "function") { + define("panoptic", this.panoptic); +} +;}).call(this); diff --git a/dist/panoptic.min.js b/dist/panoptic.min.js new file mode 100644 index 0000000..99c3dbb --- /dev/null +++ b/dist/panoptic.min.js @@ -0,0 +1,8 @@ +(function () {/* + David Rekow 2014 +*/ +function e(a,b,c){var d;b&&(Object.defineProperty(this,"_root",{value:b}),Object.defineProperty(this,"_path",{value:c}));this._prop={};this._cb={};for(d in a)a.hasOwnProperty(d)&&"function"!==typeof a[d]&&this.bind(d,a[d])} +e.prototype={get:function(a){return k(this,a)},set:function(a,b){k(this,a,b)},watch:function(a,b){if(this._root)return this._root.watch(this._path+"."+a,b);this._cb[a]||(this._cb[a]=[]);this._cb[a].push(b)},unwatch:function(a,b){var c;if(this._root)return this._root.unwatch(this._path+"."+a,b);this._cb[a]&&(b?(c=this._cb[a].indexOf(b),-1 + * @copyright (c) David Rekow 2014 + * @externs + */ + +/** + * @typedef {Object} + */ +var Observable; + +/** + * @param {string} key + * @return {*} + */ +Observable.prototype.get = function (key) {}; + +/** + * @param {string} key + * @param {*} value + */ +Observable.prototype.set = function (key, value) {}; + +/** + * @param {string} key + * @param {function(this:Observable, *)} observer + */ +Observable.prototype.watch = function (key, observer) {}; + +/** + * @param {string} key + * @param {function(this:Observable, *)=} observer + */ +Observable.prototype.unwatch = function (key, observer) {}; + +/** + * @return {string} + */ +Observable.prototype.toJSON = function () {}; + +/** + * @typedef {function(*): Observable} + */ +var panoptic; diff --git a/index.js b/index.js new file mode 100644 index 0000000..1bd26a0 --- /dev/null +++ b/index.js @@ -0,0 +1,280 @@ +/** + * @file Main panoptic module. + * @author David Rekow + * @copyright David Rekow 2014 + */ + +/** + * Observable proxy for an object. + * @constructor + * @param {*} data Object to watch for changes. + * @param {Observable=} root Root Observable object in this branch. + * @param {string=} path Path relative to root. + */ +function Observable (data, root, path) { + var k; + + if (root) + Observable.setRoot(this, root, path); + + /** + * @expose + * @private + * @type {Object.} + */ + this._prop = {}; + + /** + * @expose + * @private + * @type {Object.>} + */ + this._cb = {}; + + for (k in data) { + if (data.hasOwnProperty(k) && typeof data[k] !== 'function') + this.bind(k, data[k]); + } +} + +Observable.prototype = { + /** + * Returns a value by key. + * @expose + * @param {string} key + * @return {*} + */ + get: function (key) { + return Observable.resolve(this, key); + }, + + /** + * Sets a value by key. + * @expose + * @param {string} key + * @param {*} value + */ + set: function (key, value) { + Observable.resolve(this, key, value); + }, + + /** + * Watches an observable object for changes on a key. + * @expose + * @param {string} key + * @param {function(this:Observable, *)} observer + */ + watch: function (key, observer) { + if (this._root) + return this._root.watch(this._path + '.' + key, observer); + + if (!this._cb[key]) + this._cb[key] = []; + + this._cb[key].push(observer); + }, + + /** + * Removes an existing watcher for changes on a key. + * @expose + * @param {string} key + * @param {function(this:Observable, *)=} observer + */ + unwatch: function (key, observer) { + var i; + + if (this._root) + return this._root.unwatch(this._path + '.' + key, observer); + + if (!this._cb[key]) + return; + + if (!observer) { + this._cb[key] = []; + return; + } + + i = this._cb[key].indexOf(observer); + + if (i > -1) + this._cb[key].splice(i, 1); + }, + + /** + * @expose + * @return {Object} + */ + toJSON: function () { + return this._prop; + }, + + /** + * @expose + * @private + * @type {?Observable} + */ + _root: null, + + /** + * @expose + * @private + * @type {?string} + */ + _path: null, + + /** + * @expose + * @private + * @const + */ + constructor: Observable, + + /** + * Emits a change event for a particular key. + * @private + * @param {string} key + * @param {*} value + * @param {Observable} observed + */ + emit: function (key, value, observed) { + var observers; + + if (this._root) + return this._root.emit(this._path + '.' + key, value, observed); + + observers = this._cb[key]; + + if (!observers) + return; + + observers.forEach(function (observer) { + observer.call(observed, value); + }); + }, + + /** + * Binds observation to a particular key path and value. + * @private + * @param {string} key + * @param {*} value + */ + bind: function (key, value) { + Object.defineProperty(this, key, { + /** + * @expose + * @type {boolean} + */ + enumerable: true, + + /** + * @expose + * @this {Observable} + * @return {*} + */ + get: function () { + return this._prop[key]; + }, + + /** + * @expose + * @this {Observable} + * @param {*} value + */ + set: function (value) { + this._prop[key] = value; + this.emit(key, value, this); + } + }); + + this.set(key, value); + } +}; + +/** + * Resolves a value on an Observable object by string key and gets or sets it. + * @private + * @static + * @param {Observable} observed + * @param {string} key + * @param {*=} value + * @return {*} + */ +Observable.resolve = function (observed, key, value) { + var path = key.split('.'), + pathname = path.pop(), + fullpath = observed._path || '', + root = observed._root || observed, + _path, i; + + for (i = 0; i < path.length; i++) { + _path = path[i]; + + fullpath += '.' + _path; + + if (!observed.hasOwnProperty(_path)) { + if (value === undefined || value === null) + return null; + + observed.bind(_path, new Observable(null, root, fullpath)); + } + + observed = observed[_path]; + } + + fullpath += fullpath ? '.' + pathname : pathname; + + if (value === undefined) + return observed[pathname]; + + if (typeof value === 'object' && !(value instanceof Observable || value instanceof Array)) + value = new Observable(value, root, fullpath); + + if (!observed.hasOwnProperty(pathname)) + observed.bind(pathname); + + observed[pathname] = value; +}; + +/** + * Sets a specific Observable object as the root for another. + * @private + * @static + * @param {Observable} observed + * @param {Observable} root + * @param {string} path + */ +Observable.setRoot = function (observed, root, path) { + Object.defineProperty(observed, '_root', { + /** + * @expose + * @type {Observable} + */ + value: root + }); + + Object.defineProperty(observed, '_path', { + /** + * @expose + * @type {string} + */ + value: path + }); +}; + +/** + * Return a new Observable object. + * @expose + * @param {*} data + * @return {Observable} + */ +this.panoptic = function (data) { + return new Observable(data); +}; + +if (typeof module !== 'undefined' && typeof module['exports'] === 'object') { + /** @expose */ + module.exports = this.panoptic; +} + +if (typeof define === 'function') + define('panoptic', this.panoptic); diff --git a/package.json b/package.json new file mode 100644 index 0000000..eff464d --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "panoptic", + "description": "see all, do all. simple object keypath observers.", + "version": "0.0.1", + "license": "MIT", + "author": { + "name": "David Rekow", + "email": "d@davidrekow.com", + "url": "http://davidrekow.com" + }, + "keywords": [ + "panoptic", + "observe", + "observer", + "observers", + "observe object", + "watch", + "watcher", + "watchers", + "watch object", + "watch property", + "data", + "bind", + "databind", + "data bind", + "databinding", + "data binding", + "property binding", + "bind data", + "bind property" + ], + "homepage": "https://github.com/davidrekow/panoptic", + "bugs": "https://github.com/davidrekow/panoptic/issues", + "repository": { + "type": "git", + "url": "https://github.com/davidrekow/panoptic.git" + }, + "main": "index.js", + "scripts": { + "test": "make test" + }, + "engines": { + "node": "0.10.x" + }, + "devDependencies": { + "mocha": "<2.0.0", + "mocha-multi": "~0.4.1", + "istanbul": "~0.3.2", + "coveralls": "~2.11.2", + "closure-compiler-stream": "~0.1.14" + } +} diff --git a/test/spec.js b/test/spec.js new file mode 100644 index 0000000..f2a4024 --- /dev/null +++ b/test/spec.js @@ -0,0 +1,331 @@ +/** + * @file Package test specs for panoptic. + * @author David Rekow + * @copyright David Rekow 2014 + */ + +var assert = require("assert"), + panoptic = require("../index.js"), + equal = assert.equal; + +describe("Panopt module", function () { + + var data, + observed; + + beforeEach(function () { + data = { + a: 1, + b: { + c: 2, + d: "3", + e: [4], + f: { + g: "5" + }, + h: 6 + } + }; + + observed = panoptic(data); + }); + + afterEach(function () { + data = observed = null; + }); + + it("instantiates an observable object proxy, also proxying subobjects", function () { + equal(observed.constructor.name, "Observable", "observed object's constructor is Observable"); + equal(observed.b.constructor.name, "Observable", "nested observed object is also Observable"); + equal(observed.b.f.constructor.name, "Observable", "deeply nested observed object is also Observable"); + }); + + it("only observes and proxies instance properties", function () { + var ct = function () { this.b = 2; }; + ct.prototype.a = 1; + observed = panoptic(new ct()); + equal(observed.a, undefined, "prototype.a is undefined on observable proxy"); + equal(observed.b, 2, "b is 2"); + }) + + it("resolves object properties", function () { + equal(observed.a, 1, "a is 1"); + equal(observed.b.c, 2, "b.c is 2"); + equal(observed.b.d, "3", "b.d is '3'"); + equal(observed.b.e[0], 4, "b.e[0] is 4"); + equal(observed.b.f.g, "5", "b.f.g is '5'"); + equal(observed.b.h, 6, "b.h is 6"); + equal(observed.b.i, undefined, "b.i is undefined"); + }); + + it("resolves nested namespaces from the root", function () { + equal(observed.get("a"), 1, "a is 1"); + equal(observed.get("b.c"), 2, "b.c is 2"); + equal(observed.get("b.d"), "3", "b.d is '3'"); + equal(observed.get("b.e")[0], 4, "b.e[0] is 4"); + equal(observed.get("b.f.g"), "5", "b.f.g is '5'"); + equal(observed.get("b.h"), 6, "b.h is 6"); + equal(observed.get("i.j.k"), undefined, "i.j.k is undefined"); + }); + + it("sets object properties", function () { + observed.a = 2; + observed.b.c = 3; + observed.b.d = "4"; + observed.b.e = [5]; + observed.b.f.g = "6"; + observed.b.h = 7; + equal(observed.get("a"), 2, "updated a is 2"); + equal(observed.get("b.c"), 3, "updated b.c is 3"); + equal(observed.get("b.d"), "4", "updated b.d is '4'"); + equal(observed.get("b.e")[0], 5, "updated b.e[0] is 5"); + equal(observed.get("b.f.g"), "6", "updated b.f.g is '6'"); + equal(observed.get("b.h"), 7, "updated b.h is 7"); + }); + + it("sets nested namespaces from the root", function () { + observed.set("a", 2); + observed.set("b.c", 3); + observed.set("b.d", "4"); + observed.set("b.e", [5]); + observed.set("b.f.g", "6"); + observed.set("b.h", 7); + observed.set("i.j.k", 8); + equal(observed.a, 2, "updated a is 2"); + equal(observed.b.c, 3, "updated b.c is 3"); + equal(observed.b.d, "4", "updated b.d is '4'"); + equal(observed.b.e[0], 5, "updated b.e[0] is 5"); + equal(observed.b.f.g, "6", "updated b.f.g is '6'"); + equal(observed.b.h, 7, "updated b.h is 7"); + equal(observed.i.j.k, 8, "new i.j.k is 8"); + }); + + it("watches existing observable object properties", function () { + var updated = 0; + + observed.watch('a', function (v) { + equal(v, 2, "updated a is 2"); + updated++; + }); + observed.b.watch('c', function (v) { + equal(v, 3, "updated b.c is 3"); + updated++; + }); + observed.b.watch('d', function (v) { + equal(v, "4", "updated b.d is '4'"); + updated++; + }); + observed.b.watch('e', function (v) { + equal(v[0], 5, "updated b.e[0] is 5"); + updated++; + }); + observed.b.f.watch('g', function (v) { + equal(v, "6", "updated b.f.g is '6'"); + updated++; + }); + observed.b.watch('h', function (v) { + equal(v, 7, "updated b.h is 7"); + updated++; + }); + + observed.a = 2; + observed.b.c = 3; + observed.b.d = "4"; + observed.b.e = [5]; + observed.b.f.g = "6"; + observed.b.h = 7; + + equal(updated, 6, "all watchers were called"); + }); + + it("watches existing and future nested namespaces from the root", function () { + var updated = 0; + + observed.watch('a', function (v) { + equal(v, 2, "updated a is 2"); + updated++; + }); + observed.watch('b.c', function (v) { + equal(v, 3, "updated b.c is 3"); + updated++; + }); + observed.watch('b.d', function (v) { + equal(v, "4", "updated b.d is '4'"); + updated++; + }); + observed.watch('b.e', function (v) { + equal(v[0], 5, "updated b.e[0] is 5"); + updated++; + }); + observed.watch('b.f.g', function (v) { + equal(v, "6", "updated b.f.g is '6'"); + updated++; + }); + observed.watch('b.h', function (v) { + equal(v, 7, "updated b.h is 7"); + updated++; + }); + + observed.watch('b.f.i', function (v) { + equal(v, 8, "new b.f.i is 8"); + updated++; + }); + + observed.watch('b.j', function (v) { + equal(v, "9", "new b.j is '9'"); + updated++; + }); + + observed.watch('b.j', function (v) { + equal(v, "9", "updated b.j is '9'"); + }); + + observed.set("a", 2); + observed.set("b.c", 3); + observed.set("b.d", "4"); + observed.set("b.e", [5]); + observed.set("b.f.g", "6"); + observed.set("b.h", 7); + observed.set("b.f.i", 8); + observed.set("b.j", "9"); + + equal(updated, 8, "all watchers were called"); + }); + + it("removes watchers on existing observable object properties", function () { + var updated = 0, + aWatcher = function (v) { + equal(v, 2, "updated a is 2"); + updated++; + }, + cWatcher = function (v) { + equal(v, 3, "updated b.c is 3"); + updated++; + }, + dWatcher = function (v) { + equal(v, "4", "updated b.d is '4'"); + updated++; + }, + eWatcher = function (v) { + equal(v[0], 5, "updated b.e[0] is 5"); + updated++; + }, + gWatcher = function (v) { + equal(v, "6", "updated b.f.g is '6'"); + updated++; + }, + hWatcher = function (v) { + equal(v, 7, "updated b.h is 7"); + updated++; + }; + + observed.watch('a', aWatcher); + observed.b.watch('c', cWatcher); + observed.b.watch('d', dWatcher); + observed.b.watch('e', eWatcher); + observed.b.f.watch('g', gWatcher); + observed.b.watch('h', hWatcher); + + observed.a = 2; + observed.b.c = 3; + observed.b.d = "4"; + observed.b.e = [5]; + observed.b.f.g = "6"; + observed.b.h = 7; + + equal(updated, 6, "all watchers were called"); + + observed.unwatch('a', aWatcher); + observed.b.unwatch('c', cWatcher); + observed.b.unwatch('d', dWatcher); + observed.b.unwatch('e', eWatcher); + observed.b.f.unwatch('g', gWatcher); + observed.b.unwatch('h', hWatcher); + observed.b.unwatch('h', function () {}); + + observed.a = 1; + observed.b.c = 2; + observed.b.d = "3"; + observed.b.e = [4]; + observed.b.f.g = "5"; + observed.b.h = 6; + + equal(updated, 6, "no further watchers were called"); + + }); + + it("removes watchers on nested namespaces from the root", function () { + var updated = 0, + aWatcher = function (v) { + equal(v, 2, "updated a is 2"); + updated++; + }, + cWatcher = function (v) { + equal(v, 3, "updated b.c is 3"); + updated++; + }, + dWatcher = function (v) { + equal(v, "4", "updated b.d is '4'"); + updated++; + }, + eWatcher = function (v) { + equal(v[0], 5, "updated b.e[0] is 5"); + updated++; + }, + gWatcher = function (v) { + equal(v, "6", "updated b.f.g is '6'"); + updated++; + }, + iWatcher = function (v) { + equal(v, 8, "new b.f.i is 8"); + updated++; + }, + jWatcher = function (v) { + equal(v, "9", "new b.j is '9'"); + updated++; + }; + + observed.watch('a', aWatcher); + observed.watch('b.c', cWatcher); + observed.watch('b.d', dWatcher); + observed.watch('b.e', eWatcher); + observed.watch('b.f.g', gWatcher); + observed.watch('b.f.i', iWatcher); + observed.watch('b.j', jWatcher); + + observed.set("a", 2); + observed.set("b.c", 3); + observed.set("b.d", "4"); + observed.set("b.e", [5]); + observed.set("b.f.g", "6"); + observed.set("b.h", 7); + observed.set("b.f.i", 8); + observed.set("b.j", "9"); + + equal(updated, 7, "all watchers were called"); + + observed.unwatch('a', aWatcher); + observed.unwatch('b.c', cWatcher); + observed.unwatch('b.d', dWatcher); + observed.unwatch('b.e', eWatcher); + observed.unwatch('b.f.g', gWatcher); + observed.unwatch('b.h'); + observed.unwatch('b.f.i', iWatcher); + observed.unwatch('b.j'); + + observed.set("a", 1); + observed.set("b.c", 2); + observed.set("b.d", "3"); + observed.set("b.e", [4]); + observed.set("b.f.g", "5"); + observed.set("b.h", 6); + observed.set("b.f.i", 7); + observed.set("b.j", "8"); + + equal(updated, 7, "no further watchers were called"); + }); + + it("serializes to JSON", function () { + equal(JSON.stringify(observed), JSON.stringify(data), "JSON strings are equal"); + }); +}); \ No newline at end of file