diff --git a/builder.json b/builder.json index ceb7040389c..2b19fdf10ec 100644 --- a/builder.json +++ b/builder.json @@ -15,12 +15,12 @@ "isDefault": true }, "can/observe": { - "name": "can.Observe", - "description": "Observable key-value bindings", + "name": "can.Map, can.List, can.compute", + "description": "Observables and key-value bindings", "type": "core", "isDefault": true }, - "can/observe/compute": { + "can/compute": { "name": "can.compute", "description": "can.compute lets you make observable values", "type": "core", @@ -67,7 +67,7 @@ "type": "plugin", "description": "Live-binding Handlebars and Mustache views" }, - "can/view/mustache/bindings": { + "can/view/bindings": { "name": "can.view.bindings", "type": "plugin", "description": "Two way bindings and in-template declarative event handlers." @@ -92,27 +92,27 @@ "type": "plugin", "description": "Proxy construct methods" }, - "can/observe/delegate": { + "can/map/delegate": { "name": "can.Observe.delegate", "type": "plugin", "description": "Listen to Observe attributes" }, - "can/observe/setter": { + "can/map/setter": { "name": "can.Observe.setter", "type": "plugin", "description": "Use setter methods on Observes" }, - "can/observe/attributes": { + "can/map/attributes": { "name": "can.Observe.attributes", "type": "plugin", "description": "Define Observe attributes" }, - "can/observe/validations": { + "can/map/validations": { "name": "can.Observe.validations", "type": "plugin", "description": "Validate Observe attributes" }, - "can/observe/backup": { + "can/map/backup": { "name": "can.Observe.backup", "type": "plugin", "description": "Backup and restore an Observes state" diff --git a/can.md b/can.md index dc88d17f7fa..2f44e421754 100644 --- a/can.md +++ b/can.md @@ -15,7 +15,7 @@ framework: - [can.Construct] - inheritable constructor functions - [can.Control] - declarative event bindings - - [can.Observe], [can.Observe.List], [can.compute] - observable objects, list, and values. + - [can.Map], [can.List], [can.compute] - observable objects, list, and values. - [can.Model] - observes connected to a RESTful JSON interface - [can.view] - template loading, caching, rendering - [can.EJS] - live binding templates @@ -26,11 +26,11 @@ The following modules are typically distributed as plugins: - [can.Mustache] - Live binding Handlebars and Mustache templates - [can.Construct.proxy] - Proxy construct methods - [can.Construct.super] - Call super methods - - [can.Observe.delegate] - Listen to Observe attributes - - [can.Observe.setter] - Use setter methods on Observes - - [can.Observe.attributes] - Define Observe attributes - - [can.Observe.validations] - Validate attributes - - [can.Observe.backup] - Backup and restore an Observe's state + - [can.Map.delegate] - Listen to Observe attributes + - [can.Map.setter] - Use setter methods on Observes + - [can.Map.attributes] - Define Observe attributes + - [can.Map.validations] - Validate attributes + - [can.Map.backup] - Backup and restore an Observe's state - [can.Control.plugin] - Registers a jQuery plugin function for Controls[1] - [can.view.modifiers View modifiers] - Use jQuery modifiers to render views[1] diff --git a/changelog.md b/changelog.md index 4241961c82a..9475b7817bf 100644 --- a/changelog.md +++ b/changelog.md @@ -15,7 +15,7 @@ __1.1.6__ ( May 30 2013 ) - change: [Binding to an Observe.compute value is broken](https://github.com/bitovi/canjs/issues/372) - change: [Added validatesNumericalityOf to validations](https://github.com/bitovi/canjs/issues/370) - change: [Include can.Object in CanJS distribution](https://github.com/bitovi/canjs/issues/368) -- change: [can.Observe makes can.Deferred into an observable](https://github.com/bitovi/canjs/issues/367) +- change: [can.Map makes can.Deferred into an observable](https://github.com/bitovi/canjs/issues/367) - change: [Update sub to not break when str is undefined](https://github.com/bitovi/canjs/issues/365) - change: [List memory leak fix](https://github.com/bitovi/canjs/issues/363) - change: [Integrated incremental live lists](https://github.com/bitovi/canjs/issues/361) @@ -65,7 +65,7 @@ __1.1.5__ ( Mar 28 2013 ) - change: [fixing computes from converting type](https://github.com/bitovi/canjs/issues/278) - change: [can.view with Deferreds doesn't pass failures](https://github.com/bitovi/canjs/issues/276) - change: [HTML comments trip EJS rendering](https://github.com/bitovi/canjs/issues/271) -- change: [can.Observe.prototype.each overrides {{#each}} Mustache helper.](https://github.com/bitovi/canjs/issues/258) +- change: [can.Map.prototype.each overrides {{#each}} Mustache helper.](https://github.com/bitovi/canjs/issues/258) - change: [Any model with a "." in the key name will cause observe.js _set() to throw and error](https://github.com/bitovi/canjs/issues/257) - change: [Item.List splice method does not convert inserted elements to Item type](https://github.com/bitovi/canjs/issues/253) - change: [Mustache: DOM exception when applying certain block patterns](https://github.com/bitovi/canjs/issues/243) @@ -93,7 +93,7 @@ __1.1.4__ ( February 5, 2013 ) - fix: [Observe.List sort doesn't use custom method passed](https://github.com/bitovi/canjs/issues/169) - fix: [test&fix: null values crashing validations](https://github.com/bitovi/canjs/pull/145) - fix: [EJS rendering null value](https://github.com/bitovi/canjs/pull/118) -- fix: [can.Observe sort plugin doesn't trigger add events](https://github.com/bitovi/canjs/issues/205) +- fix: [can.Map sort plugin doesn't trigger add events](https://github.com/bitovi/canjs/issues/205) - fix: [Observe.List sort plugin erroring on item removal](https://github.com/bitovi/canjs/pull/88) - fix: [Live binding on observe.lists nested in an observe doesn't work](https://github.com/bitovi/canjs/issues/204) - fix: [Observe.List sort doesn't use custom method passed](https://github.com/bitovi/canjs/issues/169) @@ -114,7 +114,7 @@ __1.1.3__ ( December 11, 2012 ) - fix: [compute only updates once when a list's contents are replaced](https://github.com/bitovi/canjs/commit/9cb47dfabba5dbe3bef161e6aae4a5ce2965ac49) - add: [Updated jQuery hashchange plugin](https://github.com/bitovi/canjs/pull/201) - add: [Generate computes from an observe property](https://github.com/bitovi/canjs/issues/203) -- add: [Add can.Observe.List.prototype.replace](https://github.com/bitovi/canjs/issues/194) +- add: [Add can.List.prototype.replace](https://github.com/bitovi/canjs/issues/194) - add: [Return resolved data models in view callback](https://github.com/bitovi/canjs/issues/1log 83) @@ -161,18 +161,18 @@ __1.1.0__ ( November 13, 2012 ) - can.Control - add: [control does not listen to touchmove event on controller itself](https://github.com/bitovi/canjs/issues/104) - - can.Observe + - can.Map - add: [List binding on .length of an object](https://github.com/bitovi/canjs/issues/142) - fix: [validation error that incorrectly labels an attribute with a value of 0 as empty](https://github.com/bitovi/canjs/pull/132) - add: [you can now pluralise event names to listen to matching events of that type (rather than firing a single event)](https://github.com/bitovi/canjs/issues/122) - add: [compound sets now behave correctly](https://github.com/bitovi/canjs/issues/119) - - fix: [can.Observe.delegate sets wrong event.currentTarget](https://github.com/bitovi/canjs/issues/123) - - add: [ability to assign object as attribute type in can.Observe](https://github.com/bitovi/canjs/issues/107) + - fix: [can.Map.delegate sets wrong event.currentTarget](https://github.com/bitovi/canjs/issues/123) + - add: [ability to assign object as attribute type in can.Map](https://github.com/bitovi/canjs/issues/107) - can.Model - fix: [can.Model with attributes that are models gets corrupted when you call attr()](https://github.com/bitovi/canjs/pull/141) - add: [missing dependency to can/model](https://github.com/bitovi/canjs/pull/140) - - Moved can/model/elements to can/observe/elements and renamed `models` to `instances` + - Moved can/model/elements to can/map/elements and renamed `models` to `instances` - fix: [can.Model.List doesn't fire the change event on the expando properties ](https://github.com/bitovi/canjs/issues/129) __1.0.7__ (June 25nd 2012) diff --git a/component/component.js b/component/component.js index 09edc14e9ff..256ab1c7b05 100644 --- a/component/component.js +++ b/component/component.js @@ -1,4 +1,4 @@ -steal("can/util","can/control","can/observe","can/view/mustache","can/view/mustache/bindings",function(can){ +steal("can/util","can/control","can/observe","can/view/mustache","can/view/bindings",function(can){ var ignoreAttributesRegExp = /data-view-id|class|id/i /** diff --git a/component/component_test.js b/component/component_test.js index 1edd54bbf79..c4470545b91 100644 --- a/component/component_test.js +++ b/component/component_test.js @@ -74,7 +74,7 @@ test("basic tabs",function(){ }, removePanel: function(panel){ var panels = this.attr("panels"); - can.Map.startBatch(); + can.batch.start(); panels.splice(panels.indexOf(panel),1); if(panel === this.attr("active")){ if(panels.length){ @@ -83,7 +83,7 @@ test("basic tabs",function(){ this.removeAttr("active") } } - can.Map.stopBatch() + can.batch.stop() }, makeActive: function(panel){ this.attr("active",panel); diff --git a/component/examples/1.html b/component/examples/1.html index 2ccfd8e5f22..8f00fbf9903 100644 --- a/component/examples/1.html +++ b/component/examples/1.html @@ -10,7 +10,7 @@ - + + diff --git a/construct/proxy/proxy.js b/construct/proxy/proxy.js index b70b07f11b7..b2886480a2a 100644 --- a/construct/proxy/proxy.js +++ b/construct/proxy/proxy.js @@ -58,7 +58,7 @@ proxy = function( funcs ) { } can.Construct.proxy = can.Construct.prototype.proxy = proxy; // this corrects the case where can/control loads after can/construct/proxy, so static props don't have proxy - var correctedClasses = [can.Observe, can.Control, can.Model], + var correctedClasses = [can.Map, can.Control, can.Model], i = 0; for (; i < correctedClasses.length; i++ ) { if(correctedClasses[i]){ diff --git a/list/list.js b/list/list.js new file mode 100644 index 00000000000..8ff6a20ab9f --- /dev/null +++ b/list/list.js @@ -0,0 +1,1055 @@ +steal("can/util","can/map", function(can, Map){ + + + + // Helpers for `observable` lists. + var splice = [].splice, + /** + * @constructor can.List + * @inherits can.Map + * @download can/map + * @test can/map/qunit.html + * @parent canjs + * + * Use for observable array-like objects. + * + * @signature `new can.List([array])` + * + * Create an observable array-like object. + * + * @param {Array} [array] items to seed the List with + * @return {can.List} an instance of `can.List` with the elements from _array_ + * + * @signature `can.List([name,] [staticProperties,] instanceProperties)` + * + * Creates a new extended constructor function. + * + * This is deprecated. In CanJS 1.2, by default, calling the constructor function + * without `new` will create a `new` instance. Use [can.Construct.extend can.Map.extend] + * instead of calling the constructor to extend. + * + * @body + * + * ## Working with Lists + * + * `can.List` extends `[can.Map]`, so all the ways that you're used to working with + * Observes also work here, including [can.Map.prototype.bind bind], [can.Map.prototype.unbind unbind], + * and [can.Map.prototype.each each]. And just as you can directly read properties normally + * off of an Map, you can use array accessors ([]) to read elements directly off of a List. + * + * The one function of `can.Map` that works slightly differently is `attr`. As expected when working with + * arrays, top-level keys passed into `attr` are required to be numeric. (Strings may still be used when getting + * or modifying deep properties). Any top-level keys that are non-numeric are ignored. In addition, as might be + * expected, a call to argument-less `attr` returns an array instead of an object. + * + * Just as you shouldn't set properties of an Map directly, you shouldn't change elements + * of a List directly. Always use `attr` to set the elements of a List, or use [can.List.push push], + * [can.List.pop pop], [can.List.shift shift], [can.List.unshift unshift], or [can.List.splice splice]. + * + * Here is a tour through the forms of `can.List`'s `attr` that parallels the one found under [can.Map.prototype.attr attr]: + * + * @codestart + * var people = new can.List(['Alex', 'Bill']); + * + * // set an element: + * people.attr(0, 'Adam'); + * people[0] = 'Adam'; // don't do this! + * + * // get an element: + * people.attr(0); // 'Adam' + * people[0]; // 'Adam' + * + * // get all elements: + * people.attr(); // ['Adam', 'Bill'] + * + * // extend the array: + * people.attr(4, 'Charlie'); + * people.attr(); // ['Adam', 'Bill', undefined, undefined, 'Charlie'] + * + * // merge the elements: + * people.attr(['Alice', 'Bob', 'Eve']); + * people.attr(); // ['Alice', 'Bob', 'Eve', undefined, 'Charlie'] + * @codeend + * + * ## Listening to changes + * + * As with `can.Map`s, the real power of observable arrays comes from being able to + * react to changes in the member elements of the array. Lists emit five types of events: + * - the _change_ event fires on every change to a List. + * - the _set_ event is fired when an element is set. + * - the _add_ event is fired when an element is added to the List. + * - the _remove_ event is fired when an element is removed from the List. + * - the _length_ event is fired when the length of the List changes. + * + * This example presents a brief concrete survey of the times these events are fired: + * + * @codestart + * var list = new can.List(['Alice', 'Bob', 'Eve']); + * + * list.bind('change', function() { console.log('An element changed.'); }); + * list.bind('set', function() { console.log('An element was set.'); }); + * list.bind('add', function() { console.log('An element was added.'); }); + * list.bind('remove', function() { console.log('An element was removed.'); }); + * list.bind('length', function() { console.log('The length of the list changed.'); }); + * + * list.attr(0, 'Alexis'); // 'An element changed.' + * // 'An element was set.' + * + * list.attr(3, 'Xerxes'); // 'An element changed.' + * // 'An element was added.' + * // 'The length of the list was changed.' + * + * list.attr(['Adam', 'Bill']); // 'An element changed.' + * // 'An element was set.' + * // 'An element was changed.' + * // 'An element was set.' + * + * list.pop(); // 'An element changed.' + * // 'An element was removed.' + * // 'The length of the list was changed.' + * @codeend + * + * More information about binding to these events can be found under [can.List.attr attr]. + */ + list = Map( + /** + * @prototype + */ + { + setup: function( instances, options ) { + this.length = 0; + can.cid(this, ".map") + this._init = 1; + if( can.isDeferred(instances) ) { + this.replace(instances) + } else { + this.push.apply(this, can.makeArray(instances || [])); + } + // this change needs to be ignored + this.bind('change'+this._cid,can.proxy(this._changes,this)); + can.extend(this, options); + delete this._init; + }, + _triggerChange: function(attr, how, newVal, oldVal){ + + Map.prototype._triggerChange.apply(this,arguments) + // `batchTrigger` direct add and remove events... + if ( !~ attr.indexOf('.')){ + + if( how === 'add' ) { + can.batch.trigger(this, how, [newVal,+attr]); + can.batch.trigger(this,'length',[this.length]); + } else if( how === 'remove' ) { + can.batch.trigger(this, how, [oldVal, +attr]); + can.batch.trigger(this,'length',[this.length]); + } else { + can.batch.trigger(this,how,[newVal, +attr]) + } + + } + + }, + __get : function(attr){ + return attr ? this[attr] : this; + }, + ___set : function(attr, val){ + this[attr] = val; + if(+attr >= this.length){ + this.length = (+attr+1) + } + }, + _each: function(callback){ + var data = this.__get(); + for(var i =0; i < data.length; i++){ + callback(data[i],i) + } + }, + _bindsetup: Map.helpers.makeBindSetup("*"), + // Returns the serialized form of this list. + /** + * @hide + * Returns the serialized form of this list. + */ + serialize: function() { + return Map.helpers.serialize(this, 'serialize', []); + }, + /** + * @function can.List.prototype.each each + * @description Call a function on each element of a List. + * @signature `list.each( callback(item, index) )` + * + * `each` iterates through the Map, calling a function + * for each element. + * + * @param {function(*, Number)} callback the function to call for each element + * The value and index of each element will be passed as the first and second + * arguments, respectively, to the callback. If the callback returns false, + * the loop will stop. + * + * @return {can.List} this List, for chaining + * + * @body + * @codestart + * var i = 0; + * new can.Map([1, 10, 100]).each(function(element, index) { + * i += element; + * }); + * + * i; // 111 + * + * i = 0; + * new can.Map([1, 10, 100]).each(function(element, index) { + * i += element; + * if(index >= 1) { + * return false; + * } + * }); + * + * i; // 11 + * @codeend + */ + // + /** + * @function can.List.prototype.splice splice + * @description Insert and remove elements from a List. + * @signature `list.splice(index[, howMany[, ...newElements]])` + * @param {Number} index where to start removing or inserting elements + * + * @param {Number} [howMany] the number of elements to remove + * If _howMany_ is not provided, `splice` will all elements from `index` to the end of the List. + * + * @param {*} newElements elements to insert into the List + * + * @return {Array} the elements removed by `splice` + * + * @body + * `splice` lets you remove elements from and insert elements into a List. + * + * This example demonstrates how to do surgery on a list of numbers: + * + * @codestart + * var list = new can.List([0, 1, 2, 3]); + * + * // starting at index 2, remove one element and insert 'Alice' and 'Bob': + * list.splice(2, 1, 'Alice', 'Bob'); + * list.attr(); // [0, 1, 'Alice', 'Bob', 3] + * @codeend + * + * ## Events + * + * `splice` causes the List it's called on to emit _change_ events, + * _add_ events, _remove_ events, and _length_ events. If there are + * any elements to remove, a _change_ event, a _remove_ event, and a + * _length_ event will be fired. If there are any elements to insert, a + * separate _change_ event, an _add_ event, and a separate _length_ event + * will be fired. + * + * This slightly-modified version of the above example should help + * make it clear how `splice` causes events to be emitted: + * + * @codestart + * var list = new can.List(['a', 'b', 'c', 'd']); + * list.bind('change', function(ev, attr, how, newVals, oldVals) { + * console.log('change: ' + attr + ', ' + how + ', ' + newVals + ', ' + oldVals); + * }); + * list.bind('add', function(ev, newVals, where) { + * console.log('add: ' + newVals + ', ' + where); + * }); + * list.bind('remove', function(ev, oldVals, where) { + * console.log('remove: ' + oldVals + ', ' + where); + * }); + * list.bind('length', function(ev, length) { + * console.log('length: ' + length + ', ' + this.attr()); + * }); + * + * // starting at index 2, remove one element and insert 'Alice' and 'Bob': + * list.splice(2, 1, 'Alice', 'Bob'); // change: 2, 'remove', undefined, ['c'] + * // remove: ['c'], 2 + * // length: 5, ['a', 'b', 'Alice', 'Bob', 'd'] + * // change: 2, 'add', ['Alice', 'Bob'], ['c'] + * // add: ['Alice', 'Bob'], 2 + * // length: 5, ['a', 'b', 'Alice', 'Bob', 'd'] + * @codeend + * + * More information about binding to these events can be found under [can.List.attr attr]. + */ + splice: function( index, howMany ) { + var args = can.makeArray(arguments), + i; + + for ( i = 2; i < args.length; i++ ) { + var val = args[i]; + if ( Map.helpers.canMakeObserve(val) ) { + args[i] = Map.helpers.hookupBubble(val, "*", this, this.constructor.Map, this.constructor) + } + } + if ( howMany === undefined ) { + howMany = args[1] = this.length - index; + } + var removed = splice.apply(this, args); + can.batch.start(); + if ( howMany > 0 ) { + this._triggerChange(""+index, "remove", undefined, removed); + Map.helpers.unhookup(removed, this._cid); + } + if ( args.length > 2 ) { + this._triggerChange(""+index, "add", args.slice(2), removed); + } + can.batch.stop(); + return removed; + }, + /** + * @description Get or set elements in a List. + * @function can.List.prototype.attr attr + * @signature `list.attr()` + * + * Gets a collection of all the elements in this `can.List`. + * + * @return {Array} array with all the elements in this List. + * + * @signature `list.attr(index)` + * + * Reads a element from this `can.List`. + * + * @param {Number} index the element to read + * @return {*} the value at _index_. + * + * @signature `list.attr(index, value)` + * + * Assigns _value_ to the index _index_ on this `can.List`, expanding the list if necessary. + * + * @param {Number} index the element to set + * @param {*} the value to assign at _index_ + * @return {can.List} this List, for chaining + * + * @signature `list.attr(elements[, replaceCompletely])` + * + * Merges the members of _elements_ into this List, replacing each from the beginning in order. If + * _elements_ is longer than the current List, the current List will be expanded. If _elements_ + * is shorter than the current List, the extra existing members are not affected (unless + * _replaceCompletely_ is `true`). To remove elements without replacing them, use `[can.List.prototype.removeAttr removeAttr]`. + * + * @param {Array} elements an array of elements to merge in + * + * @param {bool} [replaceCompletely=false] whether to completely replace the elements of List + * If _replaceCompletely_ is `true` and _elements_ is shorter than the List, the existing + * extra members of the List will be removed. + * + * @return {can.List} this List, for chaining + * + * @body + * `attr` gets or sets elements on the `can.List` it's called on. Here's a tour through + * how all of its forms work: + * + * @codestart + * var people = new can.List(['Alex', 'Bill']); + * + * // set an element: + * people.attr(0, 'Adam'); + * + * // get an element: + * people.attr(0); // 'Adam' + * people[0]; // 'Adam' + * + * // get all elements: + * people.attr(); // ['Adam', 'Bill'] + * + * // extend the array: + * people.attr(4, 'Charlie'); + * people.attr(); // ['Adam', 'Bill', undefined, undefined, 'Charlie'] + * + * // merge the elements: + * people.attr(['Alice', 'Bob', 'Eve']); + * people.attr(); // ['Alice', 'Bob', 'Eve', undefined, 'Charlie'] + * @codeend + * + * ## Deep properties + * + * `attr` can also set and read deep properties. All you have to do is specify + * the property name as you normally would if you weren't using `attr`. + * + * @codestart + * var people = new can.List([{name: 'Alex'}, {name: 'Bob'}]); + * + * // set a property: + * people.attr('0.name', 'Alice'); + * + * // get a property: + * people.attr('0.name'); // 'Alice' + * people[0].attr('name'); // 'Alice' + * + * // get all properties: + * people.attr(); // [{name: 'Alice'}, {name: 'Bob'}] + * @codeend + * + * The discussion of deep properties under `[can.Map.prototype.attr]` may also + * be enlightening. + * + * ## Events + * + * `can.List`s emit five types of events in response to changes. They are: + * - the _change_ event fires on every change to a List. + * - the _set_ event is fired when an element is set. + * - the _add_ event is fired when an element is added to the List. + * - the _remove_ event is fired when an element is removed from the List. + * - the _length_ event is fired when the length of the List changes. + * + * * ## The _change_ event + * + * The first event that is fired is the _change_ event. The _change_ event is useful + * if you want to react to all changes on an List. + * + * @codestart + * var list = new can.List([]); + * list.bind('change', function(ev, index, how, newVal, oldVal) { + * console.log('Something changed.'); + * }); + * @codeend + * + * The parameters of the event handler for the _change_ event are: + * + * - _ev_ The event object. + * - _index_ Where the change took place. + * - _how_ Whether elements were added, removed, or set. + * Possible values are `'add'`, `'remove'`, or `'set'`. + * - _newVal_ The elements affected after the change + * _newVal_ will be a single value when an index is set, an Array when elements + * were added, and `undefined` if elements were removed. + * - _oldVal_ The elements affected before the change. + * _newVal_ will be a single value when an index is set, an Array when elements + * were removed, and `undefined` if elements were added. + * + * Here is a concrete tour through the _change_ event handler's arguments: + * + * @codestart + * var list = new can.List(); + * list.bind('change', function(ev, index, how, newVal, oldVal) { + * console.log(ev + ', ' + index + ', ' + how + ', ' + newVal + ', ' + oldVal); + * }); + * + * list.attr(['Alexis', 'Bill']); // [object Object], 0, add, ['Alexis', 'Bill'], undefined + * list.attr(2, 'Eve'); // [object Object], 2, add, Eve, undefined + * list.attr(0, 'Adam'); // [object Object], 0, set, Adam, Alexis + * list.attr(['Alice', 'Bob']); // [object Object], 0, set, Alice, Adam + * // [object Object], 1, set, Bob, Bill + * list.removeAttr(1); // [object Object], 1, remove, undefined, Bob + * @codeend + * + * ## The _set_ event + * + * _set_ events are fired when an element at an index that already exists in the List is + * modified. Actions can cause _set_ events to fire never also cause _length_ events + * to fire (although some functions, such as `[can.List.prototype.splice splice]` + * may cause unrelated sets of events to fire after being batched). + * + * The parameters of the event handler for the _set_ event are: + * + * - _ev_ The event object. + * - _newVal_ The new value of the element. + * - _index_ where the set took place. + * + * Here is a concrete tour through the _set_ event handler's arguments: + * + * @codestart + * var list = new can.List(); + * list.bind('set', function(ev, newVal, index) { + * console.log(newVal + ', ' + index); + * }); + * + * list.attr(['Alexis', 'Bill']); + * list.attr(2, 'Eve'); + * list.attr(0, 'Adam'); // Adam, 0 + * list.attr(['Alice', 'Bob']); // Alice, 0 + * // Bob, 1 + * list.removeAttr(1); + * @codeend + * + * ## The _add_ event + * + * _add_ events are fired when elements are added or inserted + * into the List. + * + * The parameters of the event handler for the _add_ event are: + * + * - _ev_ The event object. + * - _newElements_ The new elements. + * If more than one element is added, _newElements_ will be an array. + * Otherwise, it is simply the new element itself. + * - _index_ Where the add or insert took place. + * + * Here is a concrete tour through the _add_ event handler's arguments: + * + * @codestart + * var list = new can.List(); + * list.bind('add', function(ev, newElements, index) { + * console.log(newElements + ', ' + index); + * }); + * + * list.attr(['Alexis', 'Bill']); // ['Alexis', 'Bill'], 0 + * list.attr(2, 'Eve'); // Eve, 2 + * list.attr(0, 'Adam'); + * list.attr(['Alice', 'Bob']); + * + * list.removeAttr(1); + * @codeend + * + * ## The _remove_ event + * + * _remove_ events are fired when elements are removed from the list. + * + * The parameters of the event handler for the _remove_ event are: + * + * - _ev_ The event object. + * - _removedElements_ The removed elements. + * If more than one element was removed, _removedElements_ will be an array. + * Otherwise, it is simply the element itself. + * - _index_ Where the removal took place. + * + * Here is a concrete tour through the _remove_ event handler's arguments: + * + * @codestart + * var list = new can.List(); + * list.bind('remove', function(ev, removedElements, index) { + * console.log(removedElements + ', ' + index); + * }); + * + * list.attr(['Alexis', 'Bill']); + * list.attr(2, 'Eve'); + * list.attr(0, 'Adam'); + * list.attr(['Alice', 'Bob']); + * + * list.removeAttr(1); // Bob, 1 + * @codeend + * + * ## The _length_ event + * + * _length_ events are fired whenever the list changes. + * + * The parameters of the event handler for the _length_ event are: + * + * - _ev_ The event object. + *- _length_ The current length of the list. + * If events were batched when the _length_ event was triggered, _length_ + * will have the length of the list when `stopBatch` was called. Because + * of this, you may recieve multiple _length_ events with the same + * _length_ parameter. + * + * Here is a concrete tour through the _length_ event handler's arguments: + * + * @codestart + * var list = new can.List(); + * list.bind('length', function(ev, length) { + * console.log(length); + * }); + * + * list.attr(['Alexis', 'Bill']); // 2 + * list.attr(2, 'Eve'); // 3 + * list.attr(0, 'Adam'); + * list.attr(['Alice', 'Bob']); + * + * list.removeAttr(1); // 2 + * @codeend + */ + _attrs: function( items, remove ) { + if ( items === undefined ) { + return Map.helpers.serialize(this, 'attr', []); + } + + // Create a copy. + items = can.makeArray( items ); + + can.batch.start(); + this._updateAttrs(items, remove); + can.batch.stop() + }, + + _updateAttrs : function( items, remove ){ + var len = Math.min(items.length, this.length); + + for ( var prop = 0; prop < len; prop++ ) { + var curVal = this[prop], + newVal = items[prop]; + + if ( Map.helpers.canMakeObserve(curVal) && Map.helpers.canMakeObserve(newVal) ) { + curVal.attr(newVal, remove) + } else if ( curVal != newVal ) { + this._set(prop, newVal) + } else { + + } + } + if ( items.length > this.length ) { + // Add in the remaining props. + this.push.apply( this, items.slice( this.length ) ); + } else if ( items.length < this.length && remove ) { + this.splice(items.length) + } + } + }), + + + // Converts to an `array` of arguments. + getArgs = function( args ) { + return args[0] && can.isArray(args[0]) ? + args[0] : + can.makeArray(args); + }; + // Create `push`, `pop`, `shift`, and `unshift` + can.each({ + /** + * @function can.List.prototype.push push + * @description Add elements to the end of a list. + * @signature `list.push(...elements)` + * + * `push` adds elements onto the end of a List.] + * + * @param {*} elements the elements to add to the List + * + * @return {Number} the new length of the List + * + * @body + * `push` is fairly straightforward: + * + * @codestart + * var list = new can.List(['Alice']); + * + * list.push('Bob', 'Eve'); + * list.attr(); // ['Alice', 'Bob', 'Eve'] + * @codeend + * + * If you have an array you want to concatenate to the end + * of the List, you can use `apply`: + * + * @codestart + * var names = ['Bob', 'Eve'], + * list = new can.List(['Alice']); + * + * list.push.apply(list, names); + * list.attr(); // ['Alice', 'Bob', 'Eve'] + * @codeend + * + * ## Events + * + * `push` causes _change_, _add_, and _length_ events to be fired. + * + * ## See also + * + * `push` has a counterpart in [can.List.pop pop], or you may be + * looking for [can.List.unshift unshift] and its counterpart [can.List.shift shift]. + */ + push: "length", + /** + * @function can.List.prototype.unshift unshift + * @description Add elements to the beginning of a List. + * @signature `list.unshift(...elements)` + * + * `unshift` adds elements onto the beginning of a List. + * + * @param {*} elements the elements to add to the List + * + * @return {Number} the new length of the List + * + * @body + * `unshift` adds elements to the front of the list in bulk in the order specified: + * + * @codestart + * var list = new can.List(['Alice']); + * + * list.unshift('Bob', 'Eve'); + * list.attr(); // ['Bob', 'Eve', 'Alice'] + * @codeend + * + * If you have an array you want to concatenate to the beginning + * of the List, you can use `apply`: + * + * @codestart + * var names = ['Bob', 'Eve'], + * list = new can.List(['Alice']); + * + * list.push.apply(list, names); + * list.attr(); // ['Bob', 'Eve', 'Alice'] + * @codeend + * + * ## Events + * + * `unshift` causes _change_, _add_, and _length_ events to be fired. + * + * ## See also + * + * `unshift` has a counterpart in [can.List.shift shift], or you may be + * looking for [can.List.push push] and its counterpart [can.List.pop pop]. + */ + unshift: 0 + }, + // Adds a method + // `name` - The method name. + // `where` - Where items in the `array` should be added. + function( where, name ) { + var orig = [][name] + list.prototype[name] = function() { + // Get the items being added. + var args = [], + // Where we are going to add items. + len = where ? this.length : 0, + i = arguments.length, + res, + val, + constructor = this.constructor; + + // Go through and convert anything to an `map` that needs to be converted. + while(i--){ + val = arguments[i]; + args[i] = Map.helpers.canMakeObserve(val) ? + Map.helpers.hookupBubble(val, "*", this, this.constructor.Map, this.constructor) : + val; + } + + // Call the original method. + res = orig.apply(this, args); + + if ( !this.comparator || args.length ) { + + this._triggerChange(""+len, "add", args, undefined); + } + + return res; + } + }); + + can.each({ + /** + * @function can.List.prototype.pop pop + * @description Remove an element from the end of a List. + * @signature `list.pop()` + * + * `push` removes an element from the end of a List. + * + * @return {*} the element just popped off the List, or `undefined` if the List was empty + * + * @body + * `pop` is the opposite action from `[can.List.push push]`: + * + * @codestart + * var list = new can.List(['Alice']); + * + * list.push('Bob', 'Eve'); + * list.attr(); // ['Alice', 'Bob', 'Eve'] + * + * list.pop(); // 'Eve' + * list.pop(); // 'Bob' + * list.pop(); // 'Alice' + * list.pop(); // undefined + * @codeend + * + * ## Events + * + * `pop` causes _change_, _remove_, and _length_ events to be fired if the List is not empty + * when it is called. + * + * ## See also + * + * `pop` has its counterpart in [can.List.push push], or you may be + * looking for [can.List.unshift unshift] and its counterpart [can.List.shift shift]. + */ + pop: "length", + /** + * @function can.List.prototype.shift shift + * @description Remove en element from the front of a list. + * @signature `list.shift()` + * + * `shift` removes an element from the beginning of a List. + * + * @return {*} the element just shifted off the List, or `undefined` if the List is empty + * + * @body + * `shift` is the opposite action from `[can.List.unshift unshift]`: + * + * @codestart + * var list = new can.List(['Alice']); + * + * list.unshift('Bob', 'Eve'); + * list.attr(); // ['Bob', 'Eve', 'Alice'] + * + * list.shift(); // 'Bob' + * list.shift(); // 'Eve' + * list.shift(); // 'Alice' + * list.shift(); // undefined + * @codeend + * + * ## Events + * + * `pop` causes _change_, _remove_, and _length_ events to be fired if the List is not empty + * when it is called. + * + * ## See also + * + * `shift` has a counterpart in [can.List.unshift unshift], or you may be + * looking for [can.List.push push] and its counterpart [can.List.pop pop]. + */ + shift: 0 + }, + // Creates a `remove` type method + function( where, name ) { + list.prototype[name] = function() { + + var args = getArgs(arguments), + len = where && this.length ? this.length - 1 : 0; + + + var res = [][name].apply(this, args) + + // Create a change where the args are + // `len` - Where these items were removed. + // `remove` - Items removed. + // `undefined` - The new values (there are none). + // `res` - The old, removed values (should these be unbound). + this._triggerChange(""+len, "remove", undefined, [res]) + + if ( res && res.unbind ) { + res.unbind("change" + this._cid) + } + return res; + } + }); + + can.extend(list.prototype, { + /** + * @function can.List.prototype.indexOf indexOf + * @description Look for an item in a List. + * @signature `list.indexOf(item)` + * + * `indexOf` finds the position of a given item in the List. + * + * @param {*} item the item to find + * + * @return {Number} the position of the item in the List, or -1 if the item is not found. + * + * @body + * @codestart + * var list = new can.List(['Alice', 'Bob', 'Eve']); + * list.indexOf('Alice'); // 0 + * list.indexOf('Charlie'); // -1 + * @codeend + * + * It is trivial to make a `contains`-type function using `indexOf`: + * + * @codestart + * function(list, item) { + * return list.indexOf(item) >= 0; + * } + * @codeend + */ + indexOf: function(item) { + this.attr('length') + return can.inArray(item, this) + }, + + /** + * @function can.List.prototype.join join + * @description Join a List's elements into a string. + * @signature `list.join(separator)` + * + * `join` turns a List into a string by inserting _separator_ between the string representations + * of all the elements of the List. + * + * @param {String} separator the string to seperate elements with + * + * @return {String} the joined string + * + * @body + * @codestart + * var list = new can.List(['Alice', 'Bob', 'Eve']); + * list.join(', '); // 'Alice, Bob, Eve' + * + * var beatles = new can.List(['John', 'Paul', 'Ringo', 'George']); + * beatles.join('&'); // 'John&Paul&Ringo&George' + * @codeend + */ + join : [].join, + + /** + * @function can.List.prototype.reverse reverse + * @description Reverse the order of a List. + * @signature `list.reverse()` + * + * `reverse` reverses the elements of the List in place. + * + * @return {can.List} the List, for chaining + * + * @body + * @codestart + * var list = new can.List(['Alice', 'Bob', 'Eve']); + * var reversedList = list.reverse(); + * + * reversedList.attr(); // ['Eve', 'Bob', 'Alice']; + * list === reversedList; // true + * @codeend + */ + reverse: [].reverse, + + /** + * @function can.List.prototype.slice slice + * @description Make a copy of a part of a List. + * @signature `list.slice([start[, end]])` + * + * `slice` creates a copy of a portion of the List. + * + * @param {Number} [start=0] the index to start copying from + * + * @param {Number} [end] the first index not to include in the copy + * If _end_ is not supplied, `slice` will copy until the end of the list. + * + * @return {can.List} a new `can.List` with the extracted elements + * + * @body + * @codestart + * var list = new can.List(['Alice', 'Bob', 'Charlie', 'Daniel', 'Eve']); + * var newList = list.slice(1, 4); + * newList.attr(); // ['Bob', 'Charlie', 'Daniel'] + * @codeend + * + * `slice` is the simplest way to copy a List: + * + * @codestart + * var list = new can.List(['Alice', 'Bob', 'Eve']); + * var copy = list.slice(); + * + * copy.attr(); // ['Alice', 'Bob', 'Eve'] + * list === copy; // false + * @codeend + */ + slice : function() { + var temp = Array.prototype.slice.apply(this, arguments); + return new this.constructor( temp ); + }, + + /** + * @function can.List.prototype.concat concat + * @description Merge many collections together into a List. + * @signature `list.concat(...args)` + * @param {Array|can.List|*} args Any number of arrays, Lists, or values to add in + * For each parameter given, if it is an Array or a List, each of its elements will be added to + * the end of the concatenated List. Otherwise, the parameter itself will be added. + * + * @body + * `concat` makes a new List with the elements of the List followed by the elements of the parameters. + * + * @codestart + * var list = new can.List(); + * var newList = list.concat( + * 'Alice', + * ['Bob', 'Charlie']), + * new can.List(['Daniel', 'Eve']), + * {f: 'Francis'} + * ); + * newList.attr(); // ['Alice', 'Bob', 'Charlie', 'Daniel', 'Eve', {f: 'Francis'}] + * @codeend + */ + concat : function() { + var args = []; + can.each( can.makeArray( arguments ), function( arg, i ) { + args[i] = arg instanceof can.List ? arg.serialize() : arg ; + }); + return new this.constructor(Array.prototype.concat.apply(this.serialize(), args)); + }, + + /** + * @function can.List.prototype.forEach forEach + * @description Call a function for each element of a List. + * @signature `list.forEach(callback[, thisArg])` + * @param {function(element, index, list)} callback a function to call with each element of the List + * The three parameters that _callback_ gets passed are _element_, the element at _index_, _index_ the + * current element of the list, and _list_ the List the elements are coming from. + * @param {Object} [thisArg] the object to use as `this` inside the callback + * + * @body + * `forEach` calls a callback for each element in the List. + * + * @codestart + * var list = new can.List([1, 2, 3]); + * list.forEach(function(element, index, list) { + * list.attr(index, element * element); + * }); + * list.attr(); // [1, 4, 9] + * @codeend + */ + forEach : function( cb, thisarg ) { + can.each(this, cb, thisarg || this ); + }, + + /** + * @function can.List.prototype.replace replace + * @description Replace all the elements of a List. + * @signature `list.replace(collection)` + * @param {Array|can.List|can.Deferred} collection the collection of new elements to use + * If a [can.Deferred] is passed, it must resolve to an `Array` or `can.List`. + * The elements of the list are not actually removed until the Deferred resolves. + * + * @body + * `replace` replaces all the elements of this List with new ones. + * + * `replace` is especially useful when `can.List`s are live-bound into `[can.Control]`s, + * and you intend to populate them with the results of a `[can.Model]` call: + * + * @codestart + * can.Control({ + * init: function() { + * this.list = new Todo.List(); + * // live-bind the list into the DOM + * this.element.html(can.view('list.mustache', this.list)); + * // when this AJAX call returns, the live-bound DOM will be updated + * this.list.replace(Todo.findAll()); + * } + * }); + * @codeend + * + * Learn more about [can.Model.List making Lists of models]. + * + * ## Events + * + * A major difference between `replace` and `attr(newElements, true)` is that `replace` always emits + * an_add_ event and a _remove_ event, whereas `attr` will cause _set_ events along an _add_ or _remove_ + * event if needed. Corresponding _change_ and _length_ events will be fired as well. + * + * The differences in the events fired by `attr` and `replace` are demonstrated concretely by this example: + * @codestart + * var attrList = new can.List(['Alexis', 'Bill']); + * attrList.bind('change', function(ev, index, how, newVals, oldVals) { + * console.log(index + ', ' + how + ', ' + newVals + ', ' + oldVals); + * }); + * + * var replaceList = new can.List(['Alexis', 'Bill']); + * replaceList.bind('change', function(ev, index, how, newVals, oldVals) { + * console.log(index + ', ' + how + ', ' + newVals + ', ' + oldVals); + * }); + * + * attrList.attr(['Adam', 'Ben'], true); // 0, set, Adam, Alexis + * // 1, set, Ben, Bill + * replaceList.replace(['Adam', 'Ben']); // 0, remove, undefined, ['Alexis', 'Bill'] + * // 0, add, undefined, ['Adam', 'Ben'] + * + * attrList.attr(['Amber'], true); // 0, set, Amber, Adam + * // 1, remove, undefined, Ben + * replaceList.replace(['Amber']); // 0, remove, undefined, ['Adam', 'Ben'] + * // 0, add, Amber, ['Adam', 'Ben'] + * + * attrList.attr(['Alice', 'Bob', 'Eve'], true); // 0, set, Alice, Amber + * // 1, add, ['Bob', 'Eve'], undefined + * replaceList.replace(['Alice', 'Bob', 'Eve']); // 0, remove, undefined, Amber + * // 0, add, ['Alice', 'Bob', 'Eve'], Amber + * @codeend + */ + replace : function(newList) { + if(can.isDeferred(newList)) { + newList.then(can.proxy(this.replace, this)); + } else { + this.splice.apply(this, [0, this.length].concat(can.makeArray(newList || []))); + } + + return this; + } + }); + + can.List = Map.List = list; + return can.List; +}) diff --git a/list/list_test.js b/list/list_test.js new file mode 100644 index 00000000000..9d10bb1006e --- /dev/null +++ b/list/list_test.js @@ -0,0 +1,163 @@ +(function(undefined) { + +module('can/list') + + + +test("list attr changes length", function(){ + var l = new can.List([0,1,2]) + l.attr(3,3) + equal(l.length, 4); +}) + +test("list splice", function(){ + var l = new can.List([0,1,2,3]), + first = true; + + l.bind('change', function( ev, attr, how, newVals, oldVals ) { + equal(attr, "1") + // where comes from the attr ... + //equal(where, 1) + if(first){ + equal( how, "remove", "removing items" ) + equal( newVals, undefined, "no new Vals" ) + } else { + deepEqual( newVals, ["a","b"] , "got the right newVals") + equal( how, "add", "adding items" ) + } + + first = false; + }) + + l.splice(1,2, "a", "b"); + deepEqual(l.serialize(), [0,"a","b", 3], "serialized") +}); + + + +test("list pop", function(){ + var l = new can.List([0,1,2,3]); + + l.bind('change', function( ev, attr, how, newVals, oldVals ) { + equal(attr, "3") + + equal( how, "remove" ) + equal( newVals, undefined ) + deepEqual( oldVals, [3] ) + }) + + l.pop(); + deepEqual(l.serialize(), [0,1,2]) +}) + + + +test("remove nested property in item of array map", function(){ + var state = new can.List([{nested: true}]); + + state.bind("change", function(ev, attr, how, newVal, old){ + equal(attr, "0.nested"); + equal(how, "remove") + deepEqual(old, true); + }) + + state.removeAttr("0.nested"); + equal(undefined, state.attr("0.nested") ); +}); + + +test("pop unbinds", function(){ + var l = new can.List([{foo: 'bar'}]); + var o = l.attr(0), + count = 0; + l.bind('change', function(ev, attr, how, newVal, oldVal){ + count++; + if(count == 1){ + // the prop change + equal(attr, '0.foo', "count is set"); + } else if(count === 2 ){ + equal(how, "remove"); + equal(attr, "0") + } else { + ok(false, "called too many times") + } + + }) + + equal( o.attr('foo') , 'bar'); + + o.attr('foo','car') + l.pop(); + o.attr('foo','bad') +}) + +test("splice unbinds", function(){ + var l = new can.List([{foo: 'bar'}]); + var o = l.attr(0), + count = 0; + l.bind('change', function(ev, attr, how, newVal, oldVal){ + count++; + if(count == 1){ + // the prop change + equal(attr, '0.foo', "count is set"); + } else if(count === 2 ){ + equal(how, "remove"); + equal(attr, "0") + } else { + ok(false, "called too many times") + } + + }) + + equal( o.attr('foo') , 'bar'); + + o.attr('foo','car') + l.splice(0,1); + o.attr('foo','bad') +}); + + +test("always gets right attr even after moving array items", function(){ + var l = new can.List([{foo: 'bar'}]); + + // get the first item + var o = l.attr(0); + // add a new item + l.unshift("A new Value") + + // listen to change + l.bind('change', function(ev, attr, how){ + equal(attr, "1.foo") + }) + + // this should have bubbled right + o.attr('foo','led you') +}); + + + +test("Array accessor methods", 11, function() { + var l = new can.List([ 'a', 'b', 'c' ]), + sliced = l.slice(2), + joined = l.join(' | '), + concatenated = l.concat([ 2, 1 ], new can.List([ 0 ])); + + ok(sliced instanceof can.List, 'Slice is an Observable list'); + equal(sliced.length, 1, 'Sliced off two elements'); + equal(sliced[0], 'c', 'Single element as expected'); + equal(joined, 'a | b | c', 'Joined list properly'); + ok(concatenated instanceof can.List, 'Concatenated is an Observable list'); + deepEqual(concatenated.serialize(), [ 'a', 'b', 'c', 2, 1, 0 ], 'List concatenated properly'); + l.forEach(function(letter, index) { + ok(true, 'Iteration'); + if(index === 0) { + equal(letter, 'a', 'First letter right'); + } + if(index === 2) { + equal(letter, 'c', 'Last letter right'); + } + }); +}); + + +})(); diff --git a/list/test.html b/list/test.html new file mode 100644 index 00000000000..1d284dc8295 --- /dev/null +++ b/list/test.html @@ -0,0 +1,25 @@ + + + + + + +

can.List Test Suite

+ +

+ +
+

+
    +
    + + + + + + \ No newline at end of file diff --git a/observe/attributes/attributes-assocations.html b/map/attributes/attributes-assocations.html similarity index 97% rename from observe/attributes/attributes-assocations.html rename to map/attributes/attributes-assocations.html index e51f6323984..6096aa30ab1 100644 --- a/observe/attributes/attributes-assocations.html +++ b/map/attributes/attributes-assocations.html @@ -20,7 +20,7 @@ steal.config({ root: '../../' }); -}).then('can/observe/attributes', 'can/model','can/util/fixture') +}).then('can/map/attributes', 'can/model','can/util/fixture') .then(function(){ // simulate ajax response with fixtures diff --git a/observe/attributes/attributes.html b/map/attributes/attributes.html similarity index 97% rename from observe/attributes/attributes.html rename to map/attributes/attributes.html index 69840954854..f91513117c9 100644 --- a/observe/attributes/attributes.html +++ b/map/attributes/attributes.html @@ -19,7 +19,7 @@ src='../../lib/steal/steal.js'> diff --git a/observe/backup/backup.html b/map/backup/backup.html similarity index 97% rename from observe/backup/backup.html rename to map/backup/backup.html index a593d3b8870..e071d68f4aa 100644 --- a/observe/backup/backup.html +++ b/map/backup/backup.html @@ -39,9 +39,9 @@

    Observe Backup Demo

    diff --git a/observe/batch_events.md b/map/batch_events.md similarity index 85% rename from observe/batch_events.md rename to map/batch_events.md index 8a70cc756c7..16d6169c024 100644 --- a/observe/batch_events.md +++ b/map/batch_events.md @@ -1,14 +1,14 @@ -@page can.Observe.batchEvents Batch Events -@parent can.Observe +@page can.Map.batchEvents Batch Events +@parent can.Map -`can.Observe.startBatch( batchStopHandler )` and -`can.Observe.stopBatch( force, callStart )` +`can.batch.start( batchStopHandler )` and +`can.batch.stop( force, callStart )` are used to specify atomic operations. `startBatch` prevents change events from being fired until `stopBatch` is called. The following listens to changes on a `player`: - var player = new can.Observe({ + var player = new can.Map({ tvshow: "The Simpsons" }); @@ -20,12 +20,12 @@ The "change" callback handler does not get called until after `tvshow` is removed, `song` is added, and `stopBatch` is called. - can.Observe.startBatch(); + can.batch.start(); player.removeAttr("tvshow"); player.attr("song","What makes you beautiful"); - can.Observe.stopBatch(); + can.batch.stop(); Performance and correctness are the two most common reasons to use batch operations. @@ -37,24 +37,24 @@ state. For example, the previous `player` should have a `tvshow` or `song` property, but not both. Event listeners should never be called in an intermediate state. We can make this happen with `startBatch`, `stopBatch` and -the `can/observe/setter` plugin as follows: +the `can/map/setter` plugin as follows: // Selection constructor function inherits from Observe - Player = can.Observe({ + Player = can.Map({ // called when setting tvshow setTvshow: function(newVal, success){ - can.Observe.startBatch(); + can.batch.start(); this.removeAttr("song") success(newVal); - can.Observe.stopBatch(); + can.batch.stop(); }, // called when setting song setSong: function(newVal, success){ - can.Observe.startBatch(); + can.batch.start(); this.removeAttr("tvshow") success(newVal); - can.Observe.stopBatch(); + can.batch.stop(); } }); @@ -108,11 +108,11 @@ And a template that renders the number of selected items: The following updates the DOM once per click: $("#selectAll").click(function(){ - can.Observe.startBatch() + can.batch.start() items.each(function(item){ item.attr('selected', true) }) - can.Observe.stopBatch() + can.batch.stop() }) ## batchNum @@ -133,9 +133,9 @@ respond only once for a given batchNum, you can do it like: Libraries like Angular and Ember always batch operations. Set this up with: - can.Observe.startBatch(); + can.batch.start(); setTimeout(function(){ - can.Observe.stopBatch(true, true); + can.batch.stop(true, true); setTimeout(arguments.callee, 10) },10); diff --git a/observe/delegate/delegate.js b/map/delegate/delegate.js similarity index 94% rename from observe/delegate/delegate.js rename to map/delegate/delegate.js index 18414519a02..2927e4ce9f0 100644 --- a/observe/delegate/delegate.js +++ b/map/delegate/delegate.js @@ -1,4 +1,4 @@ -steal('can/util', 'can/observe', function(can) { +steal('can/util', 'can/map', function(can) { @@ -130,11 +130,11 @@ steal('can/util', 'can/observe', function(can) { } }; - can.extend(can.Observe.prototype,{ + can.extend(can.Map.prototype,{ /** - * @function can.Observe.prototype.delegate delegate - * @parent can.Observe.delegate - * @plugin can/observe/delegate + * @function can.Map.prototype.delegate delegate + * @parent can.Map.delegate + * @plugin can/map/delegate * * `delegate( selector, event, handler(ev,newVal,oldVal,from) )` listen for changes * in a child attribute from the parent. The child attribute @@ -142,7 +142,7 @@ steal('can/util', 'can/observe', function(can) { * * * // create an observable - * var observe = can.Observe({ + * var observe = can.Map({ * foo : { * bar : "Hello World" * } @@ -238,7 +238,7 @@ steal('can/util', 'can/observe', function(can) { * Delegate lets you listen on multiple values at once. The following listens * for first and last name changes: * - * var o = new can.Observe({ + * var o = new can.Map({ * name : {first: "Justin", last: "Meyer"} * }) * @@ -252,7 +252,7 @@ steal('can/util', 'can/observe', function(can) { * * Delegate lets you listen when a property is __set__ to a specific value: * - * var o = new can.Observe({ + * var o = new can.Map({ * name : "Justin" * }) * @@ -282,7 +282,7 @@ steal('can/util', 'can/observe', function(can) { * - oldVal - the old value set on the observe * - prop - the prop name that was changed * - * @return {can.Observe} the observe for chaining + * @return {can.Map} the observe for chaining */ delegate : function(selector, event, handler){ selector = can.trim(selector); @@ -326,8 +326,8 @@ steal('can/util', 'can/observe', function(can) { return this; }, /** - * @function can.Observe.prototype.undelegate undelegate - * @parent can.Observe.delegate + * @function can.Map.prototype.undelegate undelegate + * @parent can.Map.delegate * * `undelegate( selector, event, handler )` removes a delegated event handler from an observe. * @@ -336,7 +336,7 @@ steal('can/util', 'can/observe', function(can) { * @param {String} selector the attribute name of the object you want to undelegate from. * @param {String} event the event name * @param {Function} handler the callback handler - * @return {can.Observe} the observe for chaining + * @return {can.Map} the observe for chaining */ undelegate : function(selector, event, handler){ selector = selector && can.trim(selector); @@ -367,6 +367,6 @@ steal('can/util', 'can/observe', function(can) { } }); // add helpers for testing .. - can.Observe.prototype.delegate.matches = delegateMatches; - return can.Observe; + can.Map.prototype.delegate.matches = delegateMatches; + return can.Map; }) diff --git a/observe/delegate/delegate.md b/map/delegate/delegate.md similarity index 67% rename from observe/delegate/delegate.md rename to map/delegate/delegate.md index 62581451559..3d87ccb072a 100644 --- a/observe/delegate/delegate.md +++ b/map/delegate/delegate.md @@ -1,20 +1,20 @@ -@page can.Observe.delegate delegate -@parent can.Observe.plugins -@plugin can/observe/delegate -@test can/observe/delegate/test.html +@page can.Map.delegate delegate +@parent can.Map.plugins +@plugin can/map/delegate +@test can/map/delegate/test.html The __delegate__ plugin allows you to listen to more specific event changes on -[can.Observe Observes]. It allows you to specify: +[can.Map Observes]. It allows you to specify: - the __attribute__ or __attributes__ - that you want to listen to and optionally the __value__ you want it to match - the __type__ of event (add,set,remove,change) Listen to specific event changes with -[can.Observe::delegate delegate]\(selector, event, handler(ev,newVal,oldVal,from)\) : +[can.Map::delegate delegate]\(selector, event, handler(ev,newVal,oldVal,from)\) : // create an observable - var observe = new can.Observe({ + var observe = new can.Map({ name : { first : "Justin Meyer" } @@ -35,7 +35,7 @@ Listen to specific event changes with observe.attr('name.first',"Justin") Delegate will listen on the object until you -call [can.Observe::undelegate undelegate]\(selector, event, handler\) to remove the event handler. +call [can.Map::undelegate undelegate]\(selector, event, handler\) to remove the event handler. observe.undelegate("name.first","set", handler ); diff --git a/observe/delegate/delegate_test.js b/map/delegate/delegate_test.js similarity index 93% rename from observe/delegate/delegate_test.js rename to map/delegate/delegate_test.js index f5793dc2cbe..20bdd2656e0 100644 --- a/observe/delegate/delegate_test.js +++ b/map/delegate/delegate_test.js @@ -1,9 +1,9 @@ (function() { -module('can/observe/delegate') +module('can/map/delegate') -var matches = can.Observe.prototype.delegate.matches; +var matches = can.Map.prototype.delegate.matches; test("matches", function(){ equal( matches(['**'], ['foo','bar','0']) , @@ -31,7 +31,7 @@ test("matches", function(){ test("delegate", 4,function(){ - var state = new can.Observe({ + var state = new can.Map({ properties : { prices : [] } @@ -53,7 +53,7 @@ test("delegate", 4,function(){ test("delegate on add", 2, function(){ - var state = new can.Observe({}); + var state = new can.Map({}); state.delegate("foo","add", function(ev, newVal){ ok(true, "called"); @@ -67,7 +67,7 @@ test("delegate on add", 2, function(){ }); test("delegate set is called on add", 2, function(){ - var state = new can.Observe({}); + var state = new can.Map({}); state.delegate("foo","set", function(ev, newVal){ ok(true, "called"); @@ -77,7 +77,7 @@ test("delegate set is called on add", 2, function(){ }); test("delegate's this", 5, function(){ - var state = new can.Observe({ + var state = new can.Map({ person : { name : { first : "justin", @@ -113,7 +113,7 @@ test("delegate's this", 5, function(){ test("delegate on deep properties with *", function(){ - var state = new can.Observe({ + var state = new can.Map({ person : { name : { first : "justin", @@ -131,7 +131,7 @@ test("delegate on deep properties with *", function(){ test("compound sets", function(){ - var state = new can.Observe({ + var state = new can.Map({ type : "person", id: "5" }); @@ -179,7 +179,7 @@ test("compound sets", function(){ test("undelegate within event loop",1, function(){ - var state = new can.Observe({ + var state = new can.Map({ type : "person", id: "5" }); @@ -205,7 +205,7 @@ test("undelegate within event loop",1, function(){ test("selector types", 5, function() { - var state = new can.Observe({ + var state = new can.Map({ foo: "a", bar: "b", baz: "c", diff --git a/observe/delegate/test.html b/map/delegate/test.html similarity index 82% rename from observe/delegate/test.html rename to map/delegate/test.html index 252351e99eb..98609738158 100644 --- a/observe/delegate/test.html +++ b/map/delegate/test.html @@ -17,9 +17,9 @@

    diff --git a/observe/elements/elements.js b/map/elements/elements.js similarity index 95% rename from observe/elements/elements.js rename to map/elements/elements.js index 261bd727634..977673fa9f8 100644 --- a/observe/elements/elements.js +++ b/map/elements/elements.js @@ -1,4 +1,4 @@ -steal('can/util', 'can/observe', function(can, Observe) { +steal('can/util', 'can/map', function(can, Observe) { var unique = function( items ) { var collect = []; @@ -15,7 +15,7 @@ var unique = function( items ) { }); } - can.extend(can.Observe.prototype,{ + can.extend(can.Map.prototype,{ /** * Returns a unique identifier for the observe instance. For example: * @@ -111,7 +111,7 @@ var unique = function( items ) { }); }); - ret = kind ? new kind : new can.Observe.List; + ret = kind ? new kind : new can.List; ret.push.apply(ret, unique(collection)); return ret; @@ -139,7 +139,7 @@ var unique = function( items ) { * it will add the instance to the element. */ $.fn.instance = function( type ) { - if ( type && type instanceof can.Observe ) { + if ( type && type instanceof can.Map ) { type.hookup(this[0]); return this; } else { @@ -148,5 +148,5 @@ var unique = function( items ) { }; - return can.Observe; + return can.Map; }) diff --git a/observe/elements/elements_test.js b/map/elements/elements_test.js similarity index 88% rename from observe/elements/elements_test.js rename to map/elements/elements_test.js index 48c33db1f27..a5978ef632b 100644 --- a/observe/elements/elements_test.js +++ b/map/elements/elements_test.js @@ -1,6 +1,6 @@ (function() { -module("can/observe/elements") +module("can/map/elements") test("identity uses the real id", function(){ var Person = can.Model.extend({ diff --git a/observe/elements/test.html b/map/elements/test.html similarity index 83% rename from observe/elements/test.html rename to map/elements/test.html index c0d751c4b7f..52c5c10cdd0 100644 --- a/observe/elements/test.html +++ b/map/elements/test.html @@ -17,7 +17,7 @@

    diff --git a/observe/list/list.js b/map/list/list.js similarity index 94% rename from observe/list/list.js rename to map/list/list.js index 687682fd2ae..583fea6257a 100644 --- a/observe/list/list.js +++ b/map/list/list.js @@ -1,5 +1,5 @@ -steal('can/util', 'can/observe', 'can/observe/compute', function(can) { - can.extend(can.Observe.List.prototype, { +steal('can/util', 'can/map', 'can/compute', function(can) { + can.extend(can.List.prototype, { filter : function(callback) { // The filtered list var filtered = new this.constructor(); @@ -57,7 +57,7 @@ steal('can/util', 'can/observe', 'can/observe/compute', function(can) { }, map : function(callback) { - var mapped = new can.Observe.List(); + var mapped = new can.List(); var self = this; // Again, lets run a generator function var generator = function(element, index) { @@ -122,5 +122,5 @@ steal('can/util', 'can/observe', 'can/observe/compute', function(can) { */ }); - return can.Observe.List; + return can.List; }) diff --git a/observe/list/list_test.js b/map/list/list_test.js similarity index 93% rename from observe/list/list_test.js rename to map/list/list_test.js index ef38a71f2f2..ec086a636b7 100644 --- a/observe/list/list_test.js +++ b/map/list/list_test.js @@ -1,8 +1,8 @@ (function(){ - module("can/observe/list"); + module("can/map/list"); test("filter", 8, function() { - var original = new can.Observe.List([ + var original = new can.List([ { name : 'Test 1', age : 20 @@ -21,7 +21,7 @@ } ]); - var state = new can.Observe({ minAge : 20 }); + var state = new can.Map({ minAge : 20 }); var filtered = original.filter(function(element) { return element.attr('age') > state.attr('minAge'); @@ -54,7 +54,7 @@ }); test("attr updates items in position order", function(){ - var original = new can.Observe.List([ + var original = new can.List([ { id : 1, name : 'Test 1', @@ -101,7 +101,7 @@ }); test("map", function() { - var original = new can.Observe.List([ + var original = new can.List([ { name : 'Test 1', age : 20 diff --git a/observe/list/qunit.html b/map/list/qunit.html similarity index 87% rename from observe/list/qunit.html rename to map/list/qunit.html index 44f8986eaef..94d5ab7753b 100644 --- a/observe/list/qunit.html +++ b/map/list/qunit.html @@ -15,8 +15,8 @@

    diff --git a/observe/observe.html b/map/map.html similarity index 100% rename from observe/observe.html rename to map/map.html diff --git a/map/map.js b/map/map.js new file mode 100644 index 00000000000..b5743de429b --- /dev/null +++ b/map/map.js @@ -0,0 +1,832 @@ +// 1.69 +steal('can/util','can/util/bind','can/construct', 'can/util/batch',function(can, bind) { + // ## map.js + // `can.Map` + // _Provides the observable pattern for JavaScript Objects._ + // + // Removes all listeners. + var bindToChildAndBubbleToParent = function(child, prop, parent){ + child.bind("change" + parent._cid, + function( /* ev, attr */ ) { + // `batchTrigger` the type on this... + var args = can.makeArray(arguments), + ev = args.shift(); + args[0] = (prop === "*" ? + [ parent.indexOf( child ), args[0]] : + [ prop, args[0]] ).join("."); + + // track objects dispatched on this map + ev.triggeredNS = ev.triggeredNS || {}; + + // if it has already been dispatched exit + if (ev.triggeredNS[parent._cid]) { + return; + } + + ev.triggeredNS[parent._cid] = true; + // send change event with modified attr to parent + can.trigger(parent, ev, args); + // send modified attr event to parent + //can.trigger(parent, args[0], args); + }); + }, + // An `id` to track events for a given map. + observeId = 0, + attrParts = function(attr, keepKey) { + if(keepKey) { + return [attr]; + } + return can.isArray(attr) ? attr : (""+attr).split("."); + }, + makeBindSetup = function(wildcard){ + return function(){ + var parent = this; + this._each(function(child, prop){ + if(child && child.bind){ + bindToChildAndBubbleToParent(child, wildcard || prop, parent) + } + }) + }; + }; + + /** + * @add can.Map + */ + // + var Map = can.Map = can.Observe = can.Construct.extend( { + /** + * @static + */ + setup: function(){ + + can.Construct.setup.apply( this, arguments ); + + + if(can.Map){ + if(!this.defaults){ + this.defaults = {}; + } + for(var prop in this.prototype){ + if(typeof this.prototype[prop] !== "function"){ + this.defaults[prop] = this.prototype[prop]; + } + } + } + // if we inerit from can.Map, but not can.List + if(can.List && !(this.prototype instanceof can.List) ){ + this.List = Map.List({ Map : this }, {}); + } + + }, + // keep so it can be overwritten + bind : can.bindAndSetup, + unbind: can.unbindAndTeardown, + id: "id", + helpers: { + canMakeObserve : function( obj ) { + return obj && !can.isDeferred(obj) && (can.isArray(obj) || can.isPlainObject( obj ) || ( obj instanceof can.Map )); + }, + unhookup: function(items, namespace){ + return can.each(items, function(item){ + if(item && item.unbind){ + item.unbind("change" + namespace); + } + }); + }, + // Listens to changes on `child` and "bubbles" the event up. + // `child` - The object to listen for changes on. + // `prop` - The property name is at on. + // `parent` - The parent object of prop. + // `ob` - (optional) The Map object constructor + // `list` - (optional) The observable list constructor + hookupBubble: function( child, prop, parent, Ob, List ) { + Ob = Ob || Map; + List = List || Map.List; + + // If it's an `array` make a list, otherwise a child. + if (child instanceof Map){ + // We have an `map` already... + // Make sure it is not listening to this already + // It's only listening if it has bindings already. + parent._bindings && Map.helpers.unhookup([child], parent._cid); + } else if ( can.isArray(child) ) { + child = new List(child); + } else { + child = new Ob(child); + } + // only listen if something is listening to you + if(parent._bindings){ + // Listen to all changes and `batchTrigger` upwards. + bindToChildAndBubbleToParent(child, prop, parent) + } + + + return child; + }, + // A helper used to serialize an `Map` or `Map.List`. + // `map` - The observable. + // `how` - To serialize with `attr` or `serialize`. + // `where` - To put properties, in an `{}` or `[]`. + serialize: function( map, how, where ) { + // Go through each property. + map.each(function( val, name ) { + // If the value is an `object`, and has an `attrs` or `serialize` function. + where[name] = Map.helpers.canMakeObserve(val) && can.isFunction( val[how] ) ? + // Call `attrs` or `serialize` to get the original data back. + val[how]() : + // Otherwise return the value. + val; + }); + return where; + }, + makeBindSetup: makeBindSetup + }, + + // starts collecting events + // takes a callback for after they are updated + // how could you hook into after ejs + /** + * @function can.Map.keys keys + * @parent can.Map.static + * @description Iterate over the keys of an Map. + * @signature `can.Map.keys(map)` + * @param {can.Map} map the `can.Map` to get the keys from + * @return {Array} array An array containing the keys from _observe_. + * + * @body + * `keys` iterates over an map to get an array of its keys. + * + * @codestart + * var people = new can.Map({ + * a: 'Alice', + * b: 'Bob', + * e: 'Eve' + * }); + * + * can.Map.keys(people); // ['a', 'b', 'e'] + * @codeend + */ + keys: function(map) { + var keys = []; + Map.__reading && Map.__reading(map, '__keys'); + for(var keyName in map._data) { + keys.push(keyName); + } + return keys; + } + }, + /** + * @prototype + */ + { + setup: function( obj ) { + // `_data` is where we keep the properties. + this._data = {} + /** + * @property {String} can.Map.prototype._cid + * @hide + * + * A globally unique ID for this `can.Map` instance. + */ + // The namespace this `object` uses to listen to events. + can.cid(this, ".map"); + // Sets all `attrs`. + this._init = 1; + this._setupComputes(); + var data = can.extend( can.extend(true,{},this.constructor.defaults || {}), obj ) + this.attr(data); + this.bind('change'+this._cid,can.proxy(this._changes,this)); + delete this._init; + }, + _setupComputes: function(){ + var prototype = this.constructor.prototype + for(var prop in prototype){ + if(prototype[prop] && prototype[prop].isComputed){ + this[prop] = prototype[prop].clone(this); + } + } + }, + _bindsetup: makeBindSetup(), + _bindteardown: function(){ + var cid = this._cid; + this._each(function(child){ + Map.helpers.unhookup([child], cid) + }) + }, + _changes: function(ev, attr, how,newVal, oldVal){ + can.batch.trigger(this, {type:attr, batchNum: ev.batchNum}, [newVal,oldVal]); + }, + _triggerChange: function(attr, how,newVal, oldVal){ + can.batch.trigger(this,"change",can.makeArray(arguments)) + }, + // no live binding iterator + _each: function(callback){ + var data = this.__get(); + for(var prop in data){ + if(data.hasOwnProperty(prop)){ + callback(data[prop],prop) + } + } + }, + /** + * @function can.Map.prototype.attr attr + * @description Get or set properties on an Map. + * @signature `map.attr()` + * + * Gets a collection of all the properties in this `can.Map`. + * + * @return {Object} an object with all the properties in this `can.Map`. + * + * @signature `map.attr(key)` + * + * Reads a property from this `can.Map`. + * + * @param {String} key the property to read + * @return {*} the value assigned to _key_. + * + * @signature `map.attr(key, value)` + * + * Assigns _value_ to a property on this `can.Map` called _key_. + * + * @param {String} key the property to set + * @param {*} the value to assign to _key_. + * @return {can.Map} this Map, for chaining + * + * @signature `map.attr(obj[, removeOthers])` + * + * Assigns each value in _obj_ to a property on this `can.Map` named after the + * corresponding key in _obj_, effectively merging _obj_ into the Map. + * + * @param {Object} obj a collection of key-value pairs to set. + * If any properties already exist on the `can.Map`, they will be overwritten. + * + * @param {bool} [removeOthers=false] whether to remove keys not present in _obj_. + * To remove keys without setting other keys, use `[can.Map::removeAttr removeAttr]`. + * + * @return {can.Map} this Map, for chaining + * + * @body + * `attr` gets or sets properties on the `can.Map` it's called on. Here's a tour through + * how all of its forms work: + * + * @codestart + * var people = new can.Map({}); + * + * // set a property: + * people.attr('a', 'Alex'); + * + * // get a property: + * people.attr('a'); // 'Alex' + * + * // set and merge multiple properties: + * people.attr({ + * a: 'Alice', + * b: 'Bob' + * }); + * + * // get all properties: + * people.attr(); // {a: 'Alice', b: 'Bob'} + * + * // set properties while removing others: + * people.attr({ + * b: 'Bill', + * e: 'Eve' + * }, true); + * + * people.attr(); // {b: 'Bill', e: 'Eve'} + * @codeend + * + * ## Deep properties + * + * `attr` can also set and read deep properties. All you have to do is specify + * the property name as you normally would if you weren't using `attr`. + * + * @codestart + * var people = new can.Map({names: {}}); + * + * // set a property: + * people.attr('names.a', 'Alice'); + * + * // get a property: + * people.attr('names.a'); // 'Alice' + * people.names.attr('a'); // 'Alice' + * + * // get all properties: + * people.attr(); // {names: {a: 'Alice'}} + * @codeend + * + * Objects that are added to Observes become Observes themselves behind the scenes, + * so changes to deep properties fire events at each level, and you can bind at any + * level. As this example shows, all the same events are fired no matter what level + * you call `attr` at: + * + * @codestart + * var people = new can.Map({names: {}}); + * + * people.bind('change', function(ev, attr, how, newVal, oldVal) { + * console.log('people change: ' + attr + ', ' + how + ', ' + newVal + ', ' + oldVal); + * }); + * + * people.names.bind('change', function(ev, attr, how, newVal, oldVal) { + * console.log('people.names change' + attr + ', ' + how + ', ' + newVal + ', ' + oldVal); + * }); + * + * people.bind('names', function(ev, newVal, oldVal) { + * console.log('people names: ' + newVal + ', ' + oldVal); + * }); + * + * people.names.bind('a', function(ev, newVal, oldVal) { + * console.log('people.names a: ' + newVal + ', ' + oldVal); + * }); + * + * people.bind('names.a', function(ev, newVal, oldVal) { + * console.log('people names.a: ' + newVal + ', ' + oldVal); + * }); + * + * people.attr('names.a', 'Alice'); // people change: names.a, add, Alice, undefined + * // people.names change: a, add, Alice, undefined + * // people.names a: Alice, undefined + * // people names.a: Alice, undefined + * + * people.names.attr('b', 'Bob'); // people change: names.b, add, Bob, undefined + * // people.names change: b, add, Bob, undefined + * // people.names b: Bob, undefined + * // people names.b: Bob, undefined + * @codeend + * + * ## See also + * + * For information on the events that are fired on property changes and how + * to listen for those events, see [can.Map.prototype.bind bind]. + */ + attr: function( attr, val ) { + // This is super obfuscated for space -- basically, we're checking + // if the type of the attribute is not a `number` or a `string`. + var type = typeof attr; + if ( type !== "string" && type !== "number" ) { + return this._attrs(attr, val) + } else if ( arguments.length === 1 ) {// If we are getting a value. + // Let people know we are reading. + Map.__reading && Map.__reading(this, attr) + return this._get(attr) + } else { + // Otherwise we are setting. + this._set(attr, val); + return this; + } + }, + /** + * @function can.Map.prototype.each each + * @description Call a function on each property of an Map. + * @signature `map.each( callback(item, propName ) )` + * + * `each` iterates through the Map, calling a function + * for each property value and key. + * + * @param {function(*,String)} callback(item,propName) the function to call for each property + * The value and key of each property will be passed as the first and second + * arguments, respectively, to the callback. If the callback returns false, + * the loop will stop. + * + * @return {can.Map} this Map, for chaining + * + * @body + * @codestart + * var names = []; + * new can.Map({a: 'Alice', b: 'Bob', e: 'Eve'}).each(function(value, key) { + * names.push(value); + * }); + * + * names; // ['Alice', 'Bob', 'Eve'] + * + * names = []; + * new can.Map({a: 'Alice', b: 'Bob', e: 'Eve'}).each(function(value, key) { + * names.push(value); + * if(key === 'b') { + * return false; + * } + * }); + * + * names; // ['Alice', 'Bob'] + * + * @codeend + */ + each: function() { + Map.__reading && Map.__reading(this, '__keys'); + return can.each.apply(undefined, [this.__get()].concat(can.makeArray(arguments))) + }, + /** + * @function can.Map.prototype.removeAttr removeAttr + * @description Remove a property from an Map. + * @signature `map.removeAttr(attrName)` + * @param {String} attrName the name of the property to remove + * @return {*} the value of the property that was removed + * + * @body + * `removeAttr` removes a property by name from an Map. + * + * @codestart + * var people = new can.Map({a: 'Alice', b: 'Bob', e: 'Eve'}); + * + * people.removeAttr('b'); // 'Bob' + * people.attr(); // {a: 'Alice', e: 'Eve'} + * @codeend + * + * Removing an attribute will cause a _change_ event to fire with `'remove'` + * passed as the _how_ parameter and `undefined` passed as the _newVal_ to + * handlers. It will also cause a _property name_ event to fire with `undefined` + * passed as _newVal_. An in-depth description at these events can be found + * under `[can.Map.prototype.attr attr]`. + */ + removeAttr: function( attr ) { + // Info if this is List or not + var isList = this instanceof can.List, + // Convert the `attr` into parts (if nested). + parts = attrParts(attr), + // The actual property to remove. + prop = parts.shift(), + // The current value. + current = isList ? this[prop] : this._data[prop]; + + // If we have more parts, call `removeAttr` on that part. + if ( parts.length ) { + return current.removeAttr(parts) + } else { + if(isList) { + this.splice(prop, 1) + } else if( prop in this._data ){ + // Otherwise, `delete`. + delete this._data[prop]; + // Create the event. + if (!(prop in this.constructor.prototype)) { + delete this[prop] + } + // Let others know the number of keys have changed + can.batch.trigger(this, "__keys"); + this._triggerChange(prop, "remove", undefined, current); + + } + return current; + } + }, + // Reads a property from the `object`. + _get: function( attr ) { + var value = typeof attr === 'string' && !!~attr.indexOf('.') && this.__get(attr); + if(value) { + return value; + } + + // break up the attr (`"foo.bar"`) into `["foo","bar"]` + var parts = attrParts(attr), + // get the value of the first attr name (`"foo"`) + current = this.__get(parts.shift()); + // if there are other attributes to read + return parts.length ? + // and current has a value + current ? + // lookup the remaining attrs on current + current._get(parts) : + // or if there's no current, return undefined + undefined + : + // if there are no more parts, return current + current; + }, + // Reads a property directly if an `attr` is provided, otherwise + // returns the "real" data object itself. + __get: function( attr ) { + if(attr){ + if(this[attr] && this[attr].isComputed){ + return this[attr]() + } else { + return this._data[attr] + } + } else { + return this._data; + } + }, + // Sets `attr` prop as value on this object where. + // `attr` - Is a string of properties or an array of property values. + // `value` - The raw value to set. + _set: function( attr, value, keepKey) { + // Convert `attr` to attr parts (if it isn't already). + var parts = attrParts(attr, keepKey), + // The immediate prop we are setting. + prop = parts.shift(), + // The current value. + current = this.__get(prop); + + // If we have an `object` and remaining parts. + if ( Map.helpers.canMakeObserve(current) && parts.length ) { + // That `object` should set it (this might need to call attr). + current._set(parts, value) + } else if (!parts.length ) { + // We're in "real" set territory. + if(this.__convert){ + value = this.__convert(prop, value) + } + this.__set(prop, value, current) + } else { + throw "can.Map: Object does not exist" + } + }, + __set : function(prop, value, current){ + + // Otherwise, we are setting it on this `object`. + // TODO: Check if value is object and transform + // are we changing the value. + if ( value !== current ) { + // Check if we are adding this for the first time -- + // if we are, we need to create an `add` event. + var changeType = this.__get().hasOwnProperty(prop) ? "set" : "add"; + + // Set the value on data. + this.___set(prop, + + // If we are getting an object. + Map.helpers.canMakeObserve(value) ? + + // Hook it up to send event. + Map.helpers.hookupBubble(value, prop, this) : + // Value is normal. + value); + + if(changeType == "add"){ + // If there is no current value, let others know that + // the the number of keys have changed + + can.batch.trigger(this, "__keys", undefined); + + } + // `batchTrigger` the change event. + this._triggerChange(prop, changeType, value, current); + + //can.batch.trigger(this, prop, [value, current]); + // If we can stop listening to our old value, do it. + current && Map.helpers.unhookup([current], this._cid); + } + + }, + // Directly sets a property on this `object`. + ___set: function( prop, val ) { + + if(this[prop] && this[prop].isComputed){ + this[prop](val) + } + + this._data[prop] = val; + // Add property directly for easy writing. + // Check if its on the `prototype` so we don't overwrite methods like `attrs`. + if (!(can.isFunction(this.constructor.prototype[prop]))) { + this[prop] = val + } + }, + + /** + * @function can.Map.prototype.bind bind + * @description Bind event handlers to an Map. + * + * @signature `map.bind(eventType, handler)` + * + * @param {String} eventType the type of event to bind this handler to + * @param {Function} handler the handler to be called when this type of event fires + * The signature of the handler depends on the type of event being bound. See below + * for details. + * @return {can.Map} this Map, for chaining + * + * @body + * `bind` binds event handlers to property changes on `can.Map`s. When you change + * a property using `attr`, two events are fired on the Map, allowing other parts + * of your application to map the changes to the object. + * + * ## The _change_ event + * + * The first event that is fired is the _change_ event. The _change_ event is useful + * if you want to react to all changes on an Map. + * + * @codestart + * var o = new can.Map({}); + * o.bind('change', function(ev, attr, how, newVal, oldVal) { + * console.log('Something changed.'); + * }); + * @codeend + * + * The parameters of the event handler for the _change_ event are: + * + * - _ev_ The event object. + * - _attr_ Which property changed. + * - _how_ Whether the property was added, removed, or set. Possible values are `'add'`, `'remove'`, or `'set'`. + * - _newVal_ The value of the property after the change. `newVal` will be `undefined` if the property was removed. + * - _oldVal_ Thishe value of the property before the change. `oldVal` will be `undefined` if the property was added. + * + * Here is a concrete tour through the _change_ event handler's arguments: + * + * @codestart + * var o = new can.Map({}); + * o.bind('change', function(ev, attr, how, newVal, oldVal) { + * console.log(ev + ', ' + attr + ', ' + how + ', ' + newVal + ', ' + oldVal); + * }); + * + * o.attr('a', 'Alexis'); // [object Object], a, add, Alexis, undefined + * o.attr('a', 'Adam'); // [object Object], a, set, Adam, Alexis + * o.attr({ + * 'a': 'Alice', // [object Object], a, set, Alice, Adam + * 'b': 'Bob' // [object Object], b, add, Bob, undefined + * }); + * o.removeAttr('a'); // [object Object], a, remove, undefined, Alice + * @codeend + * + * (See also `[can.Map::removeAttr removeAttr]`, which removes properties). + * + * ## The _property name_ event + * + * The second event that is fired is an event whose type is the same as the changed + * property's name. This event is useful for noticing changes to a specific property. + * + * @codestart + * var o = new can.Map({}); + * o.bind('a', function(ev, newVal, oldVal) { + * console.log('The value of a changed.'); + * }); + * @codeend + * + * The parameters of the event handler for the _property name_ event are: + * + * - _ev_ The event object. + * - _newVal_ The value of the property after the change. `newVal` will be `undefined` if the property was removed. + * - _oldVal_ The value of the property before the change. `oldVal` will be `undefined` if the property was added. + * + * Here is a concrete tour through the _property name_ event handler's arguments: + * + * @codestart + * var o = new can.Map({}); + * o.bind('a', function(ev, newVal, oldVal) { + * console.log(ev + ', ' + newVal + ', ' + oldVal); + * }); + * + * o.attr('a', 'Alexis'); // [object Object], Alexis, undefined + * o.attr('a', 'Adam'); // [object Object], Adam, Alexis + * o.attr({ + * 'a': 'Alice', // [object Object], Alice, Adam + * 'b': 'Bob' + * }); + * o.removeAttr('a'); // [object Object], undefined, Alice + * @codeend + * + * ## See also + * + * More information about changing properties on Observes can be found under + * [can.Map.prototype.attr attr]. + * + * For a more specific way to changes on Observes, see the [can.Map.delegate] plugin. + */ + bind: can.bindAndSetup, + /** + * @function can.Map.prototype.unbind unbind + * @description Unbind event handlers from an Map. + * @signature `map.unbind(eventType[, handler])` + * @param {String} eventType the type of event to unbind, exactly as passed to `bind` + * @param {Function} [handler] the handler to unbind + * + * @body + * `unbind` unbinds event handlers previously bound with [can.Map.prototype.bind|`bind`]. + * If no _handler_ is passed, all handlers for the given event type will be unbound. + * + * @codestart + * var i = 0, + * increaseBy2 = function() { i += 2; }, + * increaseBy3 = function() { i += 3; }, + * o = new can.Map(); + * + * o.bind('change', increaseBy2); + * o.bind('change', increaseBy3); + * o.attr('a', 'Alice'); + * i; // 5 + * + * o.unbind('change', increaseBy2); + * o.attr('b', 'Bob'); + * i; // 8 + * + * o.unbind('change'); + * o.attr('e', 'Eve'); + * i; // 8 + * @codeend + */ + unbind: can.unbindAndTeardown, + /** + * @function can.Map.prototype.serialize serialize + * @description Serialize this object to something that + * can be passed to `JSON.stringify`. + * @signature `map.serialize()` + * + * + * Get the serialized Object form of the map. Serialized + * data is typically used to send back to a server. + * + * o.serialize() //-> { name: 'Justin' } + * + * Serialize currently returns the same data + * as [can.Map.prototype.attrs]. However, in future + * versions, serialize will be able to return serialized + * data similar to [can.Model]. The following will work: + * + * new Map({time: new Date()}) + * .serialize() //-> { time: 1319666613663 } + * + * @return {Object} a JavaScript Object that can be + * serialized with `JSON.stringify` or other methods. + * + */ + serialize: function() { + return can.Map.helpers.serialize(this, 'serialize', {}); + }, + /** + * @hide + * Set multiple properties on the observable + * @param {Object} props + * @param {Boolean} remove true if you should remove properties that are not in props + */ + _attrs: function( props, remove ) { + + if ( props === undefined ) { + return Map.helpers.serialize(this, 'attr', {}) + } + + props = can.extend({}, props); + var prop, + self = this, + newVal; + can.batch.start(); + this.each(function(curVal, prop){ + newVal = props[prop]; + + // If we are merging... + if ( newVal === undefined ) { + remove && self.removeAttr(prop); + return; + } + + if(self.__convert){ + newVal = self.__convert(prop, newVal) + } + + // if we're dealing with models, want to call _set to let converter run + if( newVal instanceof can.Map ) { + self.__set(prop, newVal, curVal) + // if its an object, let attr merge + } else if ( Map.helpers.canMakeObserve(curVal) && Map.helpers.canMakeObserve(newVal) && curVal.attr ) { + curVal.attr(newVal, remove) + // otherwise just set + } else if ( curVal != newVal ) { + self.__set(prop, newVal, curVal) + } + + delete props[prop]; + }) + // Add remaining props. + for ( var prop in props ) { + newVal = props[prop]; + this._set(prop, newVal, true) + } + can.batch.stop() + return this; + }, + + /** + * @function can.Map.prototype.compute compute + * @description Make a can.compute from an observable property. + * @signature `map.compute(attrName)` + * @param {String} attrName the property to bind to + * @return {can.compute} a [can.compute] bound to _attrName_ + * + * @body + * `compute` is a convenience method for making computes from properties + * of Observes. More information about computes can be found under [can.compute]. + * + * @codestart + * var map = new can.Map({a: 'Alexis'}); + * var name = map.compute('a'); + * name.bind('change', function(ev, nevVal, oldVal) { + * console.log('a changed from ' + oldVal + 'to' + newName + '.'); + * }); + * + * name(); // 'Alexis' + * + * map.attr('a', 'Adam'); // 'a changed from Alexis to Adam.' + * name(); // 'Adam' + * + * name('Alice'); // 'a changed from Adam to Alice.' + * name(); // 'Alice' + */ + compute: function(prop) { + if(can.isFunction( this.constructor.prototype[prop] )){ + return can.compute(this[prop], this); + } else { + return can.compute(this,prop); + } + + } + }); + + return Map; +}); diff --git a/observe/observe.md b/map/map.md similarity index 74% rename from observe/observe.md rename to map/map.md index 1267453e891..47fae50ebcf 100644 --- a/observe/observe.md +++ b/map/map.md @@ -1,39 +1,39 @@ -@constructor can.Observe +@constructor can.Map @inherits can.Construct @parent canjs -@group can.Observe.plugins plugins -@test can/observe/test.html -@plugin can/observe +@group can.Map.plugins plugins +@test can/map/test.html +@plugin can/map @description Create observable objects. -@signature `new can.Observe([props])` +@signature `new can.Map([props])` @param {Object} [props] Properties and values to seed the Observe with. -@return {can.Observe} An instance of `can.Observe` with the properties from _props_. +@return {can.Map} An instance of `can.Map` with the properties from _props_. -@signature `can.Observe([name,] [staticProperties,] instanceProperties)` +@signature `can.Map([name,] [staticProperties,] instanceProperties)` Creates a new extended constructor function. This is deprecated. In CanJS 1.2, by default, calling the constructor function -without `new` will create a `new` instance. Use [can.Construct.extend can.Observe.extend] +without `new` will create a `new` instance. Use [can.Construct.extend can.Map.extend] instead of calling the constructor to extend. @body -`can.Observe` provides a way for you to listen for and keep track of changes -to objects. When you use the getters and setters provided by `can.Observe`, -events are fired that you can react to. `can.Observe` also has support for +`can.Map` provides a way for you to listen for and keep track of changes +to objects. When you use the getters and setters provided by `can.Map`, +events are fired that you can react to. `can.Map` also has support for working with deep properties. Observable arrays are also available with -`[can.Observe.List]`, which is based on `can.Observe`. +`[can.List]`, which is based on `can.Map`. ## Working with Observes -To create an Observe, use `new can.Observe([props])`. This will return a +To create an Observe, use `new can.Map([props])`. This will return a copy of `props` that emits events when its properties are changed with -`[can.Observe.prototype.attr attr]`. +`[can.Map.prototype.attr attr]`. You can read the values of properties on Observes directly, but you should never set them directly. You can also read property values using `attr`. @@ -43,7 +43,7 @@ live-binding properties in an [can.EJS EJS] template. (If you are using @codestart var aName = {a: 'Alexis'}, - observe = can.Observe(aName); + observe = can.Map(aName); // Observes are copies of data: aName === observe; // false @@ -66,7 +66,7 @@ observe.a = 'Adam'; // wrong! @codeend Find out more about manipulating properties of Observes under -[can.Observe.protoype.attr attr] and [can.Observe.protoype.removeAtt removeAttr]. +[can.Map.protoype.attr attr] and [can.Map.protoype.removeAtt removeAttr]. ## Listening to changes @@ -74,12 +74,12 @@ The real power of observable objects comes from being able to react to properties being added, set, and removed. Observes emit events when properties are changed that you can bind to. -`can.Observe` has two types of events that fire due to changes on an Observe: +`can.Map` has two types of events that fire due to changes on an Observe: - the _change_ event fires on every change to an Observe. - an event named after the property name fires on every change to that property. @codestart -var o = new can.Observe({}); +var o = new can.Map({}); o.bind('change', function(ev, attr, how, newVal, oldVal) { console.log('Something on o changed.'); }); @@ -98,12 +98,12 @@ o.removeAttr('a'); // 'Something on o changed.' // 'a was changed.' @codeend -For more detail on how to use these events, see [can.Observe.prototype.bind bind] and -[can.Observe.prototype.unbind unbind]. There is also a plugin called [can.Observe.delegate] +For more detail on how to use these events, see [can.Map.prototype.bind bind] and +[can.Map.prototype.unbind unbind]. There is also a plugin called [can.Map.delegate] that makes binding to specific types of events easier: @codestart -var o = new can.Observe({}); +var o = new can.Map({}); o.delegate('a', 'add' function(ev, newVal, oldVal) { console.log('a was added.'); }); diff --git a/map/map_test.js b/map/map_test.js new file mode 100644 index 00000000000..94f12f8b18e --- /dev/null +++ b/map/map_test.js @@ -0,0 +1,52 @@ +(function(undefined) { + +module('can/map') + +test("Basic Map",4,function(){ + + var state = new can.Map({ + category : 5, + productType : 4 + }); + + var added; + + state.bind("change", function(ev, attr, how, val, old){ + equal(attr, "category", "correct change name") + equal(how, "set") + equal(val,6, "correct") + equal(old,5, "correct") + }); + + + + state.attr("category",6); + + state.unbind("change"); + + +}); + +test("Nested Map", 5, function(){ + var me = new can.Map({ + name : {first: "Justin", last: "Meyer"} + }); + + ok(me.attr("name") instanceof can.Map); + + me.bind("change", function(ev, attr, how, val, old){ + equal(attr, "name.first", "correct change name") + equal(how, "set") + equal(val,"Brian", "correct") + equal(old,"Justin", "correct") + }) + + me.attr("name.first","Brian"); + + me.unbind("change") + +}) + + + +})(); diff --git a/observe/setter/setter-paginate.html b/map/setter/setter-paginate.html similarity index 97% rename from observe/setter/setter-paginate.html rename to map/setter/setter-paginate.html index 3ec62becced..41a99efa433 100644 --- a/observe/setter/setter-paginate.html +++ b/map/setter/setter-paginate.html @@ -22,7 +22,7 @@ steal.config({ root: '../../' }); -}).then('can/observe/setter', 'can/control') +}).then('can/map/setter', 'can/control') .then('can/model') .then(function(){ diff --git a/observe/setter/setter.html b/map/setter/setter.html similarity index 95% rename from observe/setter/setter.html rename to map/setter/setter.html index ae781f0ff82..6201d8fc3c1 100644 --- a/observe/setter/setter.html +++ b/map/setter/setter.html @@ -16,7 +16,7 @@ diff --git a/observe/sort/sort.js b/map/sort/sort.js similarity index 91% rename from observe/sort/sort.js rename to map/sort/sort.js index a6266b189c9..b09b3944626 100644 --- a/observe/sort/sort.js +++ b/map/sort/sort.js @@ -1,6 +1,6 @@ -steal('can/util', 'can/observe', function(can) { +steal('can/util', 'can/map', function(can) { -var proto = can.Observe.List.prototype, +var proto = can.List.prototype, _changes = proto._changes, setup = proto.setup; @@ -59,7 +59,7 @@ can.each({ * @function push * Add items to the end of the list. * - * var l = new can.Observe.List([]); + * var l = new can.List([]); * * l.bind('change', function( * ev, // the change event @@ -81,9 +81,9 @@ can.each({ /** * @function unshift * Add items to the start of the list. This is very similar to - * [can.Observe.List::push]. Example: + * [can.List::push]. Example: * - * var l = new can.Observe.List(["a","b"]); + * var l = new can.List(["a","b"]); * l.unshift(1,2,3) //-> 5 * l.attr() //-> [1,2,3,"a","b"] * @@ -96,7 +96,7 @@ can.each({ // - name - method name // - where - where items in the array should be added function( where , name ) { - var proto = can.Observe.List.prototype, + var proto = can.List.prototype, old = proto[name]; proto[name] = function() { @@ -115,7 +115,7 @@ function( where , name ) { // undefined - the old value if ( this.comparator && args.length ) { this.sort(null, true); - can.Observe.triggerBatch(this,"reset", [args]); + can.batch.trigger(this,"reset", [args]); this._triggerChange(""+len, "add", args, undefined); } @@ -166,5 +166,5 @@ proto.setup = function( instances, options ) { } }; -return can.Observe; +return can.Map; }); diff --git a/observe/sort/sort.md b/map/sort/sort.md similarity index 58% rename from observe/sort/sort.md rename to map/sort/sort.md index 7e57b38ceec..463d60ffbf0 100644 --- a/observe/sort/sort.md +++ b/map/sort/sort.md @@ -1,13 +1,13 @@ -@page can.Observe.List.prototype.sort -@parent can.Observe.List.prototype -@plugin can/observe/sort -@test can/observe/sort/test.html +@page can.List.prototype.sort +@parent can.List.prototype +@plugin can/map/sort +@test can/map/sort/test.html `list.sort(sortfunc)` Sorts the instances in the list. - var list = new can.Observe.List([ + var list = new can.List([ { name: 'Justin' }, { name: 'Brian' }, { name: 'Austin' }, diff --git a/observe/sort/sort_test.js b/map/sort/sort_test.js similarity index 93% rename from observe/sort/sort_test.js rename to map/sort/sort_test.js index 828c8aaf6e2..613e98e0cd0 100644 --- a/observe/sort/sort_test.js +++ b/map/sort/sort_test.js @@ -1,10 +1,10 @@ (function() { -module("can/observe/sort"); +module("can/map/sort"); test("list events", 16, function(){ - var list = new can.Observe.List([ + var list = new can.List([ {name: 'Justin'}, {name: 'Brian'}, {name: 'Austin'}, @@ -51,7 +51,7 @@ test("list events", 16, function(){ test("list sort with func", 1, function(){ - var list = new can.Observe.List([ + var list = new can.List([ {priority: 4, name: "low"}, {priority: 1, name: "high"}, {priority: 2, name: "middle"}, @@ -70,7 +70,7 @@ test("list sort with func", 1, function(){ test("live binding with comparator (#170)", function() { var renderer = can.view.mustache(''), el = document.createElement('div'), - items = new can.Observe.List([{ + items = new can.List([{ text : 'First' }]); diff --git a/observe/sort/test.html b/map/sort/test.html similarity index 86% rename from observe/sort/test.html rename to map/sort/test.html index fd258c2e0c2..c316321c182 100644 --- a/observe/sort/test.html +++ b/map/sort/test.html @@ -17,11 +17,11 @@

    diff --git a/map/test.html b/map/test.html new file mode 100644 index 00000000000..8d67c024f28 --- /dev/null +++ b/map/test.html @@ -0,0 +1,25 @@ + + + + + + +

    can.Observe Test Suite

    + +

    + +
    +

    +
      +
      + + + + + + \ No newline at end of file diff --git a/observe/transaction/qunit.html b/map/transaction/qunit.html similarity index 80% rename from observe/transaction/qunit.html rename to map/transaction/qunit.html index 8b49497351b..b5441542d6d 100644 --- a/observe/transaction/qunit.html +++ b/map/transaction/qunit.html @@ -12,7 +12,7 @@

      \ No newline at end of file diff --git a/observe/transaction/transaction.js b/map/transaction/transaction.js similarity index 79% rename from observe/transaction/transaction.js rename to map/transaction/transaction.js index f2ae51e27d0..45098c4149d 100644 --- a/observe/transaction/transaction.js +++ b/map/transaction/transaction.js @@ -5,7 +5,7 @@ steal('can', function(can){ var events = [], transactionCount = 0, - originalBatchTrigger = can.Observe.triggerBatch, + originalBatchTrigger = can.batch.trigger, changedBatchTrigger = function(obj, ev){ originalBatchTrigger.apply(this, arguments); if(ev === "change"){ @@ -23,11 +23,11 @@ steal('can', function(can){ } }; - can.Observe.triggerBatch = changedBatchTrigger; + can.batch.trigger = changedBatchTrigger; can.transaction = function(){ if( transactionCount === 0 ) { - can.Observe.triggerBatch = recordingBatchTrigger; + can.batch.trigger = recordingBatchTrigger; } @@ -39,7 +39,7 @@ steal('can', function(can){ if( transactionCount === 0 ) { var myEvents = events.slice(0) events = []; - can.Observe.triggerBatch = changedBatchTrigger; + can.batch.trigger = changedBatchTrigger; can.each(myEvents, function(eventArgs){ originalBatchTrigger.apply(can, eventArgs); }); @@ -47,6 +47,6 @@ steal('can', function(can){ } }; - return can.Observe; + return can.Map; }); diff --git a/observe/transaction/transaction_test.js b/map/transaction/transaction_test.js similarity index 90% rename from observe/transaction/transaction_test.js rename to map/transaction/transaction_test.js index cc6aa1d8bef..9cceff4f7e2 100644 --- a/observe/transaction/transaction_test.js +++ b/map/transaction/transaction_test.js @@ -1,8 +1,8 @@ -module('can/observe/transaction') +module('can/map/transaction') test("Basic Transaction",function(){ stop(); - var obs = new can.Observe({ + var obs = new can.Map({ first: "justin", last: "meyer" }); diff --git a/observe/validations/model_validations.html b/map/validations/model_validations.html similarity index 88% rename from observe/validations/model_validations.html rename to map/validations/model_validations.html index a18b21adcab..b1a2fd7b580 100644 --- a/observe/validations/model_validations.html +++ b/map/validations/model_validations.html @@ -3,7 +3,7 @@ diff --git a/observe/validations/validations.html b/map/validations/validations.html similarity index 97% rename from observe/validations/validations.html rename to map/validations/validations.html index c949f14d81b..81329258d76 100644 --- a/observe/validations/validations.html +++ b/map/validations/validations.html @@ -29,8 +29,8 @@

      Observe Validations Demo

      Person <%= person.attr('name') %> is <%= person.ageThisYear() %> years old.

      diff --git a/model/test/associations_test.js b/model/test/associations_test.js index 802edcb2b9b..1efbad5df22 100644 --- a/model/test/associations_test.js +++ b/model/test/associations_test.js @@ -112,7 +112,7 @@ test("Model.List association serialize on save", function(){ cSave.then(function(customer){ start() ok(true, "called back") - equal(customer.loansAttr.constructor, can.Observe.List, "we get an observe list back") + equal(customer.loansAttr.constructor, can.List, "we get an observe list back") /*ok(customer.loansAttr._namespace === undefined, "_namespace does not exist"); ok(customer.loansAttr._data === undefined, "_data does not exist"); ok(customer.loansAttr._use_call === undefined, "_use_call does not exist"); diff --git a/observe/compute/compute_test.js b/observe/compute/compute_test.js deleted file mode 100644 index fc7d1d3b847..00000000000 --- a/observe/compute/compute_test.js +++ /dev/null @@ -1,532 +0,0 @@ -(function() { -module('can/observe/compute') - -test("Basic Compute",function(){ - - var o = new can.Observe({first: "Justin", last: "Meyer"}); - var prop = can.compute(function(){ - return o.attr("first") + " " +o.attr("last") - }) - - equal(prop(), "Justin Meyer"); - var handler = function(ev, newVal, oldVal){ - equal(newVal, "Brian Meyer") - equal(oldVal, "Justin Meyer") - } - prop.bind("change", handler); - - o.attr("first","Brian"); - - prop.unbind("change", handler) - o.attr("first","Brian"); -}); - - -test("compute on prototype", function(){ - - var Person = can.Observe({ - fullName: function(){ - return this.attr("first") + " " +this.attr("last") - } - }) - - var me = new Person({ - first : "Justin", - last : "Meyer" - }); - var fullName = can.compute( me.fullName, me ); - - equal(fullName(), "Justin Meyer"); - - var called = 0; - - fullName.bind("change", function( ev, newVal, oldVal ) { - called++; - equal(called, 1, "called only once"); - equal(newVal, "Justin Shah"); - equal(oldVal, "Justin Meyer") - }); - - me.attr('last',"Shah") - - // to make this work, we'd have to look for a computed function and bind to it's change ... - // maybe bind can just work this way? -}) - - -test("setter compute", function(){ - var project = new can.Observe({ - progress: 0.5 - }); - - // a setter compute that converts 50 to .5 and vice versa - var computed = can.compute(function(val){ - if(val) { - project.attr('progress', val / 100) - } else { - return parseInt( project.attr('progress') * 100 ); - } - }); - - equal(computed(), 50, "the value is right"); - computed(25); - equal(project.attr('progress'), 0.25); - equal(computed(),25 ); - - computed.bind("change", function(ev, newVal, oldVal){ - equal(newVal, 75); - equal(oldVal, 25) - }) - - computed(75); - -}) - -test("compute a compute", function() { - var project = new can.Observe({ - progress: 0.5 - }); - - var percent = can.compute(function(val){ - if(val) { - project.attr('progress', val / 100); - } else { - return parseInt( project.attr('progress') * 100, 10); - } - }); - percent.named = "PERCENT"; - - equal(percent(),50,'percent starts right'); - percent.bind('change',function() { - // noop - }); - - var fraction = can.compute(function(val) { - if(val) { - percent(parseInt(val.split('/')[0],10)); - } else { - return percent() + '/100'; - } - }); - fraction.named ="FRACTIOn" - - fraction.bind('change',function() { - // noop - }); - - equal(fraction(),'50/100','fraction starts right'); - - percent(25); - - equal(percent(),25); - equal(project.attr('progress'),0.25,'progress updated'); - equal(fraction(),'25/100','fraction updated'); - - fraction('15/100'); - - equal(fraction(),'15/100'); - equal(project.attr('progress'),0.15,'progress updated'); - equal(percent(),15,'% updated'); -}); - -test("compute with a simple compute", function() { - expect(4); - var a = can.compute(5); - var b = can.compute(function() { - return a() * 2; - }); - - equal(b(),10,'b starts correct'); - a(3); - equal(b(),6,'b updates'); - - b.bind('change',function() { - equal(b(),24,'b fires change'); - }); - a(12); - equal(b(),24,'b updates when bound'); -}); - - -test("empty compute", function(){ - var c = can.compute(); - c.bind("change", function(ev, newVal, oldVal){ - ok(oldVal === undefined, "was undefined") - ok(newVal === 0, "now zero") - }) - - c(0); - -}); - -test("only one update on a batchTransaction",function(){ - var person = new can.Observe({first: "Justin", last: "Meyer"}); - var func = function(){ - return person.attr('first')+" "+person.attr('last')+Math.random() - }; - var callbacks = 0; - can.compute.binder(func, window, function(newVal, oldVal){ - callbacks++; - }); - - person.attr({ - first: "Brian", - last: "Moschel" - }); - - equal(callbacks,1,"only one callback") -}) - -test("only one update on a start and end transaction",function(){ - var person = new can.Observe({first: "Justin", last: "Meyer"}), - age = can.compute(5); - var func = function(newVal,oldVal){ - return person.attr('first')+" "+person.attr('last')+age()+Math.random(); - }; - var callbacks = 0; - can.compute.binder(func, window, function(newVal, oldVal){ - callbacks++; - }); - - can.Observe.startBatch(); - - person.attr('first',"Brian"); - stop(); - setTimeout(function(){ - person.attr('last',"Moschel"); - age(12) - - can.Observe.stopBatch(); - - equal(callbacks,1,"only one callback") - - start(); - }) - - - -}) - -test("Compute emits change events when an embbedded observe has properties added or removed", 4, function() { - var obs = new can.Observe(), - compute1 = can.compute(function(){ - var txt = obs.attr('foo'); - obs.each(function(val){ - txt += val.toString(); - }); - return txt; - }); - - compute1.bind('change', function(ev, newVal, oldVal) { - ok(true, 'change handler fired: ' + newVal); - }) - // we're binding on adding / removing and foo - obs.attr('foo', 1); - obs.attr('bar', 2); - obs.attr('foo', 3); - obs.removeAttr('bar'); - obs.removeAttr('bar'); -}); - -test("compute only updates once when a list's contents are replaced",function(){ - - var list = new can.Observe.List([{name: "Justin"}]), - computedCount = 0; - - var compute = can.compute(function(){ - computedCount++; - list.each(function(item){ - item.attr('name') - }) - }) - equal(0,computedCount, "computes are not called until their value is read") - compute.bind("change", function(ev, newVal, oldVal){ - - }) - - equal(1,computedCount, "binding computes to store the value"); - list.replace([{name: "hank"}]); - equal(2,computedCount, "only one compute") - -}); - -test("Generate computes from Observes with can.Observe.prototype.compute (#203)", 6, function() { - var obs = new can.Observe({ - test : 'testvalue' - }); - - var compute = obs.compute('test'); - ok(compute.isComputed, '`test` is computed'); - equal(compute(), 'testvalue', 'Value is as expected'); - obs.attr('test', 'observeValue'); - equal(compute(), 'observeValue', 'Value is as expected'); - compute.bind('change', function(ev, newVal) { - equal(newVal, 'computeValue', 'new value from compute'); - }); - obs.bind('change', function(ev, name, how, newVal) { - equal(newVal, 'computeValue', 'Got new value from compute'); - }); - compute('computeValue'); - equal(compute(), 'computeValue', 'Got updated value'); -}); - -test("compute of computes", function(){ - expect(2) - var suggestedSearch = can.compute(null), - searchQuery = can.compute(''), - searchText = can.compute(function() { - var suggested = suggestedSearch(); - if(suggested) { - return suggested - } else { - return searchQuery(); - } - }); - - equal('',searchText(),"inital set"); - - searchText.bind("change", function(ev, newVal){ - equal(newVal,"food", "food set"); - }) - - - searchQuery("food") -}) - - -test("compute doesn't rebind and leak with 0 bindings", function() { - var state = new can.Observe({ - foo: "bar" - }); - var computedA = 0, computedB = 0; - var computeA = can.compute(function() { - computedA++; - return state.attr("foo") === "bar"; - }); - var computeB = can.compute(function() { - computedB++; - return state.attr("foo") === "bar" || 15; - }); - - function aChange(ev, newVal) { - if(newVal) { - computeB.bind("change.computeA", function() { - // noop - }); - } else { - computeB.unbind("change.computeA"); - } - } - - computeA.bind("change", aChange); - aChange(null, computeA()); - - equal(computedA, 1, "binding A computes the value"); - equal(computedB, 1, "A=true, so B is bound, computing the value"); - - state.attr("foo", "baz"); - equal(computedA, 2, "A recomputed and unbound B"); - equal(computedB, 1, "B was unbound, so not recomputed"); - - state.attr("foo", "bar"); - equal(computedA, 3, "A recomputed => true"); - equal(computedB, 2, "A=true so B is rebound and recomputed"); - - computeA.unbind("change", aChange); - computeB.unbind("change.computeA"); - state.attr("foo", "baz"); - equal(computedA, 3, "unbound, so didn't recompute A"); - equal(computedB, 2, "unbound, so didn't recompute B"); -}); - - -test("compute setter without external value", function(){ - - var age = can.compute(0,function(newVal, oldVal){ - var num = +newVal - if(! isNaN(num) && 0 <= num && num <= 120 ){ - return num; - } else { - return oldVal; - } - }) - equal(age(), 0, "initial value set"); - age.bind("change", function(ev, newVal, oldVal){ - equal(5, newVal) - age.unbind("change",arguments.callee) - }); - - age(5); - equal(age(), 5, "5 set") - - age("invalid"); - equal(age(), 5, "5 kept") - -}) - -test("compute value",function(){ - expect(9) - var input = { - value: 1 - } - - var value = can.compute("",{ - get: function(){ - return input.value; - }, - set: function(newVal){ - input.value = newVal; - //input.onchange && input.onchange(); - }, - on: function(update){ - input.onchange = update; - }, - off: function(){ - delete input.onchange; - } - }) - - equal(value(), 1, "original value"); - ok(!input.onchange, "nothing bound"); - value(2); - - equal(value(), 2, "updated value"); - - equal(input.value, 2, "updated input.value"); - - - - value.bind("change", function(ev, newVal, oldVal){ - equal(newVal, 3, "newVal"); - equal(oldVal, 2, "oldVal"); - value.unbind("change", arguments.callee); - }) - ok(input.onchange, "binding to onchange"); - - value(3); - ok(!input.onchange, "removed binding") - equal(value(), 3); -}); - -test("compute bound to observe",function(){ - var me = new can.Observe({name: "Justin"}); - - var bind = me.bind, - unbind = me.unbind, - bindCount = 0; - me.bind = function(){ - bindCount ++; - bind.apply(this,arguments); - } - me.unbind = function(){ - bindCount --; - unbind.apply(this,arguments); - } - - var name = can.compute(me,"name") - - equal(bindCount, 0); - equal(name(), "Justin"); - - var handler = function(ev, newVal, oldVal){ - equal(newVal, "Justin Meyer"); - equal(oldVal, "Justin") - } - - name.bind("change",handler) - - equal(bindCount, 1); - - name.unbind("change",handler); - - equal(bindCount, 0); -}); - -test("binding to a compute on an observe before reading",function(){ - var me = new can.Observe({name: "Justin"}); - - var name = can.compute(me,"name") - - - var handler = function(ev, newVal, oldVal){ - equal(newVal, "Justin Meyer"); - equal(oldVal, "Justin") - } - - name.bind("change",handler) - - equal(name(), "Justin"); -}) - -test("compute bound to input value",function(){ - var input = document.createElement('input'); - input.value = 'Justin'; - - var value = can.compute(input, "value","change") - - equal(value(),"Justin"); - - value("Justin M."); - - equal(input.value,"Justin M.","input change correctly"); - - - var handler = function(ev, newVal, oldVal){ - equal(newVal, "Justin Meyer"); - equal(oldVal, "Justin M.") - } - - value.bind("change", handler); - - - input.value = "Justin Meyer"; - - value.unbind("change", handler); - - input.value = "Brian Moschel"; - - equal(value(),"Brian Moschel"); - -}) - -test("compute on the prototype", function(){ - var Person = can.Map.extend({ - fullName: can.compute(function(fullName){ - if(arguments.length){ - var parts = fullName.split(" "); - this.attr({ - first:parts[0], - last: parts[1] - }) - } else { - return this.attr('first')+" "+this.attr('last') - } - - }) - }); - - var me = new Person(); - - var fn = me.attr({ - first: "Justin", - last: "Meyer" - }).attr("fullName"); - - equal(fn, "Justin Meyer", "can read attr") - - me.attr("fullName","Brian Moschel"); - - equal(me.attr("first"),"Brian","set first name"); - equal(me.attr("last"),"Moschel","set last name") - - -}) - - - - - - - - -})(); diff --git a/observe/observe.js b/observe/observe.js index b1cc3c9b427..1b19a60dc8b 100644 --- a/observe/observe.js +++ b/observe/observe.js @@ -1,2090 +1,4 @@ -// 1.69 -steal('can/util','can/util/bind','can/construct', function(can, bind) { - // ## observe.js - // `can.Observe` - // _Provides the observable pattern for JavaScript Objects._ - // - // Returns `true` if something is an object with properties of its own. - var canMakeObserve = function( obj ) { - return obj && !can.isDeferred(obj) && (can.isArray(obj) || can.isPlainObject( obj ) || ( obj instanceof can.Observe )); - }, - - // Removes all listeners. - unhookup = function(items, namespace){ - return can.each(items, function(item){ - if(item && item.unbind){ - item.unbind("change" + namespace); - } - }); - }, - // Listens to changes on `child` and "bubbles" the event up. - // `child` - The object to listen for changes on. - // `prop` - The property name is at on. - // `parent` - The parent object of prop. - // `ob` - (optional) The Observe object constructor - // `list` - (optional) The observable list constructor - hookupBubble = function( child, prop, parent, Ob, List ) { - Ob = Ob || Observe; - List = List || Observe.List; - - // If it's an `array` make a list, otherwise a child. - if (child instanceof Observe){ - // We have an `observe` already... - // Make sure it is not listening to this already - // It's only listening if it has bindings already. - parent._bindings &&unhookup([child], parent._cid); - } else if ( can.isArray(child) ) { - child = new List(child); - } else { - child = new Ob(child); - } - // only listen if something is listening to you - if(parent._bindings){ - // Listen to all changes and `batchTrigger` upwards. - bindToChildAndBubbleToParent(child, prop, parent) - } - - - return child; - }, - bindToChildAndBubbleToParent = function(child, prop, parent){ - child.bind("change" + parent._cid, - function( /* ev, attr */ ) { - // `batchTrigger` the type on this... - var args = can.makeArray(arguments), - ev = args.shift(); - args[0] = (prop === "*" ? - [ parent.indexOf( child ), args[0]] : - [ prop, args[0]] ).join("."); - - // track objects dispatched on this observe - ev.triggeredNS = ev.triggeredNS || {}; - - // if it has already been dispatched exit - if (ev.triggeredNS[parent._cid]) { - return; - } - - ev.triggeredNS[parent._cid] = true; - // send change event with modified attr to parent - can.trigger(parent, ev, args); - // send modified attr event to parent - //can.trigger(parent, args[0], args); - }); - }, - // An `id` to track events for a given observe. - observeId = 0, - // A helper used to serialize an `Observe` or `Observe.List`. - // `observe` - The observable. - // `how` - To serialize with `attr` or `serialize`. - // `where` - To put properties, in an `{}` or `[]`. - serialize = function( observe, how, where ) { - // Go through each property. - observe.each(function( val, name ) { - // If the value is an `object`, and has an `attrs` or `serialize` function. - where[name] = canMakeObserve(val) && can.isFunction( val[how] ) ? - // Call `attrs` or `serialize` to get the original data back. - val[how]() : - // Otherwise return the value. - val; - }); - return where; - }, - attrParts = function(attr, keepKey) { - if(keepKey) { - return [attr]; - } - return can.isArray(attr) ? attr : (""+attr).split("."); - }, - // Which batch of events this is for -- might not want to send multiple - // messages on the same batch. This is mostly for event delegation. - batchNum = 1, - // how many times has start been called without a stop - transactions = 0, - // an array of events within a transaction - batchEvents = [], - stopCallbacks = [], - makeBindSetup = function(wildcard){ - return function(){ - var parent = this; - this._each(function(child, prop){ - if(child && child.bind){ - bindToChildAndBubbleToParent(child, wildcard || prop, parent) - } - }) - }; - }; - - /** - * @add can.Observe - */ - // - var Observe = can.Map = can.Observe = can.Construct.extend( { - /** - * @static - */ - // keep so it can be overwritten - bind : can.bindAndSetup, - unbind: can.unbindAndTeardown, - id: "id", - canMakeObserve : canMakeObserve, - // starts collecting events - // takes a callback for after they are updated - // how could you hook into after ejs - /** - * @function can.Observe.startBatch startBatch - * @parent can.Observe.static - * @description Begin an event batch. - * - * @signature `can.Observe.startBatch([batchStopHandler])` - * - * @param {Function} [batchStopHandler] a callback that gets called after all batched events have been called - * - * @body - * `startBatch` causes can.Observe to begin an event batch. Until `[can.Observe.stopBatch]` is called, any - * events that would result from calls to `[can.Observe::attr attr]` are held back from firing. If you have - * lots of changes to make to can.Observes, batching them together can help performance &emdash; especially if - * those can.Observes are live-bound to the DOM. - * - * In this example, you can see how the _first_ and _change_ events are not fired (and their handlers - * are not called) until `stopBatch` is called. - * - * @codestart - * var person = new can.Observe({ - * first: 'Alexis', - * last: 'Abril' - * }); - * - * person.bind('first', function() { - * console.log("First name changed.""); - * }).bind('change', function() { - * console.log("Something changed."); - * }); - * - * can.Observe.startBatch(); - * person.attr('first', 'Alex'); - * console.log('Still in the batch.'); - * can.Observe.stopBatch(); - * - * // the log has: - * // Still in the batch. - * // First name changed. - * // Something changed. - * @codeend - * - * You can also pass a callback to `startBatch` which will be called after all the events have - * been fired: - * @codestart - * can.Observe.startBatch(function() { - * console.log('The batch is over.'); - * }); - * person.attr('first', 'Izzy'); - * console.log('Still in the batch.'); - * can.Observe.stopBatch(); - * - * // The console has: - * // Still in the batch. - * // First name changed. - * // Something changed. - * // The batch is over. - * @codeend - * - * ## Calling `startBatch` multiple times - * - * If you call `startBatch` more than once, `stopBatch` needs to be called - * the same number of times before any batched events will fire. For ways - * to circumvent this process, see [can.Observe.stopBatch]. - * - * Here is an example that demonstrates how events are affected by calling - * `startBatch` multiple times. - * - * @codestart - * var addPeople = function(observable) { - * can.Observe.startBatch(); - * observable.attr('a', 'Alice'); - * observable.attr('b', 'Bob'); - * observable.attr('e', 'Eve'); - * can.Observe.stopBatch(); - * }; - * - * // In a completely different place: - * var list = new can.Observe(); - * list.bind('change', function() { - * console.log('The list changed.'); - * }); - * - * can.Observe.startBatch(); - * addPeople(list); - * console.log('Still in the batch.'); - * - * // Here, the console has: - * // Still in the batch. - * - * can.Observe.stopBatch(); - * - * // Here, the console has: - * // Still in the batch. - * // The list changed. - * // The list changed. - * // The list changed. - * @codeend - */ - startBatch: function( batchStopHandler ) { - transactions++; - batchStopHandler && stopCallbacks.push(batchStopHandler); - }, - /** - * @function can.Observe.stopBatch stopBatch - * @parent can.Observe.static - * @description End an event batch. - * @signature `can.Observe.stopBatch([force[, callStart]])` - * @param {bool} [force=false] whether to stop batching events immediately - * @param {bool} [callStart=false] whether to call `[can.Observe.startBatch startBatch]` after firing batched events - * - * @body - * `stopBatch` matches an earlier `[can.Observe.startBatch]` call. If `stopBatch` has been - * called as many times as `startBatch` (or if _force_ is true), all batched events will be - * fired and any callbacks passed to `startBatch` since the beginning of the batch will be - * called. If _force and _callStart_ are both true, a new batch will be started when all - * the events and callbacks have been fired. - * - * See `[can.Observe.startBatch]` for examples of `startBatch` and `stopBatch` in normal use. - * - * In this example, the batch is forceably ended in the `addPeople` function. - * @codestart - * var addPeople = function(observable) { - * can.Observe.startBatch(); - * observable.attr('a', 'Alice'); - * observable.attr('b', 'Bob'); - * observable.attr('e', 'Eve'); - * can.Observe.stopBatch(true); - * }; - * - * // In a completely different place: - * var list = new can.Observe(); - * list.bind('change', function() { - * console.log('The list changed.'); - * }); - * - * can.Observe.startBatch(); - * addPeople(list); - * console.log('Still in the batch.'); - * - * // Here, the console has: - * // Still in the batch. - * - * can.Observe.stopBatch(); - * - * // Here, the console has: - * // The list changed. - * // The list changed. - * // The list changed. - * // Still in the batch. - * @codeend - */ - stopBatch: function(force, callStart){ - if(force){ - transactions = 0; - } else { - transactions--; - } - - if(transactions == 0){ - var items = batchEvents.slice(0), - callbacks = stopCallbacks.slice(0); - batchEvents= []; - stopCallbacks = []; - batchNum++; - callStart && this.startBatch(); - can.each(items, function( args ) { - can.trigger.apply(can, args); - }); - can.each(callbacks, function( cb ) { - cb(); - }); - } - }, - /** - * @function can.Observe.triggerBatch triggerBatch - * @parent can.Observe.static - * @description Trigger an event to be added to the current batch. - * @signature `can.Observe.triggerBatch(item, event [, args])` - * @param {can.Observe} item the target of the event - * @param {String|{type: String}} event the type of event, or an event object with a type given - * @param {Array} [args] the parameters to trigger the event with. - * - * @body - * If events are currently being batched, calling `triggerBatch` adds an event - * to the batch. If events are not currently being batched, the event is triggered - * immediately. - */ - triggerBatch: function( item, event, args ) { - // Don't send events if initalizing. - if ( ! item._init) { - if (transactions == 0 ) { - return can.trigger(item, event, args); - } else { - event = typeof event === "string" ? - { type: event } : - event; - event.batchNum = batchNum; - batchEvents.push([ - item, - event, - args ] ); - } - } - }, - /** - * @function can.Observe.keys keys - * @parent can.Observe.static - * @description Iterate over the keys of an Observe. - * @signature `can.Observe.keys(observe)` - * @param {can.Observe} observe the `can.Observe` to get the keys from - * @return {Array} array An array containing the keys from _observe_. - * - * @body - * `keys` iterates over an observe to get an array of its keys. - * - * @codestart - * var people = new can.Observe({ - * a: 'Alice', - * b: 'Bob', - * e: 'Eve' - * }); - * - * can.Observe.keys(people); // ['a', 'b', 'e'] - * @codeend - */ - keys: function(observe) { - var keys = []; - Observe.__reading && Observe.__reading(observe, '__keys'); - for(var keyName in observe._data) { - keys.push(keyName); - } - return keys; - } - }, - /** - * @prototype - */ - { - setup: function( obj ) { - // `_data` is where we keep the properties. - this._data = {} - /** - * @property {String} can.Observe.prototype._cid - * @hide - * - * A globally unique ID for this `can.Observe` instance. - */ - // The namespace this `object` uses to listen to events. - can.cid(this, ".observe"); - // Sets all `attrs`. - this._init = 1; - this._setupComputes(); - var data = can.extend( can.extend(true,{},this.constructor.defaults || {}), obj ) - this.attr(data); - this.bind('change'+this._cid,can.proxy(this._changes,this)); - delete this._init; - }, - _setupComputes: function(){ - var prototype = this.constructor.prototype - for(var prop in prototype){ - if(prototype[prop] && prototype[prop].isComputed){ - this[prop] = prototype[prop].clone(this); - } - } - }, - _bindsetup: makeBindSetup(), - _bindteardown: function(){ - var cid = this._cid; - this._each(function(child){ - unhookup([child], cid) - }) - }, - _changes: function(ev, attr, how,newVal, oldVal){ - Observe.triggerBatch(this, {type:attr, batchNum: ev.batchNum}, [newVal,oldVal]); - }, - _triggerChange: function(attr, how,newVal, oldVal){ - Observe.triggerBatch(this,"change",can.makeArray(arguments)) - }, - // no live binding iterator - _each: function(callback){ - var data = this.__get(); - for(var prop in data){ - if(data.hasOwnProperty(prop)){ - callback(data[prop],prop) - } - } - }, - /** - * @function can.Observe.prototype.attr attr - * @description Get or set properties on an Observe. - * @signature `observe.attr()` - * - * Gets a collection of all the properties in this `can.Observe`. - * - * @return {Object} an object with all the properties in this `can.Observe`. - * - * @signature `observe.attr(key)` - * - * Reads a property from this `can.Observe`. - * - * @param {String} key the property to read - * @return {*} the value assigned to _key_. - * - * @signature `observe.attr(key, value)` - * - * Assigns _value_ to a property on this `can.Observe` called _key_. - * - * @param {String} key the property to set - * @param {*} the value to assign to _key_. - * @return {can.Observe} this Observe, for chaining - * - * @signature `observe.attr(obj[, removeOthers])` - * - * Assigns each value in _obj_ to a property on this `can.Observe` named after the - * corresponding key in _obj_, effectively merging _obj_ into the Observe. - * - * @param {Object} obj a collection of key-value pairs to set. - * If any properties already exist on the `can.Observe`, they will be overwritten. - * - * @param {bool} [removeOthers=false] whether to remove keys not present in _obj_. - * To remove keys without setting other keys, use `[can.Observe::removeAttr removeAttr]`. - * - * @return {can.Observe} this Observe, for chaining - * - * @body - * `attr` gets or sets properties on the `can.Observe` it's called on. Here's a tour through - * how all of its forms work: - * - * @codestart - * var people = new can.Observe({}); - * - * // set a property: - * people.attr('a', 'Alex'); - * - * // get a property: - * people.attr('a'); // 'Alex' - * - * // set and merge multiple properties: - * people.attr({ - * a: 'Alice', - * b: 'Bob' - * }); - * - * // get all properties: - * people.attr(); // {a: 'Alice', b: 'Bob'} - * - * // set properties while removing others: - * people.attr({ - * b: 'Bill', - * e: 'Eve' - * }, true); - * - * people.attr(); // {b: 'Bill', e: 'Eve'} - * @codeend - * - * ## Deep properties - * - * `attr` can also set and read deep properties. All you have to do is specify - * the property name as you normally would if you weren't using `attr`. - * - * @codestart - * var people = new can.Observe({names: {}}); - * - * // set a property: - * people.attr('names.a', 'Alice'); - * - * // get a property: - * people.attr('names.a'); // 'Alice' - * people.names.attr('a'); // 'Alice' - * - * // get all properties: - * people.attr(); // {names: {a: 'Alice'}} - * @codeend - * - * Objects that are added to Observes become Observes themselves behind the scenes, - * so changes to deep properties fire events at each level, and you can bind at any - * level. As this example shows, all the same events are fired no matter what level - * you call `attr` at: - * - * @codestart - * var people = new can.Observe({names: {}}); - * - * people.bind('change', function(ev, attr, how, newVal, oldVal) { - * console.log('people change: ' + attr + ', ' + how + ', ' + newVal + ', ' + oldVal); - * }); - * - * people.names.bind('change', function(ev, attr, how, newVal, oldVal) { - * console.log('people.names change' + attr + ', ' + how + ', ' + newVal + ', ' + oldVal); - * }); - * - * people.bind('names', function(ev, newVal, oldVal) { - * console.log('people names: ' + newVal + ', ' + oldVal); - * }); - * - * people.names.bind('a', function(ev, newVal, oldVal) { - * console.log('people.names a: ' + newVal + ', ' + oldVal); - * }); - * - * people.bind('names.a', function(ev, newVal, oldVal) { - * console.log('people names.a: ' + newVal + ', ' + oldVal); - * }); - * - * people.attr('names.a', 'Alice'); // people change: names.a, add, Alice, undefined - * // people.names change: a, add, Alice, undefined - * // people.names a: Alice, undefined - * // people names.a: Alice, undefined - * - * people.names.attr('b', 'Bob'); // people change: names.b, add, Bob, undefined - * // people.names change: b, add, Bob, undefined - * // people.names b: Bob, undefined - * // people names.b: Bob, undefined - * @codeend - * - * ## See also - * - * For information on the events that are fired on property changes and how - * to listen for those events, see [can.Observe.prototype.bind bind]. - */ - attr: function( attr, val ) { - // This is super obfuscated for space -- basically, we're checking - // if the type of the attribute is not a `number` or a `string`. - var type = typeof attr; - if ( type !== "string" && type !== "number" ) { - return this._attrs(attr, val) - } else if ( arguments.length === 1 ) {// If we are getting a value. - // Let people know we are reading. - Observe.__reading && Observe.__reading(this, attr) - return this._get(attr) - } else { - // Otherwise we are setting. - this._set(attr, val); - return this; - } - }, - /** - * @function can.Observe.prototype.each each - * @description Call a function on each property of an Observe. - * @signature `observe.each( callback(item, propName ) )` - * - * `each` iterates through the Observe, calling a function - * for each property value and key. - * - * @param {function(*,String)} callback(item,propName) the function to call for each property - * The value and key of each property will be passed as the first and second - * arguments, respectively, to the callback. If the callback returns false, - * the loop will stop. - * - * @return {can.Observe} this Observe, for chaining - * - * @body - * @codestart - * var names = []; - * new can.Observe({a: 'Alice', b: 'Bob', e: 'Eve'}).each(function(value, key) { - * names.push(value); - * }); - * - * names; // ['Alice', 'Bob', 'Eve'] - * - * names = []; - * new can.Observe({a: 'Alice', b: 'Bob', e: 'Eve'}).each(function(value, key) { - * names.push(value); - * if(key === 'b') { - * return false; - * } - * }); - * - * names; // ['Alice', 'Bob'] - * - * @codeend - */ - each: function() { - Observe.__reading && Observe.__reading(this, '__keys'); - return can.each.apply(undefined, [this.__get()].concat(can.makeArray(arguments))) - }, - /** - * @function can.Observe.prototype.removeAttr removeAttr - * @description Remove a property from an Observe. - * @signature `observe.removeAttr(attrName)` - * @param {String} attrName the name of the property to remove - * @return {*} the value of the property that was removed - * - * @body - * `removeAttr` removes a property by name from an Observe. - * - * @codestart - * var people = new can.Observe({a: 'Alice', b: 'Bob', e: 'Eve'}); - * - * people.removeAttr('b'); // 'Bob' - * people.attr(); // {a: 'Alice', e: 'Eve'} - * @codeend - * - * Removing an attribute will cause a _change_ event to fire with `'remove'` - * passed as the _how_ parameter and `undefined` passed as the _newVal_ to - * handlers. It will also cause a _property name_ event to fire with `undefined` - * passed as _newVal_. An in-depth description at these events can be found - * under `[can.Observe.prototype.attr attr]`. - */ - removeAttr: function( attr ) { - // Info if this is List or not - var isList = this instanceof can.Observe.List, - // Convert the `attr` into parts (if nested). - parts = attrParts(attr), - // The actual property to remove. - prop = parts.shift(), - // The current value. - current = isList ? this[prop] : this._data[prop]; - - // If we have more parts, call `removeAttr` on that part. - if ( parts.length ) { - return current.removeAttr(parts) - } else { - if(isList) { - this.splice(prop, 1) - } else if( prop in this._data ){ - // Otherwise, `delete`. - delete this._data[prop]; - // Create the event. - if (!(prop in this.constructor.prototype)) { - delete this[prop] - } - // Let others know the number of keys have changed - Observe.triggerBatch(this, "__keys"); - this._triggerChange(prop, "remove", undefined, current); - - } - return current; - } - }, - // Reads a property from the `object`. - _get: function( attr ) { - var value = typeof attr === 'string' && !!~attr.indexOf('.') && this.__get(attr); - if(value) { - return value; - } - - // break up the attr (`"foo.bar"`) into `["foo","bar"]` - var parts = attrParts(attr), - // get the value of the first attr name (`"foo"`) - current = this.__get(parts.shift()); - // if there are other attributes to read - return parts.length ? - // and current has a value - current ? - // lookup the remaining attrs on current - current._get(parts) : - // or if there's no current, return undefined - undefined - : - // if there are no more parts, return current - current; - }, - // Reads a property directly if an `attr` is provided, otherwise - // returns the "real" data object itself. - __get: function( attr ) { - if(attr){ - if(this[attr] && this[attr].isComputed){ - return this[attr]() - } else { - return this._data[attr] - } - } else { - return this._data; - } - }, - // Sets `attr` prop as value on this object where. - // `attr` - Is a string of properties or an array of property values. - // `value` - The raw value to set. - _set: function( attr, value, keepKey) { - // Convert `attr` to attr parts (if it isn't already). - var parts = attrParts(attr, keepKey), - // The immediate prop we are setting. - prop = parts.shift(), - // The current value. - current = this.__get(prop); - - // If we have an `object` and remaining parts. - if ( canMakeObserve(current) && parts.length ) { - // That `object` should set it (this might need to call attr). - current._set(parts, value) - } else if (!parts.length ) { - // We're in "real" set territory. - if(this.__convert){ - value = this.__convert(prop, value) - } - this.__set(prop, value, current) - } else { - throw "can.Observe: Object does not exist" - } - }, - __set : function(prop, value, current){ - - // Otherwise, we are setting it on this `object`. - // TODO: Check if value is object and transform - // are we changing the value. - if ( value !== current ) { - // Check if we are adding this for the first time -- - // if we are, we need to create an `add` event. - var changeType = this.__get().hasOwnProperty(prop) ? "set" : "add"; - - // Set the value on data. - this.___set(prop, - - // If we are getting an object. - canMakeObserve(value) ? - - // Hook it up to send event. - hookupBubble(value, prop, this) : - // Value is normal. - value); - - if(changeType == "add"){ - // If there is no current value, let others know that - // the the number of keys have changed - - Observe.triggerBatch(this, "__keys", undefined); - - } - // `batchTrigger` the change event. - this._triggerChange(prop, changeType, value, current); - - //Observe.triggerBatch(this, prop, [value, current]); - // If we can stop listening to our old value, do it. - current && unhookup([current], this._cid); - } - - }, - // Directly sets a property on this `object`. - ___set: function( prop, val ) { - - if(this[prop] && this[prop].isComputed){ - this[prop](val) - } - - this._data[prop] = val; - // Add property directly for easy writing. - // Check if its on the `prototype` so we don't overwrite methods like `attrs`. - if (!(can.isFunction(this.constructor.prototype[prop]))) { - this[prop] = val - } - }, - - /** - * @function can.Observe.prototype.bind bind - * @description Bind event handlers to an Observe. - * - * @signature `observe.bind(eventType, handler)` - * - * @param {String} eventType the type of event to bind this handler to - * @param {Function} handler the handler to be called when this type of event fires - * The signature of the handler depends on the type of event being bound. See below - * for details. - * @return {can.Observe} this Observe, for chaining - * - * @body - * `bind` binds event handlers to property changes on `can.Observe`s. When you change - * a property using `attr`, two events are fired on the Observe, allowing other parts - * of your application to observe the changes to the object. - * - * ## The _change_ event - * - * The first event that is fired is the _change_ event. The _change_ event is useful - * if you want to react to all changes on an Observe. - * - * @codestart - * var o = new can.Observe({}); - * o.bind('change', function(ev, attr, how, newVal, oldVal) { - * console.log('Something changed.'); - * }); - * @codeend - * - * The parameters of the event handler for the _change_ event are: - * - * - _ev_ The event object. - * - _attr_ Which property changed. - * - _how_ Whether the property was added, removed, or set. Possible values are `'add'`, `'remove'`, or `'set'`. - * - _newVal_ The value of the property after the change. `newVal` will be `undefined` if the property was removed. - * - _oldVal_ Thishe value of the property before the change. `oldVal` will be `undefined` if the property was added. - * - * Here is a concrete tour through the _change_ event handler's arguments: - * - * @codestart - * var o = new can.Observe({}); - * o.bind('change', function(ev, attr, how, newVal, oldVal) { - * console.log(ev + ', ' + attr + ', ' + how + ', ' + newVal + ', ' + oldVal); - * }); - * - * o.attr('a', 'Alexis'); // [object Object], a, add, Alexis, undefined - * o.attr('a', 'Adam'); // [object Object], a, set, Adam, Alexis - * o.attr({ - * 'a': 'Alice', // [object Object], a, set, Alice, Adam - * 'b': 'Bob' // [object Object], b, add, Bob, undefined - * }); - * o.removeAttr('a'); // [object Object], a, remove, undefined, Alice - * @codeend - * - * (See also `[can.Observe::removeAttr removeAttr]`, which removes properties). - * - * ## The _property name_ event - * - * The second event that is fired is an event whose type is the same as the changed - * property's name. This event is useful for noticing changes to a specific property. - * - * @codestart - * var o = new can.Observe({}); - * o.bind('a', function(ev, newVal, oldVal) { - * console.log('The value of a changed.'); - * }); - * @codeend - * - * The parameters of the event handler for the _property name_ event are: - * - * - _ev_ The event object. - * - _newVal_ The value of the property after the change. `newVal` will be `undefined` if the property was removed. - * - _oldVal_ The value of the property before the change. `oldVal` will be `undefined` if the property was added. - * - * Here is a concrete tour through the _property name_ event handler's arguments: - * - * @codestart - * var o = new can.Observe({}); - * o.bind('a', function(ev, newVal, oldVal) { - * console.log(ev + ', ' + newVal + ', ' + oldVal); - * }); - * - * o.attr('a', 'Alexis'); // [object Object], Alexis, undefined - * o.attr('a', 'Adam'); // [object Object], Adam, Alexis - * o.attr({ - * 'a': 'Alice', // [object Object], Alice, Adam - * 'b': 'Bob' - * }); - * o.removeAttr('a'); // [object Object], undefined, Alice - * @codeend - * - * ## See also - * - * More information about changing properties on Observes can be found under - * [can.Observe.prototype.attr attr]. - * - * For a more specific way to changes on Observes, see the [can.Observe.delegate] plugin. - */ - bind: can.bindAndSetup, - /** - * @function can.Observe.prototype.unbind unbind - * @description Unbind event handlers from an Observe. - * @signature `observe.unbind(eventType[, handler])` - * @param {String} eventType the type of event to unbind, exactly as passed to `bind` - * @param {Function} [handler] the handler to unbind - * - * @body - * `unbind` unbinds event handlers previously bound with [can.Observe.prototype.bind|`bind`]. - * If no _handler_ is passed, all handlers for the given event type will be unbound. - * - * @codestart - * var i = 0, - * increaseBy2 = function() { i += 2; }, - * increaseBy3 = function() { i += 3; }, - * o = new can.Observe(); - * - * o.bind('change', increaseBy2); - * o.bind('change', increaseBy3); - * o.attr('a', 'Alice'); - * i; // 5 - * - * o.unbind('change', increaseBy2); - * o.attr('b', 'Bob'); - * i; // 8 - * - * o.unbind('change'); - * o.attr('e', 'Eve'); - * i; // 8 - * @codeend - */ - unbind: can.unbindAndTeardown, - /** - * @function can.Observe.prototype.serialize serialize - * @description Serialize this object to something that - * can be passed to `JSON.stringify`. - * @signature `observe.serialize()` - * - * - * Get the serialized Object form of the observe. Serialized - * data is typically used to send back to a server. - * - * o.serialize() //-> { name: 'Justin' } - * - * Serialize currently returns the same data - * as [can.Observe.prototype.attrs]. However, in future - * versions, serialize will be able to return serialized - * data similar to [can.Model]. The following will work: - * - * new Observe({time: new Date()}) - * .serialize() //-> { time: 1319666613663 } - * - * @return {Object} a JavaScript Object that can be - * serialized with `JSON.stringify` or other methods. - * - */ - serialize: function() { - return serialize(this, 'serialize', {}); - }, - /** - * @hide - * Set multiple properties on the observable - * @param {Object} props - * @param {Boolean} remove true if you should remove properties that are not in props - */ - _attrs: function( props, remove ) { - - if ( props === undefined ) { - return serialize(this, 'attr', {}) - } - - props = can.extend({}, props); - var prop, - self = this, - newVal; - Observe.startBatch(); - this.each(function(curVal, prop){ - newVal = props[prop]; - - // If we are merging... - if ( newVal === undefined ) { - remove && self.removeAttr(prop); - return; - } - - if(self.__convert){ - newVal = self.__convert(prop, newVal) - } - - // if we're dealing with models, want to call _set to let converter run - if( newVal instanceof can.Observe ) { - self.__set(prop, newVal, curVal) - // if its an object, let attr merge - } else if ( canMakeObserve(curVal) && canMakeObserve(newVal) && curVal.attr ) { - curVal.attr(newVal, remove) - // otherwise just set - } else if ( curVal != newVal ) { - self.__set(prop, newVal, curVal) - } - - delete props[prop]; - }) - // Add remaining props. - for ( var prop in props ) { - newVal = props[prop]; - this._set(prop, newVal, true) - } - Observe.stopBatch() - return this; - }, - - /** - * @function can.Observe.prototype.compute compute - * @description Make a can.compute from an observable property. - * @signature `observe.compute(attrName)` - * @param {String} attrName the property to bind to - * @return {can.compute} a [can.compute] bound to _attrName_ - * - * @body - * `compute` is a convenience method for making computes from properties - * of Observes. More information about computes can be found under [can.compute]. - * - * @codestart - * var observe = new can.Observe({a: 'Alexis'}); - * var name = observe.compute('a'); - * name.bind('change', function(ev, nevVal, oldVal) { - * console.log('a changed from ' + oldVal + 'to' + newName + '.'); - * }); - * - * name(); // 'Alexis' - * - * observe.attr('a', 'Adam'); // 'a changed from Alexis to Adam.' - * name(); // 'Adam' - * - * name('Alice'); // 'a changed from Adam to Alice.' - * name(); // 'Alice' - */ - compute: function(prop) { - if(can.isFunction( this.constructor.prototype[prop] )){ - return can.compute(this[prop], this); - } else { - return can.compute(this,prop); - } - - } - }); - // Helpers for `observable` lists. - var splice = [].splice, - /** - * @constructor can.Observe.List - * @inherits can.Observe - * @download can/observe - * @test can/observe/qunit.html - * @parent canjs - * - * Use for observable array-like objects. - * - * @signature `new can.Observe.List([array])` - * - * Create an observable array-like object. - * - * @param {Array} [array] items to seed the List with - * @return {can.Observe.List} an instance of `can.Observe.List` with the elements from _array_ - * - * @signature `can.Observe.List([name,] [staticProperties,] instanceProperties)` - * - * Creates a new extended constructor function. - * - * This is deprecated. In CanJS 1.2, by default, calling the constructor function - * without `new` will create a `new` instance. Use [can.Construct.extend can.Observe.extend] - * instead of calling the constructor to extend. - * - * @body - * - * ## Working with Lists - * - * `can.Observe.List` extends `[can.Observe]`, so all the ways that you're used to working with - * Observes also work here, including [can.Observe.prototype.bind bind], [can.Observe.prototype.unbind unbind], - * and [can.Observe.prototype.each each]. And just as you can directly read properties normally - * off of an Observe, you can use array accessors ([]) to read elements directly off of a List. - * - * The one function of `can.Observe` that works slightly differently is `attr`. As expected when working with - * arrays, top-level keys passed into `attr` are required to be numeric. (Strings may still be used when getting - * or modifying deep properties). Any top-level keys that are non-numeric are ignored. In addition, as might be - * expected, a call to argument-less `attr` returns an array instead of an object. - * - * Just as you shouldn't set properties of an Observe directly, you shouldn't change elements - * of a List directly. Always use `attr` to set the elements of a List, or use [can.Observe.List.push push], - * [can.Observe.List.pop pop], [can.Observe.List.shift shift], [can.Observe.List.unshift unshift], or [can.Observe.List.splice splice]. - * - * Here is a tour through the forms of `can.Observe.List`'s `attr` that parallels the one found under [can.Observe.prototype.attr attr]: - * - * @codestart - * var people = new can.Observe.List(['Alex', 'Bill']); - * - * // set an element: - * people.attr(0, 'Adam'); - * people[0] = 'Adam'; // don't do this! - * - * // get an element: - * people.attr(0); // 'Adam' - * people[0]; // 'Adam' - * - * // get all elements: - * people.attr(); // ['Adam', 'Bill'] - * - * // extend the array: - * people.attr(4, 'Charlie'); - * people.attr(); // ['Adam', 'Bill', undefined, undefined, 'Charlie'] - * - * // merge the elements: - * people.attr(['Alice', 'Bob', 'Eve']); - * people.attr(); // ['Alice', 'Bob', 'Eve', undefined, 'Charlie'] - * @codeend - * - * ## Listening to changes - * - * As with `can.Observe`s, the real power of observable arrays comes from being able to - * react to changes in the member elements of the array. Lists emit five types of events: - * - the _change_ event fires on every change to a List. - * - the _set_ event is fired when an element is set. - * - the _add_ event is fired when an element is added to the List. - * - the _remove_ event is fired when an element is removed from the List. - * - the _length_ event is fired when the length of the List changes. - * - * This example presents a brief concrete survey of the times these events are fired: - * - * @codestart - * var list = new can.Observe.List(['Alice', 'Bob', 'Eve']); - * - * list.bind('change', function() { console.log('An element changed.'); }); - * list.bind('set', function() { console.log('An element was set.'); }); - * list.bind('add', function() { console.log('An element was added.'); }); - * list.bind('remove', function() { console.log('An element was removed.'); }); - * list.bind('length', function() { console.log('The length of the list changed.'); }); - * - * list.attr(0, 'Alexis'); // 'An element changed.' - * // 'An element was set.' - * - * list.attr(3, 'Xerxes'); // 'An element changed.' - * // 'An element was added.' - * // 'The length of the list was changed.' - * - * list.attr(['Adam', 'Bill']); // 'An element changed.' - * // 'An element was set.' - * // 'An element was changed.' - * // 'An element was set.' - * - * list.pop(); // 'An element changed.' - * // 'An element was removed.' - * // 'The length of the list was changed.' - * @codeend - * - * More information about binding to these events can be found under [can.Observe.List.attr attr]. - */ - list = Observe( - /** - * @prototype - */ - { - setup: function( instances, options ) { - this.length = 0; - can.cid(this, ".observe") - this._init = 1; - if( can.isDeferred(instances) ) { - this.replace(instances) - } else { - this.push.apply(this, can.makeArray(instances || [])); - } - // this change needs to be ignored - this.bind('change'+this._cid,can.proxy(this._changes,this)); - can.extend(this, options); - delete this._init; - }, - _triggerChange: function(attr, how, newVal, oldVal){ - - Observe.prototype._triggerChange.apply(this,arguments) - // `batchTrigger` direct add and remove events... - if ( !~ attr.indexOf('.')){ - - if( how === 'add' ) { - Observe.triggerBatch(this, how, [newVal,+attr]); - Observe.triggerBatch(this,'length',[this.length]); - } else if( how === 'remove' ) { - Observe.triggerBatch(this, how, [oldVal, +attr]); - Observe.triggerBatch(this,'length',[this.length]); - } else { - Observe.triggerBatch(this,how,[newVal, +attr]) - } - - } - - }, - __get : function(attr){ - return attr ? this[attr] : this; - }, - ___set : function(attr, val){ - this[attr] = val; - if(+attr >= this.length){ - this.length = (+attr+1) - } - }, - _each: function(callback){ - var data = this.__get(); - for(var i =0; i < data.length; i++){ - callback(data[i],i) - } - }, - _bindsetup: makeBindSetup("*"), - // Returns the serialized form of this list. - /** - * @hide - * Returns the serialized form of this list. - */ - serialize: function() { - return serialize(this, 'serialize', []); - }, - /** - * @function can.Observe.List.prototype.each each - * @description Call a function on each element of a List. - * @signature `list.each( callback(item, index) )` - * - * `each` iterates through the Observe, calling a function - * for each element. - * - * @param {function(*, Number)} callback the function to call for each element - * The value and index of each element will be passed as the first and second - * arguments, respectively, to the callback. If the callback returns false, - * the loop will stop. - * - * @return {can.Observe.List} this List, for chaining - * - * @body - * @codestart - * var i = 0; - * new can.Observe([1, 10, 100]).each(function(element, index) { - * i += element; - * }); - * - * i; // 111 - * - * i = 0; - * new can.Observe([1, 10, 100]).each(function(element, index) { - * i += element; - * if(index >= 1) { - * return false; - * } - * }); - * - * i; // 11 - * @codeend - */ - // - /** - * @function can.Observe.List.prototype.splice splice - * @description Insert and remove elements from a List. - * @signature `list.splice(index[, howMany[, ...newElements]])` - * @param {Number} index where to start removing or inserting elements - * - * @param {Number} [howMany] the number of elements to remove - * If _howMany_ is not provided, `splice` will all elements from `index` to the end of the List. - * - * @param {*} newElements elements to insert into the List - * - * @return {Array} the elements removed by `splice` - * - * @body - * `splice` lets you remove elements from and insert elements into a List. - * - * This example demonstrates how to do surgery on a list of numbers: - * - * @codestart - * var list = new can.Observe.List([0, 1, 2, 3]); - * - * // starting at index 2, remove one element and insert 'Alice' and 'Bob': - * list.splice(2, 1, 'Alice', 'Bob'); - * list.attr(); // [0, 1, 'Alice', 'Bob', 3] - * @codeend - * - * ## Events - * - * `splice` causes the List it's called on to emit _change_ events, - * _add_ events, _remove_ events, and _length_ events. If there are - * any elements to remove, a _change_ event, a _remove_ event, and a - * _length_ event will be fired. If there are any elements to insert, a - * separate _change_ event, an _add_ event, and a separate _length_ event - * will be fired. - * - * This slightly-modified version of the above example should help - * make it clear how `splice` causes events to be emitted: - * - * @codestart - * var list = new can.Observe.List(['a', 'b', 'c', 'd']); - * list.bind('change', function(ev, attr, how, newVals, oldVals) { - * console.log('change: ' + attr + ', ' + how + ', ' + newVals + ', ' + oldVals); - * }); - * list.bind('add', function(ev, newVals, where) { - * console.log('add: ' + newVals + ', ' + where); - * }); - * list.bind('remove', function(ev, oldVals, where) { - * console.log('remove: ' + oldVals + ', ' + where); - * }); - * list.bind('length', function(ev, length) { - * console.log('length: ' + length + ', ' + this.attr()); - * }); - * - * // starting at index 2, remove one element and insert 'Alice' and 'Bob': - * list.splice(2, 1, 'Alice', 'Bob'); // change: 2, 'remove', undefined, ['c'] - * // remove: ['c'], 2 - * // length: 5, ['a', 'b', 'Alice', 'Bob', 'd'] - * // change: 2, 'add', ['Alice', 'Bob'], ['c'] - * // add: ['Alice', 'Bob'], 2 - * // length: 5, ['a', 'b', 'Alice', 'Bob', 'd'] - * @codeend - * - * More information about binding to these events can be found under [can.Observe.List.attr attr]. - */ - splice: function( index, howMany ) { - var args = can.makeArray(arguments), - i; - - for ( i = 2; i < args.length; i++ ) { - var val = args[i]; - if ( canMakeObserve(val) ) { - args[i] = hookupBubble(val, "*", this, this.constructor.Observe, this.constructor) - } - } - if ( howMany === undefined ) { - howMany = args[1] = this.length - index; - } - var removed = splice.apply(this, args); - can.Observe.startBatch(); - if ( howMany > 0 ) { - this._triggerChange(""+index, "remove", undefined, removed); - unhookup(removed, this._cid); - } - if ( args.length > 2 ) { - this._triggerChange(""+index, "add", args.slice(2), removed); - } - can.Observe.stopBatch(); - return removed; - }, - /** - * @description Get or set elements in a List. - * @function can.Observe.List.prototype.attr attr - * @signature `list.attr()` - * - * Gets a collection of all the elements in this `can.Observe.List`. - * - * @return {Array} array with all the elements in this List. - * - * @signature `list.attr(index)` - * - * Reads a element from this `can.Observe.List`. - * - * @param {Number} index the element to read - * @return {*} the value at _index_. - * - * @signature `list.attr(index, value)` - * - * Assigns _value_ to the index _index_ on this `can.Observe.List`, expanding the list if necessary. - * - * @param {Number} index the element to set - * @param {*} the value to assign at _index_ - * @return {can.Observe.List} this List, for chaining - * - * @signature `list.attr(elements[, replaceCompletely])` - * - * Merges the members of _elements_ into this List, replacing each from the beginning in order. If - * _elements_ is longer than the current List, the current List will be expanded. If _elements_ - * is shorter than the current List, the extra existing members are not affected (unless - * _replaceCompletely_ is `true`). To remove elements without replacing them, use `[can.Observe.List.prototype.removeAttr removeAttr]`. - * - * @param {Array} elements an array of elements to merge in - * - * @param {bool} [replaceCompletely=false] whether to completely replace the elements of List - * If _replaceCompletely_ is `true` and _elements_ is shorter than the List, the existing - * extra members of the List will be removed. - * - * @return {can.Observe.List} this List, for chaining - * - * @body - * `attr` gets or sets elements on the `can.Observe.List` it's called on. Here's a tour through - * how all of its forms work: - * - * @codestart - * var people = new can.Observe.List(['Alex', 'Bill']); - * - * // set an element: - * people.attr(0, 'Adam'); - * - * // get an element: - * people.attr(0); // 'Adam' - * people[0]; // 'Adam' - * - * // get all elements: - * people.attr(); // ['Adam', 'Bill'] - * - * // extend the array: - * people.attr(4, 'Charlie'); - * people.attr(); // ['Adam', 'Bill', undefined, undefined, 'Charlie'] - * - * // merge the elements: - * people.attr(['Alice', 'Bob', 'Eve']); - * people.attr(); // ['Alice', 'Bob', 'Eve', undefined, 'Charlie'] - * @codeend - * - * ## Deep properties - * - * `attr` can also set and read deep properties. All you have to do is specify - * the property name as you normally would if you weren't using `attr`. - * - * @codestart - * var people = new can.Observe.List([{name: 'Alex'}, {name: 'Bob'}]); - * - * // set a property: - * people.attr('0.name', 'Alice'); - * - * // get a property: - * people.attr('0.name'); // 'Alice' - * people[0].attr('name'); // 'Alice' - * - * // get all properties: - * people.attr(); // [{name: 'Alice'}, {name: 'Bob'}] - * @codeend - * - * The discussion of deep properties under `[can.Observe.prototype.attr]` may also - * be enlightening. - * - * ## Events - * - * `can.Observe.List`s emit five types of events in response to changes. They are: - * - the _change_ event fires on every change to a List. - * - the _set_ event is fired when an element is set. - * - the _add_ event is fired when an element is added to the List. - * - the _remove_ event is fired when an element is removed from the List. - * - the _length_ event is fired when the length of the List changes. - * - * * ## The _change_ event - * - * The first event that is fired is the _change_ event. The _change_ event is useful - * if you want to react to all changes on an List. - * - * @codestart - * var list = new can.Observe.List([]); - * list.bind('change', function(ev, index, how, newVal, oldVal) { - * console.log('Something changed.'); - * }); - * @codeend - * - * The parameters of the event handler for the _change_ event are: - * - * - _ev_ The event object. - * - _index_ Where the change took place. - * - _how_ Whether elements were added, removed, or set. - * Possible values are `'add'`, `'remove'`, or `'set'`. - * - _newVal_ The elements affected after the change - * _newVal_ will be a single value when an index is set, an Array when elements - * were added, and `undefined` if elements were removed. - * - _oldVal_ The elements affected before the change. - * _newVal_ will be a single value when an index is set, an Array when elements - * were removed, and `undefined` if elements were added. - * - * Here is a concrete tour through the _change_ event handler's arguments: - * - * @codestart - * var list = new can.Observe.List(); - * list.bind('change', function(ev, index, how, newVal, oldVal) { - * console.log(ev + ', ' + index + ', ' + how + ', ' + newVal + ', ' + oldVal); - * }); - * - * list.attr(['Alexis', 'Bill']); // [object Object], 0, add, ['Alexis', 'Bill'], undefined - * list.attr(2, 'Eve'); // [object Object], 2, add, Eve, undefined - * list.attr(0, 'Adam'); // [object Object], 0, set, Adam, Alexis - * list.attr(['Alice', 'Bob']); // [object Object], 0, set, Alice, Adam - * // [object Object], 1, set, Bob, Bill - * list.removeAttr(1); // [object Object], 1, remove, undefined, Bob - * @codeend - * - * ## The _set_ event - * - * _set_ events are fired when an element at an index that already exists in the List is - * modified. Actions can cause _set_ events to fire never also cause _length_ events - * to fire (although some functions, such as `[can.Observe.List.prototype.splice splice]` - * may cause unrelated sets of events to fire after being batched). - * - * The parameters of the event handler for the _set_ event are: - * - * - _ev_ The event object. - * - _newVal_ The new value of the element. - * - _index_ where the set took place. - * - * Here is a concrete tour through the _set_ event handler's arguments: - * - * @codestart - * var list = new can.Observe.List(); - * list.bind('set', function(ev, newVal, index) { - * console.log(newVal + ', ' + index); - * }); - * - * list.attr(['Alexis', 'Bill']); - * list.attr(2, 'Eve'); - * list.attr(0, 'Adam'); // Adam, 0 - * list.attr(['Alice', 'Bob']); // Alice, 0 - * // Bob, 1 - * list.removeAttr(1); - * @codeend - * - * ## The _add_ event - * - * _add_ events are fired when elements are added or inserted - * into the List. - * - * The parameters of the event handler for the _add_ event are: - * - * - _ev_ The event object. - * - _newElements_ The new elements. - * If more than one element is added, _newElements_ will be an array. - * Otherwise, it is simply the new element itself. - * - _index_ Where the add or insert took place. - * - * Here is a concrete tour through the _add_ event handler's arguments: - * - * @codestart - * var list = new can.Observe.List(); - * list.bind('add', function(ev, newElements, index) { - * console.log(newElements + ', ' + index); - * }); - * - * list.attr(['Alexis', 'Bill']); // ['Alexis', 'Bill'], 0 - * list.attr(2, 'Eve'); // Eve, 2 - * list.attr(0, 'Adam'); - * list.attr(['Alice', 'Bob']); - * - * list.removeAttr(1); - * @codeend - * - * ## The _remove_ event - * - * _remove_ events are fired when elements are removed from the list. - * - * The parameters of the event handler for the _remove_ event are: - * - * - _ev_ The event object. - * - _removedElements_ The removed elements. - * If more than one element was removed, _removedElements_ will be an array. - * Otherwise, it is simply the element itself. - * - _index_ Where the removal took place. - * - * Here is a concrete tour through the _remove_ event handler's arguments: - * - * @codestart - * var list = new can.Observe.List(); - * list.bind('remove', function(ev, removedElements, index) { - * console.log(removedElements + ', ' + index); - * }); - * - * list.attr(['Alexis', 'Bill']); - * list.attr(2, 'Eve'); - * list.attr(0, 'Adam'); - * list.attr(['Alice', 'Bob']); - * - * list.removeAttr(1); // Bob, 1 - * @codeend - * - * ## The _length_ event - * - * _length_ events are fired whenever the list changes. - * - * The parameters of the event handler for the _length_ event are: - * - * - _ev_ The event object. - *- _length_ The current length of the list. - * If events were batched when the _length_ event was triggered, _length_ - * will have the length of the list when `stopBatch` was called. Because - * of this, you may recieve multiple _length_ events with the same - * _length_ parameter. - * - * Here is a concrete tour through the _length_ event handler's arguments: - * - * @codestart - * var list = new can.Observe.List(); - * list.bind('length', function(ev, length) { - * console.log(length); - * }); - * - * list.attr(['Alexis', 'Bill']); // 2 - * list.attr(2, 'Eve'); // 3 - * list.attr(0, 'Adam'); - * list.attr(['Alice', 'Bob']); - * - * list.removeAttr(1); // 2 - * @codeend - */ - _attrs: function( items, remove ) { - if ( items === undefined ) { - return serialize(this, 'attr', []); - } - - // Create a copy. - items = can.makeArray( items ); - - Observe.startBatch(); - this._updateAttrs(items, remove); - Observe.stopBatch() - }, - - _updateAttrs : function( items, remove ){ - var len = Math.min(items.length, this.length); - - for ( var prop = 0; prop < len; prop++ ) { - var curVal = this[prop], - newVal = items[prop]; - - if ( canMakeObserve(curVal) && canMakeObserve(newVal) ) { - curVal.attr(newVal, remove) - } else if ( curVal != newVal ) { - this._set(prop, newVal) - } else { - - } - } - if ( items.length > this.length ) { - // Add in the remaining props. - this.push.apply( this, items.slice( this.length ) ); - } else if ( items.length < this.length && remove ) { - this.splice(items.length) - } - } - }), - - - // Converts to an `array` of arguments. - getArgs = function( args ) { - return args[0] && can.isArray(args[0]) ? - args[0] : - can.makeArray(args); - }; - // Create `push`, `pop`, `shift`, and `unshift` - can.each({ - /** - * @function can.Observe.List.prototype.push push - * @description Add elements to the end of a list. - * @signature `list.push(...elements)` - * - * `push` adds elements onto the end of a List.] - * - * @param {*} elements the elements to add to the List - * - * @return {Number} the new length of the List - * - * @body - * `push` is fairly straightforward: - * - * @codestart - * var list = new can.Observe.List(['Alice']); - * - * list.push('Bob', 'Eve'); - * list.attr(); // ['Alice', 'Bob', 'Eve'] - * @codeend - * - * If you have an array you want to concatenate to the end - * of the List, you can use `apply`: - * - * @codestart - * var names = ['Bob', 'Eve'], - * list = new can.Observe.List(['Alice']); - * - * list.push.apply(list, names); - * list.attr(); // ['Alice', 'Bob', 'Eve'] - * @codeend - * - * ## Events - * - * `push` causes _change_, _add_, and _length_ events to be fired. - * - * ## See also - * - * `push` has a counterpart in [can.Observe.List.pop pop], or you may be - * looking for [can.Observe.List.unshift unshift] and its counterpart [can.Observe.List.shift shift]. - */ - push: "length", - /** - * @function can.Observe.List.prototype.unshift unshift - * @description Add elements to the beginning of a List. - * @signature `list.unshift(...elements)` - * - * `unshift` adds elements onto the beginning of a List. - * - * @param {*} elements the elements to add to the List - * - * @return {Number} the new length of the List - * - * @body - * `unshift` adds elements to the front of the list in bulk in the order specified: - * - * @codestart - * var list = new can.Observe.List(['Alice']); - * - * list.unshift('Bob', 'Eve'); - * list.attr(); // ['Bob', 'Eve', 'Alice'] - * @codeend - * - * If you have an array you want to concatenate to the beginning - * of the List, you can use `apply`: - * - * @codestart - * var names = ['Bob', 'Eve'], - * list = new can.Observe.List(['Alice']); - * - * list.push.apply(list, names); - * list.attr(); // ['Bob', 'Eve', 'Alice'] - * @codeend - * - * ## Events - * - * `unshift` causes _change_, _add_, and _length_ events to be fired. - * - * ## See also - * - * `unshift` has a counterpart in [can.Observe.List.shift shift], or you may be - * looking for [can.Observe.List.push push] and its counterpart [can.Observe.List.pop pop]. - */ - unshift: 0 - }, - // Adds a method - // `name` - The method name. - // `where` - Where items in the `array` should be added. - function( where, name ) { - var orig = [][name] - list.prototype[name] = function() { - // Get the items being added. - var args = [], - // Where we are going to add items. - len = where ? this.length : 0, - i = arguments.length, - res, - val, - constructor = this.constructor; - - // Go through and convert anything to an `observe` that needs to be converted. - while(i--){ - val = arguments[i]; - args[i] = canMakeObserve(val) ? - hookupBubble(val, "*", this, this.constructor.Observe, this.constructor) : - val; - } - - // Call the original method. - res = orig.apply(this, args); - - if ( !this.comparator || args.length ) { - - this._triggerChange(""+len, "add", args, undefined); - } - - return res; - } - }); - - can.each({ - /** - * @function can.Observe.List.prototype.pop pop - * @description Remove an element from the end of a List. - * @signature `list.pop()` - * - * `push` removes an element from the end of a List. - * - * @return {*} the element just popped off the List, or `undefined` if the List was empty - * - * @body - * `pop` is the opposite action from `[can.Observe.List.push push]`: - * - * @codestart - * var list = new can.Observe.List(['Alice']); - * - * list.push('Bob', 'Eve'); - * list.attr(); // ['Alice', 'Bob', 'Eve'] - * - * list.pop(); // 'Eve' - * list.pop(); // 'Bob' - * list.pop(); // 'Alice' - * list.pop(); // undefined - * @codeend - * - * ## Events - * - * `pop` causes _change_, _remove_, and _length_ events to be fired if the List is not empty - * when it is called. - * - * ## See also - * - * `pop` has its counterpart in [can.Observe.List.push push], or you may be - * looking for [can.Observe.List.unshift unshift] and its counterpart [can.Observe.List.shift shift]. - */ - pop: "length", - /** - * @function can.Observe.List.prototype.shift shift - * @description Remove en element from the front of a list. - * @signature `list.shift()` - * - * `shift` removes an element from the beginning of a List. - * - * @return {*} the element just shifted off the List, or `undefined` if the List is empty - * - * @body - * `shift` is the opposite action from `[can.Observe.List.unshift unshift]`: - * - * @codestart - * var list = new can.Observe.List(['Alice']); - * - * list.unshift('Bob', 'Eve'); - * list.attr(); // ['Bob', 'Eve', 'Alice'] - * - * list.shift(); // 'Bob' - * list.shift(); // 'Eve' - * list.shift(); // 'Alice' - * list.shift(); // undefined - * @codeend - * - * ## Events - * - * `pop` causes _change_, _remove_, and _length_ events to be fired if the List is not empty - * when it is called. - * - * ## See also - * - * `shift` has a counterpart in [can.Observe.List.unshift unshift], or you may be - * looking for [can.Observe.List.push push] and its counterpart [can.Observe.List.pop pop]. - */ - shift: 0 - }, - // Creates a `remove` type method - function( where, name ) { - list.prototype[name] = function() { - - var args = getArgs(arguments), - len = where && this.length ? this.length - 1 : 0; - - - var res = [][name].apply(this, args) - - // Create a change where the args are - // `len` - Where these items were removed. - // `remove` - Items removed. - // `undefined` - The new values (there are none). - // `res` - The old, removed values (should these be unbound). - this._triggerChange(""+len, "remove", undefined, [res]) - - if ( res && res.unbind ) { - res.unbind("change" + this._cid) - } - return res; - } - }); - - can.extend(list.prototype, { - /** - * @function can.Observe.List.prototype.indexOf indexOf - * @description Look for an item in a List. - * @signature `list.indexOf(item)` - * - * `indexOf` finds the position of a given item in the List. - * - * @param {*} item the item to find - * - * @return {Number} the position of the item in the List, or -1 if the item is not found. - * - * @body - * @codestart - * var list = new can.Observe.List(['Alice', 'Bob', 'Eve']); - * list.indexOf('Alice'); // 0 - * list.indexOf('Charlie'); // -1 - * @codeend - * - * It is trivial to make a `contains`-type function using `indexOf`: - * - * @codestart - * function(list, item) { - * return list.indexOf(item) >= 0; - * } - * @codeend - */ - indexOf: function(item) { - this.attr('length') - return can.inArray(item, this) - }, - - /** - * @function can.Observe.List.prototype.join join - * @description Join a List's elements into a string. - * @signature `list.join(separator)` - * - * `join` turns a List into a string by inserting _separator_ between the string representations - * of all the elements of the List. - * - * @param {String} separator the string to seperate elements with - * - * @return {String} the joined string - * - * @body - * @codestart - * var list = new can.Observe.List(['Alice', 'Bob', 'Eve']); - * list.join(', '); // 'Alice, Bob, Eve' - * - * var beatles = new can.Observe.List(['John', 'Paul', 'Ringo', 'George']); - * beatles.join('&'); // 'John&Paul&Ringo&George' - * @codeend - */ - join : [].join, - - /** - * @function can.Observe.List.prototype.reverse reverse - * @description Reverse the order of a List. - * @signature `list.reverse()` - * - * `reverse` reverses the elements of the List in place. - * - * @return {can.Observe.List} the List, for chaining - * - * @body - * @codestart - * var list = new can.Observe.List(['Alice', 'Bob', 'Eve']); - * var reversedList = list.reverse(); - * - * reversedList.attr(); // ['Eve', 'Bob', 'Alice']; - * list === reversedList; // true - * @codeend - */ - reverse: [].reverse, - - /** - * @function can.Observe.List.prototype.slice slice - * @description Make a copy of a part of a List. - * @signature `list.slice([start[, end]])` - * - * `slice` creates a copy of a portion of the List. - * - * @param {Number} [start=0] the index to start copying from - * - * @param {Number} [end] the first index not to include in the copy - * If _end_ is not supplied, `slice` will copy until the end of the list. - * - * @return {can.Observe.List} a new `can.Observe.List` with the extracted elements - * - * @body - * @codestart - * var list = new can.Observe.List(['Alice', 'Bob', 'Charlie', 'Daniel', 'Eve']); - * var newList = list.slice(1, 4); - * newList.attr(); // ['Bob', 'Charlie', 'Daniel'] - * @codeend - * - * `slice` is the simplest way to copy a List: - * - * @codestart - * var list = new can.Observe.List(['Alice', 'Bob', 'Eve']); - * var copy = list.slice(); - * - * copy.attr(); // ['Alice', 'Bob', 'Eve'] - * list === copy; // false - * @codeend - */ - slice : function() { - var temp = Array.prototype.slice.apply(this, arguments); - return new this.constructor( temp ); - }, - - /** - * @function can.Observe.List.prototype.concat concat - * @description Merge many collections together into a List. - * @signature `list.concat(...args)` - * @param {Array|can.Observe.List|*} args Any number of arrays, Lists, or values to add in - * For each parameter given, if it is an Array or a List, each of its elements will be added to - * the end of the concatenated List. Otherwise, the parameter itself will be added. - * - * @body - * `concat` makes a new List with the elements of the List followed by the elements of the parameters. - * - * @codestart - * var list = new can.Observe.List(); - * var newList = list.concat( - * 'Alice', - * ['Bob', 'Charlie']), - * new can.Observe.List(['Daniel', 'Eve']), - * {f: 'Francis'} - * ); - * newList.attr(); // ['Alice', 'Bob', 'Charlie', 'Daniel', 'Eve', {f: 'Francis'}] - * @codeend - */ - concat : function() { - var args = []; - can.each( can.makeArray( arguments ), function( arg, i ) { - args[i] = arg instanceof can.Observe.List ? arg.serialize() : arg ; - }); - return new this.constructor(Array.prototype.concat.apply(this.serialize(), args)); - }, - - /** - * @function can.Observe.List.prototype.forEach forEach - * @description Call a function for each element of a List. - * @signature `list.forEach(callback[, thisArg])` - * @param {function(element, index, list)} callback a function to call with each element of the List - * The three parameters that _callback_ gets passed are _element_, the element at _index_, _index_ the - * current element of the list, and _list_ the List the elements are coming from. - * @param {Object} [thisArg] the object to use as `this` inside the callback - * - * @body - * `forEach` calls a callback for each element in the List. - * - * @codestart - * var list = new can.Observe.List([1, 2, 3]); - * list.forEach(function(element, index, list) { - * list.attr(index, element * element); - * }); - * list.attr(); // [1, 4, 9] - * @codeend - */ - forEach : function( cb, thisarg ) { - can.each(this, cb, thisarg || this ); - }, - - /** - * @function can.Observe.List.prototype.replace replace - * @description Replace all the elements of a List. - * @signature `list.replace(collection)` - * @param {Array|can.Observe.List|can.Deferred} collection the collection of new elements to use - * If a [can.Deferred] is passed, it must resolve to an `Array` or `can.Observe.List`. - * The elements of the list are not actually removed until the Deferred resolves. - * - * @body - * `replace` replaces all the elements of this List with new ones. - * - * `replace` is especially useful when `can.Observe.List`s are live-bound into `[can.Control]`s, - * and you intend to populate them with the results of a `[can.Model]` call: - * - * @codestart - * can.Control({ - * init: function() { - * this.list = new Todo.List(); - * // live-bind the list into the DOM - * this.element.html(can.view('list.mustache', this.list)); - * // when this AJAX call returns, the live-bound DOM will be updated - * this.list.replace(Todo.findAll()); - * } - * }); - * @codeend - * - * Learn more about [can.Model.List making Lists of models]. - * - * ## Events - * - * A major difference between `replace` and `attr(newElements, true)` is that `replace` always emits - * an_add_ event and a _remove_ event, whereas `attr` will cause _set_ events along an _add_ or _remove_ - * event if needed. Corresponding _change_ and _length_ events will be fired as well. - * - * The differences in the events fired by `attr` and `replace` are demonstrated concretely by this example: - * @codestart - * var attrList = new can.Observe.List(['Alexis', 'Bill']); - * attrList.bind('change', function(ev, index, how, newVals, oldVals) { - * console.log(index + ', ' + how + ', ' + newVals + ', ' + oldVals); - * }); - * - * var replaceList = new can.Observe.List(['Alexis', 'Bill']); - * replaceList.bind('change', function(ev, index, how, newVals, oldVals) { - * console.log(index + ', ' + how + ', ' + newVals + ', ' + oldVals); - * }); - * - * attrList.attr(['Adam', 'Ben'], true); // 0, set, Adam, Alexis - * // 1, set, Ben, Bill - * replaceList.replace(['Adam', 'Ben']); // 0, remove, undefined, ['Alexis', 'Bill'] - * // 0, add, undefined, ['Adam', 'Ben'] - * - * attrList.attr(['Amber'], true); // 0, set, Amber, Adam - * // 1, remove, undefined, Ben - * replaceList.replace(['Amber']); // 0, remove, undefined, ['Adam', 'Ben'] - * // 0, add, Amber, ['Adam', 'Ben'] - * - * attrList.attr(['Alice', 'Bob', 'Eve'], true); // 0, set, Alice, Amber - * // 1, add, ['Bob', 'Eve'], undefined - * replaceList.replace(['Alice', 'Bob', 'Eve']); // 0, remove, undefined, Amber - * // 0, add, ['Alice', 'Bob', 'Eve'], Amber - * @codeend - */ - replace : function(newList) { - if(can.isDeferred(newList)) { - newList.then(can.proxy(this.replace, this)); - } else { - this.splice.apply(this, [0, this.length].concat(can.makeArray(newList || []))); - } - - return this; - } - }); - - can.List = Observe.List = list; - Observe.setup = function(){ - - can.Construct.setup.apply( this, arguments ); - - - if(can.Map){ - if(!this.defaults){ - this.defaults = {}; - } - for(var prop in this.prototype){ - if(typeof this.prototype[prop] !== "function"){ - this.defaults[prop] = this.prototype[prop]; - } - } - } - if(can.List){ - this.List = Observe.List({ Observe : this }, {}); - } - - } - return Observe; -}); +// Loads all observable core modules +steal("can/util","can/map","can/list","can/compute",function(can){ + return can; +}) diff --git a/observe/observe_test.js b/observe/observe_test.js index 1f6fa276ecf..5314023b5d6 100644 --- a/observe/observe_test.js +++ b/observe/observe_test.js @@ -1,9 +1,10 @@ (function(undefined) { -module('can/observe') -test("Basic Observe",9,function(){ +module('can/observe map+list') + +test("Basic Map",9,function(){ - var state = new can.Observe({ + var state = new can.Map({ category : 5, productType : 4, properties : { @@ -42,13 +43,13 @@ test("Basic Observe",9,function(){ }); test("list attr changes length", function(){ - var l = new can.Observe.List([0,1,2]) + var l = new can.List([0,1,2]) l.attr(3,3) equal(l.length, 4); }) test("list splice", function(){ - var l = new can.Observe.List([0,1,2,3]), + var l = new can.List([0,1,2,3]), first = true; l.bind('change', function( ev, attr, how, newVals, oldVals ) { @@ -73,7 +74,7 @@ test("list splice", function(){ test("list pop", function(){ - var l = new can.Observe.List([0,1,2,3]); + var l = new can.List([0,1,2,3]); l.bind('change', function( ev, attr, how, newVals, oldVals ) { equal(attr, "3") @@ -88,7 +89,7 @@ test("list pop", function(){ }) test("changing an object unbinds", function(){ - var state = new can.Observe({ + var state = new can.Map({ category : 5, productType : 4, properties : { @@ -116,7 +117,7 @@ test("changing an object unbinds", function(){ }); test("replacing with an object that object becomes observable",function(){ - var state = new can.Observe({ + var state = new can.Map({ properties : { brand: [], model : [], @@ -132,20 +133,20 @@ test("replacing with an object that object becomes observable",function(){ }); test("attr does not blow away old observable", function(){ - var state = new can.Observe({ + var state = new can.Map({ properties : { brand: ['gain'] } }); var oldCid = state.attr("properties.brand")._cid; state.attr({properties:{brand:[]}}, true); - deepEqual(state.attr("properties.brand")._cid, oldCid, "should be the same observe, so that views bound to the old one get updates") + deepEqual(state.attr("properties.brand")._cid, oldCid, "should be the same map, so that views bound to the old one get updates") equal(state.attr("properties.brand").length, 0, "list should be empty"); }); test("sub observes respect attr remove parameter", function() { var bindCalled = 0, - state = new can.Observe({ + state = new can.Map({ monkey : { tail: 'brain' } @@ -159,17 +160,17 @@ test("sub observes respect attr remove parameter", function() { }); state.attr({monkey: {}}); - equal("brain", state.attr("monkey.tail"), "should not remove attribute of sub observe when remove param is false"); - equal(0, bindCalled, "remove event not fired for sub observe when remove param is false"); + equal("brain", state.attr("monkey.tail"), "should not remove attribute of sub map when remove param is false"); + equal(0, bindCalled, "remove event not fired for sub map when remove param is false"); state.attr({monkey: {}}, true); - equal(undefined, state.attr("monkey.tail"), "should remove attribute of sub observe when remove param is false"); - equal(1, bindCalled, "remove event fired for sub observe when remove param is false"); + equal(undefined, state.attr("monkey.tail"), "should remove attribute of sub map when remove param is false"); + equal(1, bindCalled, "remove event fired for sub map when remove param is false"); }); test("remove attr", function(){ - var state = new can.Observe({ + var state = new can.Map({ properties : { brand: [], model : [], @@ -192,7 +193,7 @@ test("remove attr", function(){ }); test("remove nested attr", function(){ - var state = new can.Observe({ + var state = new can.Map({ properties : { nested: true } @@ -209,7 +210,7 @@ test("remove nested attr", function(){ }); test("remove item in nested array", function(){ - var state = new can.Observe({ + var state = new can.Map({ array : ["a", "b"] }); @@ -226,7 +227,7 @@ test("remove item in nested array", function(){ }); test("remove nested property in item of array", function(){ - var state = new can.Observe({ + var state = new can.Map({ array : [{ nested: true }] @@ -242,8 +243,8 @@ test("remove nested property in item of array", function(){ equal(undefined, state.attr("array.0.nested") ); }); -test("remove nested property in item of array observe", function(){ - var state = new can.Observe.List([{nested: true}]); +test("remove nested property in item of array map", function(){ + var state = new can.List([{nested: true}]); state.bind("change", function(ev, attr, how, newVal, old){ equal(attr, "0.nested"); @@ -256,7 +257,7 @@ test("remove nested property in item of array observe", function(){ }); test("attr with an object", function(){ - var state = new can.Observe({ + var state = new can.Map({ properties : { foo: "bar", brand: [] @@ -300,13 +301,13 @@ test("attr with an object", function(){ }); test("empty get", function(){ - var state = new can.Observe({}); + var state = new can.Map({}); equal(state.attr('foo.bar'), undefined) }); test("attr deep array ", function(){ - var state = new can.Observe({}); + var state = new can.Map({}); var arr = [{ foo: "bar" }], @@ -331,12 +332,12 @@ test('attr semi-serialize', function(){ arr: [1,2,3, {four: '5'}] }; - var res = new can.Observe(first).attr(); + var res = new can.Map(first).attr(); deepEqual(res,compare, "test") }) test("attr sends events after it is done", function(){ - var state = new can.Observe({foo: 1, bar: 2}) + var state = new can.Map({foo: 1, bar: 2}) state.bind('change', function(){ equal(state.attr('foo'), -1, "foo set"); equal(state.attr('bar'), -2, "bar set") @@ -345,13 +346,13 @@ test("attr sends events after it is done", function(){ }) test("direct property access", function(){ - var state = new can.Observe({foo: 1, attr: 2}); + var state = new can.Map({foo: 1, attr: 2}); equal(state.foo,1); equal(typeof state.attr, 'function') }) test("pop unbinds", function(){ - var l = new can.Observe.List([{foo: 'bar'}]); + var l = new can.List([{foo: 'bar'}]); var o = l.attr(0), count = 0; l.bind('change', function(ev, attr, how, newVal, oldVal){ @@ -376,7 +377,7 @@ test("pop unbinds", function(){ }) test("splice unbinds", function(){ - var l = new can.Observe.List([{foo: 'bar'}]); + var l = new can.List([{foo: 'bar'}]); var o = l.attr(0), count = 0; l.bind('change', function(ev, attr, how, newVal, oldVal){ @@ -402,7 +403,7 @@ test("splice unbinds", function(){ test("always gets right attr even after moving array items", function(){ - var l = new can.Observe.List([{foo: 'bar'}]); + var l = new can.List([{foo: 'bar'}]); // get the first item var o = l.attr(0); @@ -421,13 +422,13 @@ test("always gets right attr even after moving array items", function(){ test("recursive observers do not cause stack overflow", function() { expect(0); - var a = new can.Observe(); - var b = new can.Observe({a: a}); + var a = new can.Map(); + var b = new can.Map({a: a}); a.attr("b", b); }); test("bind to specific attribute changes when an existing attribute's value is changed", function() { - var paginate = new can.Observe( { offset: 100, limit: 100, count: 2000 } ); + var paginate = new can.Map( { offset: 100, limit: 100, count: 2000 } ); paginate.bind( 'offset', function( ev, newVal, oldVal ) { equal(newVal, 200); equal(oldVal, 100); @@ -435,7 +436,7 @@ test("bind to specific attribute changes when an existing attribute's value is c paginate.attr( 'offset', 200 ); }); test("bind to specific attribute changes when an attribute is removed", 2, function() { - var paginate = new can.Observe( { offset: 100, limit: 100, count: 2000 } ); + var paginate = new can.Map( { offset: 100, limit: 100, count: 2000 } ); paginate.bind( 'offset', function( ev, newVal, oldVal ) { equal(newVal, undefined); equal(oldVal, 100); @@ -444,16 +445,16 @@ test("bind to specific attribute changes when an attribute is removed", 2, funct }); test("Array accessor methods", 11, function() { - var l = new can.Observe.List([ 'a', 'b', 'c' ]), + var l = new can.List([ 'a', 'b', 'c' ]), sliced = l.slice(2), joined = l.join(' | '), - concatenated = l.concat([ 2, 1 ], new can.Observe.List([ 0 ])); + concatenated = l.concat([ 2, 1 ], new can.List([ 0 ])); - ok(sliced instanceof can.Observe.List, 'Slice is an Observable list'); + ok(sliced instanceof can.List, 'Slice is an Observable list'); equal(sliced.length, 1, 'Sliced off two elements'); equal(sliced[0], 'c', 'Single element as expected'); equal(joined, 'a | b | c', 'Joined list properly'); - ok(concatenated instanceof can.Observe.List, 'Concatenated is an Observable list'); + ok(concatenated instanceof can.List, 'Concatenated is an Observable list'); deepEqual(concatenated.serialize(), [ 'a', 'b', 'c', 2, 1, 0 ], 'List concatenated properly'); l.forEach(function(letter, index) { ok(true, 'Iteration'); @@ -466,8 +467,8 @@ test("Array accessor methods", 11, function() { }); }); -test("instantiating can.Observe.List of correct type", function() { - var Ob = can.Observe({ +test("instantiating can.List of correct type", function() { + var Ob = can.Map({ getName : function() { return this.attr('name'); } @@ -478,17 +479,17 @@ test("instantiating can.Observe.List of correct type", function() { }]); equal(list.length, 1, 'List length is correct'); - ok(list[0] instanceof can.Observe, 'Initialized list item converted to can.Observe'); + ok(list[0] instanceof can.Map, 'Initialized list item converted to can.Map'); ok(list[0] instanceof Ob, 'Initialized list item converted to Ob'); - equal(list[0].getName(), 'Tester', 'Converted to extended Observe instance, could call getName()'); + equal(list[0].getName(), 'Tester', 'Converted to extended Map instance, could call getName()'); list.push({ name : 'Another test' }); equal(list[1].getName(), 'Another test', 'Pushed item gets converted as well'); }); -test("can.Observe.List.prototype.splice converts objects (#253)", function() { - var Ob = can.Observe({ +test("can.List.prototype.splice converts objects (#253)", function() { + var Ob = can.Map({ getAge : function() { return this.attr('age') + 10; } @@ -514,7 +515,7 @@ test("can.Observe.List.prototype.splice converts objects (#253)", function() { test("removing an already missing attribute does not cause an event", function(){ expect(0); - var ob = new can.Observe(); + var ob = new can.Map(); ob.bind("change", function(){ ok(false) }) @@ -522,14 +523,14 @@ test("removing an already missing attribute does not cause an event", function() }); test("Only plain objects should be converted to Observes", function() { - var ob = new can.Observe(); + var ob = new can.Map(); ob.attr('date', new Date()); ok(ob.attr('date') instanceof Date, 'Date should not be converted'); var selected = can.$('body'); ob.attr('sel', selected); if(can.isArray(selected)) { - ok(ob.attr('sel') instanceof can.Observe.List, 'can.$() as array converted into Observe.List'); + ok(ob.attr('sel') instanceof can.List, 'can.$() as array converted into List'); } else { equal(ob.attr('sel'), selected, 'can.$() should not be converted'); } @@ -543,7 +544,7 @@ test("Only plain objects should be converted to Observes", function() { test("bind on deep properties",function(){ expect(2) - var ob = new can.Observe({name: {first: "Brian"}}); + var ob = new can.Map({name: {first: "Brian"}}); ob.bind("name.first",function(ev, newVal, oldVal){ equal(newVal,"Justin"); equal(oldVal,"Brian") @@ -555,7 +556,7 @@ test("bind on deep properties",function(){ test("startBatch and stopBatch and changed event", 5, function(){ - var ob = new can.Observe({name: {first: "Brian"}, age: 29}), + var ob = new can.Map({name: {first: "Brian"}, age: 29}), bothSet = false, changeCallCount = 0, changedCalled = false; @@ -574,7 +575,7 @@ test("startBatch and stopBatch and changed event", 5, function(){ changedCalled = true; })*/ stop(); - can.Observe.startBatch(function(){ + can.batch.start(function(){ ok(true, "batch callback called") }); @@ -582,7 +583,7 @@ test("startBatch and stopBatch and changed event", 5, function(){ setTimeout(function(){ ob.attr('age',30); bothSet = true; - can.Observe.stopBatch(); + can.batch.stop(); start(); },1) @@ -592,7 +593,7 @@ test("startBatch and stopBatch and changed event", 5, function(){ test("startBatch callback", 4, function(){ - var ob = new can.Observe({ + var ob = new can.Map({ game: { name: "Legend of Zelda" }, @@ -604,20 +605,20 @@ test("startBatch callback", 4, function(){ equal(callbackCalled, false, 'startBatch callback not called yet'); }); - can.Observe.startBatch(function(){ + can.batch.start(function(){ ok(true, "startBatch callback called"); callbackCalled = true; }); ob.attr('hearts', 16); equal(callbackCalled, false, 'startBatch callback not called yet'); - can.Observe.stopBatch(); + can.batch.stop(); equal(callbackCalled, true, 'startBatch callback called'); }); -test("nested observe attr", function() { - var person1 = new can.Observe( { name: {first: 'Josh' } } ), - person2 = new can.Observe( { name: {first: 'Justin', last: 'Meyer' } } ), +test("nested map attr", function() { + var person1 = new can.Map( { name: {first: 'Josh' } } ), + person2 = new can.Map( { name: {first: 'Justin', last: 'Meyer' } } ), count = 0; person1.bind("change", function(ev, attr, how, val, old){ @@ -637,18 +638,18 @@ test("nested observe attr", function() { test("Nested array conversion (#172)", 4, function() { var original = [ [1, 2], [3, 4], [5, 6] ], - list = new can.Observe.List(original); + list = new can.List(original); - equal(list.length, 3, 'Observe list length is correct'); + equal(list.length, 3, 'list length is correct'); deepEqual(list.serialize(), original, 'Lists are the same'); list.unshift([10, 11], [12, 13]); - ok(list[0] instanceof can.Observe.List, 'Unshifted array converted to observe list'); + ok(list[0] instanceof can.List, 'Unshifted array converted to map list'); deepEqual(list.serialize(), [[10, 11], [12, 13]].concat(original), 'Arrays unshifted properly'); }); -test("can.Observe.List.prototype.replace (#194)", 7, function() { - var list = new can.Observe.List(['a', 'b', 'c']), +test("can.List.prototype.replace (#194)", 7, function() { + var list = new can.List(['a', 'b', 'c']), replaceList = ['d', 'e', 'f', 'g'], dfd = new can.Deferred(); @@ -692,10 +693,10 @@ test("can.Observe.List.prototype.replace (#194)", 7, function() { }, 100); }); -test("replace with a deferred that resolves to an Observe.List", function(){ +test("replace with a deferred that resolves to an List", function(){ var def = new can.Deferred(); - def.resolve(new can.Observe.List([{name: "foo"},{name: "bar"}])); - var list = new can.Observe.List([{name: "1"},{name: "2"}]); + def.resolve(new can.List([{name: "foo"},{name: "bar"}])); + var list = new can.List([{name: "1"},{name: "2"}]); list.bind("change",function(){ equal(list.length, 2, "length is still 2"); equal(list[0].attr("name"),"foo", "set to foo"); @@ -706,7 +707,7 @@ test("replace with a deferred that resolves to an Observe.List", function(){ test(".attr method doesn't merge nested objects (#207)", function() { // From http://jsfiddle.net/andrewborovin/wsNZB/ - var test = new can.Observe({ + var test = new can.Map({ a: { a1: 1, a2: 2 @@ -729,17 +730,17 @@ test(".attr method doesn't merge nested objects (#207)", function() { deepEqual(test.attr(), {"a":{"a1":1,"a2":3},"b":{"b1":3,"b2":2}}, "Object merged as expected"); }); -test("IE8 error on list setup with Observe.List (#226)", function() { - var list = new can.Observe.List(['first', 'second', 'third']), - otherList = new can.Observe.List(list); +test("IE8 error on list setup with List (#226)", function() { + var list = new can.List(['first', 'second', 'third']), + otherList = new can.List(list); deepEqual(list.attr(), otherList.attr(), 'Lists are the same'); }); -test("initialize Observe.List with a deferred",function(){ +test("initialize List with a deferred",function(){ stop() var def = new can.Deferred(); - var list = new can.Observe.List(def); + var list = new can.List(def); list.bind("add",function(ev, items, index){ deepEqual(items,["a","b"]); equal(index, 0); @@ -759,20 +760,20 @@ test("triggering a event while in a batch (#291)", function(){ // so a "change","destroyed" event bubbles. // this test errors if things are broken stop(); - var observe = new can.Observe(); + var map = new can.Map(); - can.Observe.startBatch(); - can.trigger(observe, "change","random") + can.batch.start(); + can.trigger(map, "change","random") setTimeout(function(){ - can.Observe.stopBatch(); + can.batch.stop(); start() },10); }); test("dot separated keys (#257, #296)", function() { - var ob = new can.Observe({ + var ob = new can.Map({ 'test.value': 'testing', other: { test: 'value' @@ -795,8 +796,8 @@ test("dot separated keys (#257, #296)", function() { test("cycle binding",function(){ - var first = new can.Observe(), - second= new can.Observe(); + var first = new can.Map(), + second= new can.Map(); first.attr('second',second); @@ -815,7 +816,7 @@ test("cycle binding",function(){ test("Deferreds are not converted", function() { var dfd = can.Deferred(), - ob = new can.Observe({ + ob = new can.Map({ test: dfd }); @@ -824,7 +825,7 @@ test("Deferreds are not converted", function() { }); test("Setting property to undefined", function(){ - var ob = new can.Observe({ + var ob = new can.Map({ "foo": "bar" }); ob.attr("foo", undefined); @@ -833,7 +834,7 @@ test("Setting property to undefined", function(){ }); test("removing list items containing computes", function(){ - var list = new can.Observe.List([{ + var list = new can.List([{ comp: can.compute(function(){ return false; }) @@ -843,4 +844,530 @@ test("removing list items containing computes", function(){ equal(list.length, 0, "list is empty"); }); + +module('can/observe compute') + +test("Basic Compute",function(){ + + var o = new can.Map({first: "Justin", last: "Meyer"}); + var prop = can.compute(function(){ + return o.attr("first") + " " +o.attr("last") + }) + + equal(prop(), "Justin Meyer"); + var handler = function(ev, newVal, oldVal){ + equal(newVal, "Brian Meyer") + equal(oldVal, "Justin Meyer") + } + prop.bind("change", handler); + + o.attr("first","Brian"); + + prop.unbind("change", handler) + o.attr("first","Brian"); +}); + + +test("compute on prototype", function(){ + + var Person = can.Map({ + fullName: function(){ + return this.attr("first") + " " +this.attr("last") + } + }) + + var me = new Person({ + first : "Justin", + last : "Meyer" + }); + var fullName = can.compute( me.fullName, me ); + + equal(fullName(), "Justin Meyer"); + + var called = 0; + + fullName.bind("change", function( ev, newVal, oldVal ) { + called++; + equal(called, 1, "called only once"); + equal(newVal, "Justin Shah"); + equal(oldVal, "Justin Meyer") + }); + + me.attr('last',"Shah") + + // to make this work, we'd have to look for a computed function and bind to it's change ... + // maybe bind can just work this way? +}) + + +test("setter compute", function(){ + var project = new can.Map({ + progress: 0.5 + }); + + // a setter compute that converts 50 to .5 and vice versa + var computed = can.compute(function(val){ + if(val) { + project.attr('progress', val / 100) + } else { + return parseInt( project.attr('progress') * 100 ); + } + }); + + equal(computed(), 50, "the value is right"); + computed(25); + equal(project.attr('progress'), 0.25); + equal(computed(),25 ); + + computed.bind("change", function(ev, newVal, oldVal){ + equal(newVal, 75); + equal(oldVal, 25) + }) + + computed(75); + +}) + +test("compute a compute", function() { + var project = new can.Map({ + progress: 0.5 + }); + + var percent = can.compute(function(val){ + if(val) { + project.attr('progress', val / 100); + } else { + return parseInt( project.attr('progress') * 100, 10); + } + }); + percent.named = "PERCENT"; + + equal(percent(),50,'percent starts right'); + percent.bind('change',function() { + // noop + }); + + var fraction = can.compute(function(val) { + if(val) { + percent(parseInt(val.split('/')[0],10)); + } else { + return percent() + '/100'; + } + }); + fraction.named ="FRACTIOn" + + fraction.bind('change',function() { + // noop + }); + + equal(fraction(),'50/100','fraction starts right'); + + percent(25); + + equal(percent(),25); + equal(project.attr('progress'),0.25,'progress updated'); + equal(fraction(),'25/100','fraction updated'); + + fraction('15/100'); + + equal(fraction(),'15/100'); + equal(project.attr('progress'),0.15,'progress updated'); + equal(percent(),15,'% updated'); +}); + +test("compute with a simple compute", function() { + expect(4); + var a = can.compute(5); + var b = can.compute(function() { + return a() * 2; + }); + + equal(b(),10,'b starts correct'); + a(3); + equal(b(),6,'b updates'); + + b.bind('change',function() { + equal(b(),24,'b fires change'); + }); + a(12); + equal(b(),24,'b updates when bound'); +}); + + +test("empty compute", function(){ + var c = can.compute(); + c.bind("change", function(ev, newVal, oldVal){ + ok(oldVal === undefined, "was undefined") + ok(newVal === 0, "now zero") + }) + + c(0); + +}); + +test("only one update on a batchTransaction",function(){ + var person = new can.Map({first: "Justin", last: "Meyer"}); + var func = function(){ + return person.attr('first')+" "+person.attr('last')+Math.random() + }; + var callbacks = 0; + can.compute.binder(func, window, function(newVal, oldVal){ + callbacks++; + }); + + person.attr({ + first: "Brian", + last: "Moschel" + }); + + equal(callbacks,1,"only one callback") +}) + +test("only one update on a start and end transaction",function(){ + var person = new can.Map({first: "Justin", last: "Meyer"}), + age = can.compute(5); + var func = function(newVal,oldVal){ + return person.attr('first')+" "+person.attr('last')+age()+Math.random(); + }; + var callbacks = 0; + can.compute.binder(func, window, function(newVal, oldVal){ + callbacks++; + }); + + can.batch.start(); + + person.attr('first',"Brian"); + stop(); + setTimeout(function(){ + person.attr('last',"Moschel"); + age(12) + + can.batch.stop(); + + equal(callbacks,1,"only one callback") + + start(); + }) + + + +}) + +test("Compute emits change events when an embbedded observe has properties added or removed", 4, function() { + var obs = new can.Map(), + compute1 = can.compute(function(){ + var txt = obs.attr('foo'); + obs.each(function(val){ + txt += val.toString(); + }); + return txt; + }); + + compute1.bind('change', function(ev, newVal, oldVal) { + ok(true, 'change handler fired: ' + newVal); + }) + // we're binding on adding / removing and foo + obs.attr('foo', 1); + obs.attr('bar', 2); + obs.attr('foo', 3); + obs.removeAttr('bar'); + obs.removeAttr('bar'); +}); + +test("compute only updates once when a list's contents are replaced",function(){ + + var list = new can.List([{name: "Justin"}]), + computedCount = 0; + + var compute = can.compute(function(){ + computedCount++; + list.each(function(item){ + item.attr('name') + }) + }) + equal(0,computedCount, "computes are not called until their value is read") + compute.bind("change", function(ev, newVal, oldVal){ + + }) + + equal(1,computedCount, "binding computes to store the value"); + list.replace([{name: "hank"}]); + equal(2,computedCount, "only one compute") + +}); + +test("Generate computes from Observes with can.Map.prototype.compute (#203)", 6, function() { + var obs = new can.Map({ + test : 'testvalue' + }); + + var compute = obs.compute('test'); + ok(compute.isComputed, '`test` is computed'); + equal(compute(), 'testvalue', 'Value is as expected'); + obs.attr('test', 'observeValue'); + equal(compute(), 'observeValue', 'Value is as expected'); + compute.bind('change', function(ev, newVal) { + equal(newVal, 'computeValue', 'new value from compute'); + }); + obs.bind('change', function(ev, name, how, newVal) { + equal(newVal, 'computeValue', 'Got new value from compute'); + }); + compute('computeValue'); + equal(compute(), 'computeValue', 'Got updated value'); +}); + +test("compute of computes", function(){ + expect(2) + var suggestedSearch = can.compute(null), + searchQuery = can.compute(''), + searchText = can.compute(function() { + var suggested = suggestedSearch(); + if(suggested) { + return suggested + } else { + return searchQuery(); + } + }); + + equal('',searchText(),"inital set"); + + searchText.bind("change", function(ev, newVal){ + equal(newVal,"food", "food set"); + }) + + + searchQuery("food") +}) + + +test("compute doesn't rebind and leak with 0 bindings", function() { + var state = new can.Map({ + foo: "bar" + }); + var computedA = 0, computedB = 0; + var computeA = can.compute(function() { + computedA++; + return state.attr("foo") === "bar"; + }); + var computeB = can.compute(function() { + computedB++; + return state.attr("foo") === "bar" || 15; + }); + + function aChange(ev, newVal) { + if(newVal) { + computeB.bind("change.computeA", function() { + // noop + }); + } else { + computeB.unbind("change.computeA"); + } + } + + computeA.bind("change", aChange); + aChange(null, computeA()); + + equal(computedA, 1, "binding A computes the value"); + equal(computedB, 1, "A=true, so B is bound, computing the value"); + + state.attr("foo", "baz"); + equal(computedA, 2, "A recomputed and unbound B"); + equal(computedB, 1, "B was unbound, so not recomputed"); + + state.attr("foo", "bar"); + equal(computedA, 3, "A recomputed => true"); + equal(computedB, 2, "A=true so B is rebound and recomputed"); + + computeA.unbind("change", aChange); + computeB.unbind("change.computeA"); + state.attr("foo", "baz"); + equal(computedA, 3, "unbound, so didn't recompute A"); + equal(computedB, 2, "unbound, so didn't recompute B"); +}); + + +test("compute setter without external value", function(){ + + var age = can.compute(0,function(newVal, oldVal){ + var num = +newVal + if(! isNaN(num) && 0 <= num && num <= 120 ){ + return num; + } else { + return oldVal; + } + }) + equal(age(), 0, "initial value set"); + age.bind("change", function(ev, newVal, oldVal){ + equal(5, newVal) + age.unbind("change",arguments.callee) + }); + + age(5); + equal(age(), 5, "5 set") + + age("invalid"); + equal(age(), 5, "5 kept") + +}) + +test("compute value",function(){ + expect(9) + var input = { + value: 1 + } + + var value = can.compute("",{ + get: function(){ + return input.value; + }, + set: function(newVal){ + input.value = newVal; + //input.onchange && input.onchange(); + }, + on: function(update){ + input.onchange = update; + }, + off: function(){ + delete input.onchange; + } + }) + + equal(value(), 1, "original value"); + ok(!input.onchange, "nothing bound"); + value(2); + + equal(value(), 2, "updated value"); + + equal(input.value, 2, "updated input.value"); + + + + value.bind("change", function(ev, newVal, oldVal){ + equal(newVal, 3, "newVal"); + equal(oldVal, 2, "oldVal"); + value.unbind("change", arguments.callee); + }) + ok(input.onchange, "binding to onchange"); + + value(3); + ok(!input.onchange, "removed binding") + equal(value(), 3); +}); + +test("compute bound to observe",function(){ + var me = new can.Map({name: "Justin"}); + + var bind = me.bind, + unbind = me.unbind, + bindCount = 0; + me.bind = function(){ + bindCount ++; + bind.apply(this,arguments); + } + me.unbind = function(){ + bindCount --; + unbind.apply(this,arguments); + } + + var name = can.compute(me,"name") + + equal(bindCount, 0); + equal(name(), "Justin"); + + var handler = function(ev, newVal, oldVal){ + equal(newVal, "Justin Meyer"); + equal(oldVal, "Justin") + } + + name.bind("change",handler) + + equal(bindCount, 1); + + name.unbind("change",handler); + + equal(bindCount, 0); +}); + +test("binding to a compute on an observe before reading",function(){ + var me = new can.Map({name: "Justin"}); + + var name = can.compute(me,"name") + + + var handler = function(ev, newVal, oldVal){ + equal(newVal, "Justin Meyer"); + equal(oldVal, "Justin") + } + + name.bind("change",handler) + + equal(name(), "Justin"); +}) + +test("compute bound to input value",function(){ + var input = document.createElement('input'); + input.value = 'Justin'; + + var value = can.compute(input, "value","change") + + equal(value(),"Justin"); + + value("Justin M."); + + equal(input.value,"Justin M.","input change correctly"); + + + var handler = function(ev, newVal, oldVal){ + equal(newVal, "Justin Meyer"); + equal(oldVal, "Justin M.") + } + + value.bind("change", handler); + + + input.value = "Justin Meyer"; + + value.unbind("change", handler); + + input.value = "Brian Moschel"; + + equal(value(),"Brian Moschel"); + +}) + +test("compute on the prototype", function(){ + var Person = can.Map.extend({ + fullName: can.compute(function(fullName){ + if(arguments.length){ + var parts = fullName.split(" "); + this.attr({ + first:parts[0], + last: parts[1] + }) + } else { + return this.attr('first')+" "+this.attr('last') + } + + }) + }); + + var me = new Person(); + + var fn = me.attr({ + first: "Justin", + last: "Meyer" + }).attr("fullName"); + + equal(fn, "Justin Meyer", "can read attr") + + me.attr("fullName","Brian Moschel"); + + equal(me.attr("first"),"Brian","set first name"); + equal(me.attr("last"),"Moschel","set last name") + + +}) + + + })(); diff --git a/observe/test.html b/observe/test.html index 0e733298ae3..765a332d864 100644 --- a/observe/test.html +++ b/observe/test.html @@ -17,7 +17,7 @@

      diff --git a/route/demo.html b/route/demo.html index de464e48e90..ddb44d76c99 100644 --- a/route/demo.html +++ b/route/demo.html @@ -102,7 +102,7 @@

      Events

      diff --git a/view/ejs/demo.html b/view/ejs/demo.html index 07e1a30c2d1..c1bfcb2a548 100644 --- a/view/ejs/demo.html +++ b/view/ejs/demo.html @@ -53,7 +53,7 @@ diff --git a/view/live.js b/view/live.js index dcd214a8431..5857f184baf 100644 --- a/view/live.js +++ b/view/live.js @@ -3,7 +3,7 @@ steal('can/util', './elements.js','can/view','./node_lists.js', // ## live.js // // The live module provides live binding for computes - // and can.Observe.List. + // and can.List. // // Currently, it's API is designed for `can/view/render`, but // it could easily be used for other purposes. diff --git a/view/modifiers/test.html b/view/modifiers/test.html index bccdfab7cc7..4f340ec7f2a 100644 --- a/view/modifiers/test.html +++ b/view/modifiers/test.html @@ -21,7 +21,7 @@

      steal.config({ root: '../../' }); - }, "can/observe", "can/view/ejs", "can/view/modifiers").then("can/test", "can/view/modifiers/modifiers_test.js", function() { + }, "can/map", "can/view/ejs", "can/view/modifiers").then("can/test", "can/view/modifiers/modifiers_test.js", function() { QUnit.start(); }); diff --git a/view/mustache/doc/helper.md b/view/mustache/doc/helper.md index 3eb5e30508b..454e7b6213d 100644 --- a/view/mustache/doc/helper.md +++ b/view/mustache/doc/helper.md @@ -24,7 +24,7 @@ Will call a `madLib` helper with the following arguements. // number -> 4 }); -If a [can.Mustache.key] represents a [can.Observe] attribute, +If a [can.Mustache.key] represents a [can.Map] attribute, it is converted to a [can.compute] getter/setter function. This enables 2-way binding helpers. @@ -49,7 +49,7 @@ And used by the following template: And rendered with: - {me: new can.Observe({name: "Payal"})} + {me: new can.Map({name: "Payal"})} @param {can.Mustache.helperOptions} options An options object that gets populated with optional: diff --git a/view/mustache/doc/helpers.md b/view/mustache/doc/helpers.md index e4fde0af1f3..e095a44130d 100644 --- a/view/mustache/doc/helpers.md +++ b/view/mustache/doc/helpers.md @@ -194,15 +194,15 @@ will render: my string localized -__Helpers with can.Observe attributes__ +__Helpers with can.Map attributes__ -If a can.Observe attribute is passed as an argument to a helper, it is converted to a can.compute getter/setter function. This is to allow creating 2-way binding type functionality between a can.Observe attribute and a form element. For example in your template: +If a can.Map attribute is passed as an argument to a helper, it is converted to a can.compute getter/setter function. This is to allow creating 2-way binding type functionality between a can.Map attribute and a form element. For example in your template:
      {{addPrefix name}}
      Your helper would look like: - var item = new can.Observe({name: "Brian"}), + var item = new can.Map({name: "Brian"}), frag = can.view("#template", item, { addPrefix: function(name){ return "Mr." + name() diff --git a/view/mustache/doc/livebinding.md b/view/mustache/doc/livebinding.md index b329ee3af6e..8f8273a6484 100644 --- a/view/mustache/doc/livebinding.md +++ b/view/mustache/doc/livebinding.md @@ -20,7 +20,7 @@ In this example, we have a simple user welcome screen. {{/messages}}

      - var data = new can.Observe({ + var data = new can.Map({ user: 'Tina Fey', messages: 0 }); @@ -35,14 +35,14 @@ Since we have no message it will render: Now say we have a request that updates the `messages` attribute to have `5` messages. We -call the [attr](can.Observe.prototype.attr) method on the [can.Observe](can.Observe) to update +call the [attr](can.Map.prototype.attr) method on the [can.Map](can.Map) to update the attribute to the new value. data.attr('message', 5) -After [can.Observe](can.Observe) recieves this update, it will automatically +After [can.Map](can.Map) recieves this update, it will automatically update the paragraph tag to reflect the new value.

      You have 5 new message.

      -For more information visit the [can.Observe](can.Observe). \ No newline at end of file +For more information visit the [can.Map](can.Map). \ No newline at end of file diff --git a/view/mustache/doc/mustache.md b/view/mustache/doc/mustache.md index dcb4d1912c8..e132c50553c 100644 --- a/view/mustache/doc/mustache.md +++ b/view/mustache/doc/mustache.md @@ -9,7 +9,7 @@ @download http://canjs.us/release/latest/can.view.mustache.js @description Logic-less [http://mustache.github.io/ mustache] templates with live binding -when used with [can.Observes](#can_observe). +when used with [can.Maps](#can_observe). @signature `new can.Mustache(options)` @@ -45,7 +45,7 @@ __Mustache Template__ __JavaScript__ - var data = new can.Observe({ + var data = new can.Map({ user: 'Tina Fey', messages: 0 }); diff --git a/view/mustache/mustache.html b/view/mustache/mustache.html index 91989868461..1e91c85bbc9 100644 --- a/view/mustache/mustache.html +++ b/view/mustache/mustache.html @@ -17,7 +17,7 @@