diff --git a/packages/ember-data.js b/packages/ember-data.js index b0c10f6..ea72af1 100644 --- a/packages/ember-data.js +++ b/packages/ember-data.js @@ -1,5 +1,10 @@ -// Version: v0.13-102-g6bdebe7 -// Last commit: 6bdebe7 (2013-08-14 00:51:19 -0500) +/*! + * @overview Ember Data + * @copyright Copyright 2011-2014 Tilde Inc. and contributors. + * Portions Copyright 2011 LivingSocial Inc. + * @license Licensed under MIT license (see license.js) + * @version 1.0.0-beta.7+canary.238bb5ce + */ (function() { @@ -52,510 +57,812 @@ var define, requireModule; @class DS @static */ - +var DS; if ('undefined' === typeof DS) { + /** + @property VERSION + @type String + @default '1.0.0-beta.7+canary.238bb5ce' + @static + */ DS = Ember.Namespace.create({ - VERSION: '0.13' + VERSION: '1.0.0-beta.7+canary.238bb5ce' }); if ('undefined' !== typeof window) { window.DS = DS; } + + if (Ember.libraries) { + Ember.libraries.registerCoreLibrary('Ember Data', DS.VERSION); + } } + })(); (function() { -var get = Ember.get, set = Ember.set; +/** + This is used internally to enable deprecation of container paths and provide + a decent message to the user indicating how to fix the issue. -DS.NewJSONSerializer = Ember.Object.extend({ - deserialize: function(type, data) { - var store = get(this, 'store'); + @class ContainerProxy + @namespace DS + @private +*/ +var ContainerProxy = function (container){ + this.container = container; +}; - type.eachRelationship(function(key, relationship) { - var type = relationship.type, - value = data[key]; +ContainerProxy.prototype.aliasedFactory = function(path, preLookup) { + var _this = this; - if (value == null) { return; } + return {create: function(){ + if (preLookup) { preLookup(); } - if (relationship.kind === 'belongsTo') { - this.deserializeRecordId(data, key, type, value); - } else if (relationship.kind === 'hasMany') { - this.deserializeRecordIds(data, key, type, value); - } - }, this); + return _this.container.lookup(path); + }}; +}; - return data; - }, +ContainerProxy.prototype.registerAlias = function(source, dest, preLookup) { + var factory = this.aliasedFactory(dest, preLookup); - deserializeRecordId: function(data, key, type, id) { - if (typeof id === 'number' || typeof id === 'string') { - data[key] = get(this, 'store').recordFor(type, id); - } - }, + return this.container.register(source, factory); +}; - deserializeRecordIds: function(data, key, type, ids) { - for (var i=0, l=ids.length; i 0; i--) { + var proxyPair = proxyPairs[i - 1], + deprecated = proxyPair['deprecated'], + valid = proxyPair['valid']; + + this.registerDeprecation(deprecated, valid); } -}); +}; + +DS.ContainerProxy = ContainerProxy; })(); (function() { -/** - @module ember-data -*/ -// Keep ED compatible with previous versions of ember -// TODO: Remove this check for Ember 1.0 -if (!Ember.DataAdapter) { return; } +var get = Ember.get, set = Ember.set, isNone = Ember.isNone; -var get = Ember.get, capitalize = Ember.String.capitalize, underscore = Ember.String.underscore, DS = window.DS ; +// Simple dispatcher to support overriding the aliased +// method in subclasses. +function aliasMethod(methodName) { + return function() { + return this[methodName].apply(this, arguments); + }; +} /** - Extend `Ember.DataAdapter` with ED specific code. + In Ember Data a Serializer is used to serialize and deserialize + records when they are transferred in and out of an external source. + This process involves normalizing property names, transforming + attribute values and serializing relationships. + + For maximum performance Ember Data recommends you use the + [RESTSerializer](DS.RESTSerializer.html) or one of its subclasses. + + `JSONSerializer` is useful for simpler or legacy backends that may + not support the http://jsonapi.org/ spec. + + @class JSONSerializer + @namespace DS */ -DS.DebugAdapter = Ember.DataAdapter.extend({ - getFilters: function() { - return [ - { name: 'isNew', desc: 'New' }, - { name: 'isModified', desc: 'Modified' }, - { name: 'isClean', desc: 'Clean' } - ]; - }, +DS.JSONSerializer = Ember.Object.extend({ + /** + The primaryKey is used when serializing and deserializing + data. Ember Data always uses the `id` property to store the id of + the record. The external source may not always follow this + convention. In these cases it is useful to override the + primaryKey property to match the primaryKey of your external + store. - detect: function(klass) { - return klass !== DS.Model && DS.Model.detect(klass); - }, + Example - columnsForType: function(type) { - var columns = [{ name: 'id', desc: 'Id' }], count = 0, self = this; - Ember.A(get(type, 'attributes')).forEach(function(name, meta) { - if (count++ > self.attributeLimit) { return false; } - var desc = capitalize(underscore(name).replace('_', ' ')); - columns.push({ name: name, desc: desc }); + ```javascript + App.ApplicationSerializer = DS.JSONSerializer.extend({ + primaryKey: '_id' }); - return columns; - }, + ``` - getRecords: function(type) { - return this.get('store').all(type); + @property primaryKey + @type {String} + @default 'id' + */ + primaryKey: 'id', + + /** + Given a subclass of `DS.Model` and a JSON object this method will + iterate through each attribute of the `DS.Model` and invoke the + `DS.Transform#deserialize` method on the matching property of the + JSON object. This method is typically called after the + serializer's `normalize` method. + + @method applyTransforms + @private + @param {subclass of DS.Model} type + @param {Object} data The data to transform + @return {Object} data The transformed data object + */ + applyTransforms: function(type, data) { + type.eachTransformedAttribute(function(key, type) { + var transform = this.transformFor(type); + data[key] = transform.deserialize(data[key]); + }, this); + + return data; }, - getRecordColumnValues: function(record) { - var self = this, count = 0, - columnValues = { id: get(record, 'id') }; + /** + Normalizes a part of the JSON payload returned by + the server. You should override this method, munge the hash + and call super if you have generic normalization to do. - record.eachAttribute(function(key) { - if (count++ > self.attributeLimit) { - return false; + It takes the type of the record that is being normalized + (as a DS.Model class), the property where the hash was + originally found, and the hash to normalize. + + You can use this method, for example, to normalize underscored keys to camelized + or other general-purpose normalizations. + + Example + + ```javascript + App.ApplicationSerializer = DS.JSONSerializer.extend({ + normalize: function(type, hash) { + var fields = Ember.get(type, 'fields'); + fields.forEach(function(field) { + var payloadField = Ember.String.underscore(field); + if (field === payloadField) { return; } + + hash[field] = hash[payloadField]; + delete hash[payloadField]; + }); + return this._super.apply(this, arguments); } - var value = get(record, key); - columnValues[key] = value; }); - return columnValues; - }, + ``` - getRecordKeywords: function(record) { - var keywords = [], keys = Ember.A(['id']); - record.eachAttribute(function(key) { - keys.push(key); - }); - keys.forEach(function(key) { - keywords.push(get(record, key)); - }); - return keywords; - }, + @method normalize + @param {subclass of DS.Model} type + @param {Object} hash + @return {Object} + */ + normalize: function(type, hash) { + if (!hash) { return hash; } - getRecordFilterValues: function(record) { - return { - isNew: record.get('isNew'), - isModified: record.get('isDirty') && !record.get('isNew'), - isClean: !record.get('isDirty') - }; + this.applyTransforms(type, hash); + return hash; }, - getRecordColor: function(record) { - var color = 'black'; - if (record.get('isNew')) { - color = 'green'; - } else if (record.get('isDirty')) { - color = 'blue'; - } - return color; - }, + // SERIALIZE + /** + Called when a record is saved in order to convert the + record into JSON. - observeRecord: function(record, recordUpdated) { - var releaseMethods = Ember.A(), self = this, - keysToObserve = Ember.A(['id', 'isNew', 'isDirty']); + By default, it creates a JSON object with a key for + each attribute and belongsTo relationship. - record.eachAttribute(function(key) { - keysToObserve.push(key); - }); + For example, consider this model: - keysToObserve.forEach(function(key) { - var handler = function() { - recordUpdated(self.wrapRecord(record)); - }; - Ember.addObserver(record, key, handler); - releaseMethods.push(function() { - Ember.removeObserver(record, key, handler); - }); + ```javascript + App.Comment = DS.Model.extend({ + title: DS.attr(), + body: DS.attr(), + + author: DS.belongsTo('user') }); + ``` - var release = function() { - releaseMethods.forEach(function(fn) { fn(); } ); - }; + The default serialization would create a JSON object like: - return release; - } + ```javascript + { + "title": "Rails is unagi", + "body": "Rails? Omakase? O_O", + "author": 12 + } + ``` -}); + By default, attributes are passed through as-is, unless + you specified an attribute type (`DS.attr('date')`). If + you specify a transform, the JavaScript value will be + serialized when inserted into the JSON hash. + By default, belongs-to relationships are converted into + IDs when inserted into the JSON hash. -})(); + ## IDs + `serialize` takes an options hash with a single option: + `includeId`. If this option is `true`, `serialize` will, + by default include the ID in the JSON object it builds. + The adapter passes in `includeId: true` when serializing + a record for `createRecord`, but not for `updateRecord`. -(function() { -/** - @module ember-data -*/ + ## Customization -var set = Ember.set; + Your server may expect a different JSON format than the + built-in serialization format. -/* - This code registers an injection for Ember.Application. + In that case, you can implement `serialize` yourself and + return a JSON hash of your choosing. - If an Ember.js developer defines a subclass of DS.Store on their application, - this code will automatically instantiate it and make it available on the - router. + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + serialize: function(post, options) { + var json = { + POST_TTL: post.get('title'), + POST_BDY: post.get('body'), + POST_CMS: post.get('comments').mapProperty('id') + } - Additionally, after an application's controllers have been injected, they will - each have the store made available to them. + if (options.includeId) { + json.POST_ID_ = post.get('id'); + } - For example, imagine an Ember.js application with the following classes: + return json; + } + }); + ``` - App.Store = DS.Store.extend({ - adapter: 'App.MyCustomAdapter' - }); + ## Customizing an App-Wide Serializer - App.PostsController = Ember.ArrayController.extend({ - // ... - }); + If you want to define a serializer for your entire + application, you'll probably want to use `eachAttribute` + and `eachRelationship` on the record. - When the application is initialized, `App.Store` will automatically be - instantiated, and the instance of `App.PostsController` will have its `store` - property set to that instance. + ```javascript + App.ApplicationSerializer = DS.JSONSerializer.extend({ + serialize: function(record, options) { + var json = {}; - Note that this code will only be run if the `ember-application` package is - loaded. If Ember Data is being used in an environment other than a - typical application (e.g., node.js where only `ember-runtime` is available), - this code will be ignored. -*/ + record.eachAttribute(function(name) { + json[serverAttributeName(name)] = record.get(name); + }) -Ember.onLoad('Ember.Application', function(Application) { - Application.initializer({ - name: "store", + record.eachRelationship(function(name, relationship) { + if (relationship.kind === 'hasMany') { + json[serverHasManyName(name)] = record.get(name).mapBy('id'); + } + }); - initialize: function(container, application) { - Ember.assert("You included Ember Data but didn't define "+application.toString()+".Store", application.Store); + if (options.includeId) { + json.ID_ = record.get('id'); + } + + return json; + } + }); - application.register('store:main', application.Store); - application.register('serializer:_default', DS.NewJSONSerializer); + function serverAttributeName(attribute) { + return attribute.underscore().toUpperCase(); + } - // Eagerly generate the store so defaultStore is populated. - // TODO: Do this in a finisher hook - container.lookup('store:main'); + function serverHasManyName(name) { + return serverAttributeName(name.singularize()) + "_IDS"; } - }); + ``` + + This serializer will generate JSON that looks like this: + + ```javascript + { + "TITLE": "Rails is omakase", + "BODY": "Yep. Omakase.", + "COMMENT_IDS": [ 1, 2, 3 ] + } + ``` + + ## Tweaking the Default JSON + + If you just want to do some small tweaks on the default JSON, + you can call super first and make the tweaks on the returned + JSON. + + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + serialize: function(record, options) { + var json = this._super.apply(this, arguments); - // Keep ED compatible with previous versions of ember - // TODO: Remove the if statement for Ember 1.0 - if (DS.DebugAdapter) { - Application.initializer({ - name: "dataAdapter", + json.subject = json.title; + delete json.title; - initialize: function(container, application) { - application.register('dataAdapter:main', DS.DebugAdapter); + return json; } }); - } + ``` - Application.initializer({ - name: "injectStore", + @method serialize + @param {subclass of DS.Model} record + @param {Object} options + @return {Object} json + */ + serialize: function(record, options) { + var json = {}; - initialize: function(container, application) { - application.inject('controller', 'store', 'store:main'); - application.inject('route', 'store', 'store:main'); - application.inject('dataAdapter', 'store', 'store:main'); + if (options && options.includeId) { + var id = get(record, 'id'); + + if (id) { + json[get(this, 'primaryKey')] = id; + } } - }); -}); + record.eachAttribute(function(key, attribute) { + this.serializeAttribute(record, json, key, attribute); + }, this); -})(); + record.eachRelationship(function(key, relationship) { + if (relationship.kind === 'belongsTo') { + this.serializeBelongsTo(record, json, relationship); + } else if (relationship.kind === 'hasMany') { + this.serializeHasMany(record, json, relationship); + } + }, this); + return json; + }, + /** + `serializeAttribute` can be used to customize how `DS.attr` + properties are serialized -(function() { -/** - @module ember-data -*/ + For example if you wanted to ensure all you attributes were always + serialized as properties on an `attributes` object you could + write: -/** - Date.parse with progressive enhancement for ISO 8601 + ```javascript + App.ApplicationSerializer = DS.JSONSerializer.extend({ + serializeAttribute: function(record, json, key, attributes) { + json.attributes = json.attributes || {}; + this._super(record, json.attributes, key, attributes); + } + }); + ``` - © 2011 Colin Snover + @method serializeAttribute + @param {DS.Model} record + @param {Object} json + @param {String} key + @param {Object} attribute + */ + serializeAttribute: function(record, json, key, attribute) { + var attrs = get(this, 'attrs'); + var value = get(record, key), type = attribute.type; - Released under MIT license. + if (type) { + var transform = this.transformFor(type); + value = transform.serialize(value); + } - @class Date - @namespace Ember - @static -*/ -Ember.Date = Ember.Date || {}; + // if provided, use the mapping provided by `attrs` in + // the serializer + key = attrs && attrs[key] || (this.keyForAttribute ? this.keyForAttribute(key) : key); -var origParse = Date.parse, numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; + json[key] = value; + }, -/** - @method parse - @param date -*/ -Ember.Date.parse = function (date) { - var timestamp, struct, minutesOffset = 0; + /** + `serializeBelongsTo` can be used to customize how `DS.belongsTo` + properties are serialized. - // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string - // before falling back to any implementation-specific date parsing, so that’s what we do, even if native - // implementations could be faster - // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm - if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) { - // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC - for (var i = 0, k; (k = numericKeys[i]); ++i) { - struct[k] = +struct[k] || 0; - } + Example - // allow undefined days and months - struct[2] = (+struct[2] || 1) - 1; - struct[3] = +struct[3] || 1; + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + serializeBelongsTo: function(record, json, relationship) { + var key = relationship.key; - if (struct[8] !== 'Z' && struct[9] !== undefined) { - minutesOffset = struct[10] * 60 + struct[11]; + var belongsTo = get(record, key); - if (struct[9] === '+') { - minutesOffset = 0 - minutesOffset; - } - } + key = this.keyForRelationship ? this.keyForRelationship(key, "belongsTo") : key; - timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]); - } - else { - timestamp = origParse ? origParse(date) : NaN; - } + json[key] = Ember.isNone(belongsTo) ? belongsTo : belongsTo.toJSON(); + } + }); + ``` - return timestamp; -}; + @method serializeBelongsTo + @param {DS.Model} record + @param {Object} json + @param {Object} relationship + */ + serializeBelongsTo: function(record, json, relationship) { + var key = relationship.key; -if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Date) { - Date.parse = Ember.Date.parse; -} + var belongsTo = get(record, key); -})(); + key = this.keyForRelationship ? this.keyForRelationship(key, "belongsTo") : key; + if (isNone(belongsTo)) { + json[key] = belongsTo; + } else { + json[key] = get(belongsTo, 'id'); + } + if (relationship.options.polymorphic) { + this.serializePolymorphicType(record, json, relationship); + } + }, -(function() { + /** + `serializeHasMany` can be used to customize how `DS.hasMany` + properties are serialized. -})(); + Example + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + serializeHasMany: function(record, json, relationship) { + var key = relationship.key; + if (key === 'comments') { + return; + } else { + this._super.apply(this, arguments); + } + } + }); + ``` + @method serializeHasMany + @param {DS.Model} record + @param {Object} json + @param {Object} relationship + */ + serializeHasMany: function(record, json, relationship) { + var key = relationship.key; -(function() { -/** - @module ember-data -*/ + var relationshipType = DS.RelationshipChange.determineRelationshipType(record.constructor, relationship); -var Evented = Ember.Evented, // ember-runtime/mixins/evented - Deferred = Ember.DeferredMixin, // ember-runtime/mixins/evented - run = Ember.run, // ember-metal/run-loop - get = Ember.get; // ember-metal/accessors + if (relationshipType === 'manyToNone' || relationshipType === 'manyToMany') { + json[key] = get(record, key).mapBy('id'); + // TODO support for polymorphic manyToNone and manyToMany relationships + } + }, -var LoadPromise = Ember.Mixin.create(Evented, Deferred, { - init: function() { - this._super.apply(this, arguments); + /** + You can use this method to customize how polymorphic objects are + serialized. Objects are considered to be polymorphic if + `{polymorphic: true}` is pass as the second argument to the + `DS.belongsTo` function. - this.one('didLoad', this, function() { - this.resolve(this); - }); + Example - this.one('becameError', this, function() { - this.reject(this); + ```javascript + App.CommentSerializer = DS.JSONSerializer.extend({ + serializePolymorphicType: function(record, json, relationship) { + var key = relationship.key, + belongsTo = get(record, key); + key = this.keyForAttribute ? this.keyForAttribute(key) : key; + json[key + "_type"] = belongsTo.constructor.typeKey; + } }); + ``` - if (get(this, 'isLoaded')) { - this.trigger('didLoad'); - } - } -}); + @method serializePolymorphicType + @param {DS.Model} record + @param {Object} json + @param {Object} relationship + */ + serializePolymorphicType: Ember.K, -DS.LoadPromise = LoadPromise; + // EXTRACT -})(); + /** + The `extract` method is used to deserialize payload data from the + server. By default the `JSONSerializer` does not push the records + into the store. However records that subclass `JSONSerializer` + such as the `RESTSerializer` may push records into the store as + part of the extract call. + This method delegates to a more specific extract method based on + the `requestType`. + Example -(function() { -/** - @module ember-data -*/ + ```javascript + var get = Ember.get; + socket.on('message', function(message) { + var modelName = message.model; + var data = message.data; + var type = store.modelFor(modelName); + var serializer = store.serializerFor(type.typeKey); + var record = serializer.extract(store, type, data, get(data, 'id'), 'single'); + store.push(modelName, record); + }); + ``` -var get = Ember.get, set = Ember.set; + @method extract + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @param {String or Number} id + @param {String} requestType + @return {Object} json The deserialized payload + */ + extract: function(store, type, payload, id, requestType) { + this.extractMeta(store, type, payload); -var LoadPromise = DS.LoadPromise; // system/mixins/load_promise + var specificExtract = "extract" + requestType.charAt(0).toUpperCase() + requestType.substr(1); + return this[specificExtract](store, type, payload, id, requestType); + }, -/** - A record array is an array that contains records of a certain type. The record - array materializes records as needed when they are retrieved for the first - time. You should not create record arrays yourself. Instead, an instance of - DS.RecordArray or its subclasses will be returned by your application's store - in response to queries. + /** + `extractFindAll` is a hook into the extract method used when a + call is made to `DS.Store#findAll`. By default this method is an + alias for [extractArray](#method_extractArray). - @class RecordArray - @namespace DS - @extends Ember.ArrayProxy - @uses Ember.Evented - @uses DS.LoadPromise -*/ + @method extractFindAll + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Array} array An array of deserialized objects + */ + extractFindAll: aliasMethod('extractArray'), + /** + `extractFindQuery` is a hook into the extract method used when a + call is made to `DS.Store#findQuery`. By default this method is an + alias for [extractArray](#method_extractArray). -DS.RecordArray = Ember.ArrayProxy.extend(LoadPromise, { + @method extractFindQuery + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Array} array An array of deserialized objects + */ + extractFindQuery: aliasMethod('extractArray'), /** - The model type contained by this record array. + `extractFindMany` is a hook into the extract method used when a + call is made to `DS.Store#findMany`. By default this method is + alias for [extractArray](#method_extractArray). - @property type - @type DS.Model + @method extractFindMany + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Array} array An array of deserialized objects */ - type: null, + extractFindMany: aliasMethod('extractArray'), + /** + `extractFindHasMany` is a hook into the extract method used when a + call is made to `DS.Store#findHasMany`. By default this method is + alias for [extractArray](#method_extractArray). - // The array of client ids backing the record array. When a - // record is requested from the record array, the record - // for the client id at the same index is materialized, if - // necessary, by the store. - content: null, + @method extractFindHasMany + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Array} array An array of deserialized objects + */ + extractFindHasMany: aliasMethod('extractArray'), - isLoaded: false, - isUpdating: false, + /** + `extractCreateRecord` is a hook into the extract method used when a + call is made to `DS.Store#createRecord`. By default this method is + alias for [extractSave](#method_extractSave). - // The store that created this record array. - store: null, + @method extractCreateRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractCreateRecord: aliasMethod('extractSave'), + /** + `extractUpdateRecord` is a hook into the extract method used when + a call is made to `DS.Store#update`. By default this method is alias + for [extractSave](#method_extractSave). - objectAtContent: function(index) { - var content = get(this, 'content'), - reference = content.objectAt(index), - store = get(this, 'store'); + @method extractUpdateRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractUpdateRecord: aliasMethod('extractSave'), + /** + `extractDeleteRecord` is a hook into the extract method used when + a call is made to `DS.Store#deleteRecord`. By default this method is + alias for [extractSave](#method_extractSave). - if (reference instanceof DS.Model) { - return reference; - } + @method extractDeleteRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractDeleteRecord: aliasMethod('extractSave'), - if (reference) { - return store.recordForReference(reference); - } - }, + /** + `extractFind` is a hook into the extract method used when + a call is made to `DS.Store#find`. By default this method is + alias for [extractSingle](#method_extractSingle). + + @method extractFind + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractFind: aliasMethod('extractSingle'), + /** + `extractFindBelongsTo` is a hook into the extract method used when + a call is made to `DS.Store#findBelongsTo`. By default this method is + alias for [extractSingle](#method_extractSingle). - materializedObjectAt: function(index) { - var reference = get(this, 'content').objectAt(index); - if (!reference) { return; } + @method extractFindBelongsTo + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractFindBelongsTo: aliasMethod('extractSingle'), + /** + `extractSave` is a hook into the extract method used when a call + is made to `DS.Model#save`. By default this method is alias + for [extractSingle](#method_extractSingle). - if (get(this, 'store').recordIsMaterialized(reference)) { - return this.objectAt(index); - } - }, + @method extractSave + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractSave: aliasMethod('extractSingle'), - update: function() { - if (get(this, 'isUpdating')) { return; } + /** + `extractSingle` is used to deserialize a single record returned + from the adapter. - var store = get(this, 'store'), - type = get(this, 'type'); + Example - store.fetchAll(type, this); - }, + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + extractSingle: function(store, type, payload) { + payload.comments = payload._embedded.comment; + delete payload._embedded; + + return this._super(store, type, payload); + }, + }); + ``` - addReference: function(reference) { - get(this, 'content').addObject(reference); + @method extractSingle + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractSingle: function(store, type, payload) { + return this.normalize(type, payload); }, - removeReference: function(reference) { - get(this, 'content').removeObject(reference); - } -}); + /** + `extractArray` is used to deserialize an array of records + returned from the adapter. -})(); + Example + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + extractArray: function(store, type, payload) { + return payload.map(function(json) { + return this.extractSingle(json); + }, this); + } + }); + ``` + @method extractArray + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Array} array An array of deserialized objects + */ + extractArray: function(store, type, payload) { + return this.normalize(type, payload); + }, -(function() { -/** - @module ember-data -*/ + /** + `extractMeta` is used to deserialize any meta information in the + adapter payload. By default Ember Data expects meta information to + be located on the `meta` property of the payload object. -var get = Ember.get; + Example -/** - @class FilteredRecordArray - @namespace DS - @extends DS.RecordArray -*/ -DS.FilteredRecordArray = DS.RecordArray.extend({ - filterFunction: null, - isLoaded: true, + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + extractMeta: function(store, type, payload) { + if (payload && payload._pagination) { + store.metaForType(type, payload._pagination); + delete payload._pagination; + } + } + }); + ``` - replace: function() { - var type = get(this, 'type').toString(); - throw new Error("The result of a client-side filter (on " + type + ") is immutable."); + @method extractMeta + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + */ + extractMeta: function(store, type, payload) { + if (payload && payload.meta) { + store.metaForType(type, payload.meta); + delete payload.meta; + } }, - updateFilter: Ember.observer(function() { - var manager = get(this, 'manager'); - manager.updateFilter(this, get(this, 'type'), get(this, 'filterFunction')); - }, 'filterFunction') -}); - -})(); + /** + `keyForAttribute` can be used to define rules for how to convert an + attribute name in your model to a key in your JSON. + Example + ```javascript + App.ApplicationSerializer = DS.RESTSerializer.extend({ + keyForAttribute: function(attr) { + return Ember.String.underscore(attr).toUpperCase(); + } + }); + ``` -(function() { -/** - @module ember-data -*/ + @method keyForAttribute + @param {String} key + @return {String} normalized key + */ -var get = Ember.get, set = Ember.set; -/** - @class AdapterPopulatedRecordArray - @namespace DS - @extends DS.RecordArray -*/ -DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ - query: null, + /** + `keyForRelationship` can be used to define a custom key when + serializing relationship properties. By default `JSONSerializer` + does not provide an implementation of this method. - replace: function() { - var type = get(this, 'type').toString(); - throw new Error("The result of a server query (on " + type + ") is immutable."); - }, + Example - load: function(references) { - this.setProperties({ - content: Ember.A(references), - isLoaded: true + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + keyForRelationship: function(key, relationship) { + return 'rel_' + Ember.String.underscore(key); + } }); + ``` - // TODO: does triggering didLoad event should be the last action of the runLoop? - Ember.run.once(this, 'trigger', 'didLoad'); + @method keyForRelationship + @param {String} key + @param {String} relationship type + @return {String} normalized key + */ + + // HELPERS + + /** + @method transformFor + @private + @param {String} attributeType + @param {Boolean} skipAssertion + @return {DS.Transform} transform + */ + transformFor: function(attributeType, skipAssertion) { + var transform = this.container.lookup('transform:' + attributeType); + Ember.assert("Unable to find transform for '" + attributeType + "'", skipAssertion || !!transform); + return transform; } }); @@ -567,191 +874,187 @@ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ /** @module ember-data */ - -var get = Ember.get, set = Ember.set; -var map = Ember.EnumerableUtils.map; +var get = Ember.get, capitalize = Ember.String.capitalize, underscore = Ember.String.underscore, DS = window.DS ; /** - A ManyArray is a RecordArray that represents the contents of a has-many - relationship. + Extend `Ember.DataAdapter` with ED specific code. - The ManyArray is instantiated lazily the first time the relationship is - requested. + @class DebugAdapter + @namespace DS + @extends Ember.DataAdapter + @private +*/ +DS.DebugAdapter = Ember.DataAdapter.extend({ + getFilters: function() { + return [ + { name: 'isNew', desc: 'New' }, + { name: 'isModified', desc: 'Modified' }, + { name: 'isClean', desc: 'Clean' } + ]; + }, - ### Inverses + detect: function(klass) { + return klass !== DS.Model && DS.Model.detect(klass); + }, - Often, the relationships in Ember Data applications will have - an inverse. For example, imagine the following models are - defined: - - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); - - App.Comment = DS.Model.extend({ - post: DS.belongsTo('App.Post') - }); - - If you created a new instance of `App.Post` and added - a `App.Comment` record to its `comments` has-many - relationship, you would expect the comment's `post` - property to be set to the post that contained - the has-many. - - We call the record to which a relationship belongs the - relationship's _owner_. - - @class ManyArray - @namespace DS - @extends DS.RecordArray -*/ -DS.ManyArray = DS.RecordArray.extend({ - init: function() { - this._super.apply(this, arguments); - this._changesToSync = Ember.OrderedSet.create(); + columnsForType: function(type) { + var columns = [{ name: 'id', desc: 'Id' }], count = 0, self = this; + get(type, 'attributes').forEach(function(name, meta) { + if (count++ > self.attributeLimit) { return false; } + var desc = capitalize(underscore(name).replace('_', ' ')); + columns.push({ name: name, desc: desc }); + }); + return columns; }, - /** - The record to which this relationship belongs. - - @property {DS.Model} - @private - */ - owner: null, - - /** - `true` if the relationship is polymorphic, `false` otherwise. + getRecords: function(type) { + return this.get('store').all(type); + }, - @property {Boolean} - @private - */ - isPolymorphic: false, + getRecordColumnValues: function(record) { + var self = this, count = 0, + columnValues = { id: get(record, 'id') }; - // LOADING STATE + record.eachAttribute(function(key) { + if (count++ > self.attributeLimit) { + return false; + } + var value = get(record, key); + columnValues[key] = value; + }); + return columnValues; + }, - isLoaded: false, + getRecordKeywords: function(record) { + var keywords = [], keys = Ember.A(['id']); + record.eachAttribute(function(key) { + keys.push(key); + }); + keys.forEach(function(key) { + keywords.push(get(record, key)); + }); + return keywords; + }, - loadingRecordsCount: function(count) { - this.loadingRecordsCount = count; + getRecordFilterValues: function(record) { + return { + isNew: record.get('isNew'), + isModified: record.get('isDirty') && !record.get('isNew'), + isClean: !record.get('isDirty') + }; }, - loadedRecord: function() { - this.loadingRecordsCount--; - if (this.loadingRecordsCount === 0) { - set(this, 'isLoaded', true); - this.trigger('didLoad'); + getRecordColor: function(record) { + var color = 'black'; + if (record.get('isNew')) { + color = 'green'; + } else if (record.get('isDirty')) { + color = 'blue'; } + return color; }, - fetch: function() { - var references = get(this, 'content'), - store = get(this, 'store'), - owner = get(this, 'owner'); + observeRecord: function(record, recordUpdated) { + var releaseMethods = Ember.A(), self = this, + keysToObserve = Ember.A(['id', 'isNew', 'isDirty']); - store.fetchUnloadedReferences(references, owner); - }, + record.eachAttribute(function(key) { + keysToObserve.push(key); + }); - // Overrides Ember.Array's replace method to implement - replaceContent: function(index, removed, added) { - // Map the array of record objects into an array of client ids. - added = map(added, function(record) { - Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type').detectInstance(record)) ); - return get(record, '_reference'); - }, this); + keysToObserve.forEach(function(key) { + var handler = function() { + recordUpdated(self.wrapRecord(record)); + }; + Ember.addObserver(record, key, handler); + releaseMethods.push(function() { + Ember.removeObserver(record, key, handler); + }); + }); - this._super(index, removed, added); - }, + var release = function() { + releaseMethods.forEach(function(fn) { fn(); } ); + }; - arrangedContentDidChange: function() { - this.fetch(); - }, + return release; + } - arrayContentWillChange: function(index, removed, added) { - var owner = get(this, 'owner'), - name = get(this, 'name'); +}); - if (!owner._suspendedRelationships) { - // This code is the first half of code that continues inside - // of arrayContentDidChange. It gets or creates a change from - // the child object, adds the current owner as the old - // parent if this is the first time the object was removed - // from a ManyArray, and sets `newParent` to null. - // - // Later, if the object is added to another ManyArray, - // the `arrayContentDidChange` will set `newParent` on - // the change. - for (var i=index; i - @method removeCleanRecords - @private - */ - removeCleanRecords: function() { - var records = get(this, 'records'); - forEach(records, function(record) { - if(!record.get('isDirty')) { - this.remove(record); - } - }, this); - }, + © 2011 Colin Snover - /** - This method moves a record into a different transaction without the normal - checks that ensure that the user is not doing something weird, like moving - a dirty record into a new transaction. + Released under MIT license. - It is designed for internal use, such as when we are moving a clean record - into a new transaction when the transaction is committed. + @class Date + @namespace Ember + @static +*/ +Ember.Date = Ember.Date || {}; - This method must not be called unless the record is clean. +var origParse = Date.parse, numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; - @method adoptRecord - @private - @param {DS.Model} record - */ - adoptRecord: function(record) { - var oldTransaction = get(record, 'transaction'); +/** + @method parse + @param date +*/ +Ember.Date.parse = function (date) { + var timestamp, struct, minutesOffset = 0; - if (oldTransaction) { - oldTransaction.removeRecord(record); - } + // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string + // before falling back to any implementation-specific date parsing, so that’s what we do, even if native + // implementations could be faster + // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm + if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) { + // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC + for (var i = 0, k; (k = numericKeys[i]); ++i) { + struct[k] = +struct[k] || 0; + } - get(this, 'records').add(record); - set(record, 'transaction', this); - }, + // allow undefined days and months + struct[2] = (+struct[2] || 1) - 1; + struct[3] = +struct[3] || 1; - /** - Removes the record without performing the normal checks - to ensure that the record is re-added to the store's - default transaction. + if (struct[8] !== 'Z' && struct[9] !== undefined) { + minutesOffset = struct[10] * 60 + struct[11]; - @method removeRecord - @private - @param record - */ - removeRecord: function(record) { - get(this, 'records').remove(record); - } + if (struct[9] === '+') { + minutesOffset = 0 - minutesOffset; + } + } -}); + timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]); + } + else { + timestamp = origParse ? origParse(date) : NaN; + } -DS.Transaction.reopenClass({ - ensureSameTransaction: function(records){ - var transactions = Ember.A(); - forEach( records, function(record){ - if (record){ transactions.pushObject(get(record, 'transaction')); } - }); + return timestamp; +}; - var transaction = transactions.reduce(function(prev, t) { - if (!get(t, 'isDefault')) { - if (prev === null) { return t; } - Ember.assert("All records in a changed relationship must be in the same transaction. You tried to change the relationship between records when one is in " + t + " and the other is in " + prev, t === prev); - } +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Date) { + Date.parse = Ember.Date.parse; +} - return prev; - }, null); +})(); - if (transaction) { - forEach( records, function(record){ - if (record){ transaction.add(record); } - }); - } else { - transaction = transactions.objectAt(0); - } - return transaction; - } -}); + + +(function() { })(); @@ -1152,898 +1426,1157 @@ DS.Transaction.reopenClass({ @module ember-data */ -var get = Ember.get; +var get = Ember.get, set = Ember.set; -var resolveMapConflict = function(oldValue, newValue) { - return oldValue; -}; +/** + A record array is an array that contains records of a certain type. The record + array materializes records as needed when they are retrieved for the first + time. You should not create record arrays yourself. Instead, an instance of + `DS.RecordArray` or its subclasses will be returned by your application's store + in response to queries. -var transformMapKey = function(key, value) { - return key; -}; + @class RecordArray + @namespace DS + @extends Ember.ArrayProxy + @uses Ember.Evented +*/ -var transformMapValue = function(key, value) { - return value; -}; +DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, { + /** + The model type contained by this record array. -/** - The Mappable mixin is designed for classes that would like to - behave as a map for configuration purposes. + @property type + @type DS.Model + */ + type: null, - For example, the DS.Adapter class can behave like a map, with - more semantic API, via the `map` API: + /** + The array of client ids backing the record array. When a + record is requested from the record array, the record + for the client id at the same index is materialized, if + necessary, by the store. - DS.Adapter.map('App.Person', { firstName: { key: 'FIRST' } }); + @property content + @private + @type Ember.Array + */ + content: null, - Class configuration via a map-like API has a few common requirements - that differentiate it from the standard Ember.Map implementation. + /** + The flag to signal a `RecordArray` is currently loading data. - First, values often are provided as strings that should be normalized - into classes the first time the configuration options are used. + Example - Second, the values configured on parent classes should also be taken - into account. + ```javascript + var people = store.all(App.Person); + people.get('isLoaded'); // true + ``` - Finally, setting the value of a key sometimes should merge with the - previous value, rather than replacing it. + @property isLoaded + @type Boolean + */ + isLoaded: false, + /** + The flag to signal a `RecordArray` is currently loading data. - This mixin provides a instance method, `createInstanceMapFor`, that - will reify all of the configuration options set on an instance's - constructor and provide it for the instance to use. + Example - Classes can implement certain hooks that allow them to customize - the requirements listed above: + ```javascript + var people = store.all(App.Person); + people.get('isUpdating'); // false + people.update(); + people.get('isUpdating'); // true + ``` - * `resolveMapConflict` - called when a value is set for an existing - value - * `transformMapKey` - allows a key name (for example, a global path - to a class) to be normalized - * `transformMapValue` - allows a value (for example, a class that - should be instantiated) to be normalized + @property isUpdating + @type Boolean + */ + isUpdating: false, - Classes that implement this mixin should also implement a class - method built using the `generateMapFunctionFor` method: + /** + The store that created this record array. - DS.Adapter.reopenClass({ - map: DS.Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) { - var existingValue = map.get(key); + @property store + @private + @type DS.Store + */ + store: null, - for (var prop in newValue) { - if (!newValue.hasOwnProperty(prop)) { continue; } - existingValue[prop] = newValue[prop]; - } - }) - }); + /** + Retrieves an object from the content by index. - The function passed to `generateMapFunctionFor` is invoked every time a - new value is added to the map. + @method objectAtContent + @private + @param {Number} index + @return {DS.Model} record + */ + objectAtContent: function(index) { + var content = get(this, 'content'); - @class _Mappable - @private - @namespace DS -**/ -DS._Mappable = Ember.Mixin.create({ - createInstanceMapFor: function(mapName) { - var instanceMeta = getMappableMeta(this); + return content.objectAt(index); + }, - instanceMeta.values = instanceMeta.values || {}; + /** + Used to get the latest version of all of the records in this array + from the adapter. - if (instanceMeta.values[mapName]) { return instanceMeta.values[mapName]; } + Example - var instanceMap = instanceMeta.values[mapName] = new Ember.Map(); + ```javascript + var people = store.all(App.Person); + people.get('isUpdating'); // false + people.update(); + people.get('isUpdating'); // true + ``` - var klass = this.constructor; + @method update + */ + update: function() { + if (get(this, 'isUpdating')) { return; } - while (klass && klass !== DS.Store) { - this._copyMap(mapName, klass, instanceMap); - klass = klass.superclass; - } + var store = get(this, 'store'), + type = get(this, 'type'); - instanceMeta.values[mapName] = instanceMap; - return instanceMap; + return store.fetchAll(type, this); }, - _copyMap: function(mapName, klass, instanceMap) { - var classMeta = getMappableMeta(klass); - - var classMap = classMeta[mapName]; - if (classMap) { - classMap.forEach(eachMap, this); - } + /** + Adds a record to the `RecordArray`. - function eachMap(key, value) { - var transformedKey = (klass.transformMapKey || transformMapKey)(key, value); - var transformedValue = (klass.transformMapValue || transformMapValue)(key, value); - - var oldValue = instanceMap.get(transformedKey); - var newValue = transformedValue; - - if (oldValue) { - newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue); - } + @method addRecord + @private + @param {DS.Model} record + */ + addRecord: function(record) { + get(this, 'content').addObject(record); + }, - instanceMap.set(transformedKey, newValue); - } - } + /** + Removes a record to the `RecordArray`. + @method removeRecord + @private + @param {DS.Model} record + */ + removeRecord: function(record) { + get(this, 'content').removeObject(record); + }, -}); + /** + Saves all of the records in the `RecordArray`. -DS._Mappable.generateMapFunctionFor = function(mapName, transform) { - return function(key, value) { - var meta = getMappableMeta(this); + Example - var map = meta[mapName] || Ember.MapWithDefault.create({ - defaultValue: function() { return {}; } + ```javascript + var messages = store.all(App.Message); + messages.forEach(function(message) { + message.set('hasBeenSeen', true); }); + messages.save(); + ``` - transform.call(this, key, value, map); - - meta[mapName] = map; - }; -}; - -function getMappableMeta(obj) { - var meta = Ember.meta(obj, true), - keyName = 'DS.Mappable', - value = meta[keyName]; - - if (!value) { meta[keyName] = {}; } + @method save + @return {DS.PromiseArray} promise + */ + save: function() { + var promiseLabel = "DS: RecordArray#save " + get(this, 'type'); + var promise = Ember.RSVP.all(this.invoke("save"), promiseLabel).then(function(array) { + return Ember.A(array); + }, null, "DS: RecordArray#save apply Ember.NativeArray"); - if (!meta.hasOwnProperty(keyName)) { - meta[keyName] = Ember.create(meta[keyName]); + return DS.PromiseArray.create({ promise: promise }); } - - return meta[keyName]; -} +}); })(); (function() { -/*globals Ember*/ -/*jshint eqnull:true*/ /** @module ember-data */ -var get = Ember.get, set = Ember.set; -var once = Ember.run.once; -var isNone = Ember.isNone; -var forEach = Ember.EnumerableUtils.forEach; -var indexOf = Ember.EnumerableUtils.indexOf; -var map = Ember.EnumerableUtils.map; +var get = Ember.get; -// These values are used in the data cache when clientIds are -// needed but the underlying data has not yet been loaded by -// the server. -var UNLOADED = 'unloaded'; -var LOADING = 'loading'; -var MATERIALIZED = { materialized: true }; -var CREATED = { created: true }; +/** + Represents a list of records whose membership is determined by the + store. As records are created, loaded, or modified, the store + evaluates them to determine if they should be part of the record + array. -// Implementors Note: -// -// The variables in this file are consistently named according to the following -// scheme: -// -// * +id+ means an identifier managed by an external source, provided inside -// the data provided by that source. These are always coerced to be strings -// before being used internally. -// * +clientId+ means a transient numerical identifier generated at runtime by -// the data store. It is important primarily because newly created objects may -// not yet have an externally generated id. -// * +reference+ means a record reference object, which holds metadata about a -// record, even if it has not yet been fully materialized. -// * +type+ means a subclass of DS.Model. + @class FilteredRecordArray + @namespace DS + @extends DS.RecordArray +*/ +DS.FilteredRecordArray = DS.RecordArray.extend({ + /** + The filterFunction is a function used to test records from the store to + determine if they should be part of the record array. -// Used by the store to normalize IDs entering the store. Despite the fact -// that developers may provide IDs as numbers (e.g., `store.find(Person, 1)`), -// it is important that internally we use strings, since IDs may be serialized -// and lose type information. For example, Ember's router may put a record's -// ID into the URL, and if we later try to deserialize that URL and find the -// corresponding record, we will not know if it is a string or a number. -var coerceId = function(id) { - return id == null ? null : id+''; -}; + Example -/** - The store contains all of the data for records loaded from the server. - It is also responsible for creating instances of DS.Model that wrap - the individual data for a record, so that they can be bound to in your - Handlebars templates. + ```javascript + var allPeople = store.all('person'); + allPeople.mapBy('name'); // ["Tom Dale", "Yehuda Katz", "Trek Glowacki"] - Define your application's store like this: + var people = store.filter('person', function(person) { + if (person.get('name').match(/Katz$/)) { return true; } + }); + people.mapBy('name'); // ["Yehuda Katz"] - MyApp.Store = DS.Store.extend(); + var notKatzFilter = function(person) { + return !person.get('name').match(/Katz$/); + }; + people.set('filterFunction', notKatzFilter); + people.mapBy('name'); // ["Tom Dale", "Trek Glowacki"] + ``` - Most Ember.js applications will only have a single `DS.Store` that is - automatically created by their `Ember.Application`. + @method filterFunction + @param {DS.Model} record + @return {Boolean} `true` if the record should be in the array + */ + filterFunction: null, + isLoaded: true, - You can retrieve models from the store in several ways. To retrieve a record - for a specific id, use `DS.Model`'s `find()` method: + replace: function() { + var type = get(this, 'type').toString(); + throw new Error("The result of a client-side filter (on " + type + ") is immutable."); + }, - var person = App.Person.find(123); + /** + @method updateFilter + @private + */ + updateFilter: Ember.observer(function() { + var manager = get(this, 'manager'); + manager.updateFilter(this, get(this, 'type'), get(this, 'filterFunction')); + }, 'filterFunction') +}); - If your application has multiple `DS.Store` instances (an unusual case), you can - specify which store should be used: +})(); - var person = store.find(App.Person, 123); - In general, you should retrieve models using the methods on `DS.Model`; you should - rarely need to interact with the store directly. - By default, the store will talk to your backend using a standard REST mechanism. - You can customize how the store talks to your backend by specifying a custom adapter: +(function() { +/** + @module ember-data +*/ - MyApp.store = DS.Store.create({ - adapter: 'MyApp.CustomAdapter' - }); +var get = Ember.get, set = Ember.set; - You can learn more about writing a custom adapter by reading the `DS.Adapter` - documentation. +/** + Represents an ordered list of records whose order and membership is + determined by the adapter. For example, a query sent to the adapter + may trigger a search on the server, whose results would be loaded + into an instance of the `AdapterPopulatedRecordArray`. - @class Store + @class AdapterPopulatedRecordArray @namespace DS - @extends Ember.Object - @uses DS._Mappable + @extends DS.RecordArray */ -DS.Store = Ember.Object.extend(DS._Mappable, { - - /** - Many methods can be invoked without specifying which store should be used. - In those cases, the first store created will be used as the default. If - an application has multiple stores, it should specify which store to use - when performing actions, such as finding records by ID. +DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ + query: null, - The init method registers this store as the default if none is specified. + replace: function() { + var type = get(this, 'type').toString(); + throw new Error("The result of a server query (on " + type + ") is immutable."); + }, - @method init + /** + @method load + @private + @param {Array} data */ - init: function() { - if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) { - set(DS, 'defaultStore', this); - } + load: function(data) { + var store = get(this, 'store'), + type = get(this, 'type'), + records = store.pushMany(type, data), + meta = store.metadataFor(type); - // internal bookkeeping; not observable - this.typeMaps = {}; - this.recordArrayManager = DS.RecordArrayManager.create({ - store: this + this.setProperties({ + content: Ember.A(records), + isLoaded: true, + meta: meta }); - this.relationshipChanges = {}; - - set(this, 'currentTransaction', this.transaction()); - set(this, 'defaultTransaction', this.transaction()); - }, - /** - Returns a new transaction scoped to this store. This delegates - responsibility for invoking the adapter's commit mechanism to - a transaction. - - Transaction are responsible for tracking changes to records - added to them, and supporting `commit` and `rollback` - functionality. Committing a transaction invokes the store's - adapter, while rolling back a transaction reverses all - changes made to records added to the transaction. + // TODO: does triggering didLoad event should be the last action of the runLoop? + Ember.run.once(this, 'trigger', 'didLoad'); + } +}); - A store has an implicit (default) transaction, which tracks changes - made to records not explicitly added to a transaction. +})(); - @method transaction - @returns DS.Transaction - */ - transaction: function() { - return DS.Transaction.create({ store: this }); - }, - /** - Instructs the store to materialize the data for a given record. - To materialize a record, the store first retrieves the opaque data that was - passed to either `load()` or `loadMany()`. Then, the data and the record - are passed to the adapter's `materialize()` method, which allows the adapter - to translate arbitrary data structures from the adapter into the normalized - form the record expects. +(function() { +/** + @module ember-data +*/ - The adapter's `materialize()` method will invoke `materializeAttribute()`, - `materializeHasMany()` and `materializeBelongsTo()` on the record to - populate it with normalized values. +var get = Ember.get, set = Ember.set; +var map = Ember.EnumerableUtils.map; - @method materializeData - @private - @param {DS.Model} record - */ - materializeData: function(record) { - var reference = get(record, '_reference'), - data = reference.data, - adapter = this.adapterForType(record.constructor); +/** + A `ManyArray` is a `RecordArray` that represents the contents of a has-many + relationship. - reference.data = MATERIALIZED; + The `ManyArray` is instantiated lazily the first time the relationship is + requested. - record.setupData(); + ### Inverses - if (data !== CREATED) { - // Instructs the adapter to extract information from the - // opaque data and materialize the record's attributes and - // relationships. - adapter.materialize(record, data, reference.prematerialized); - } - }, + Often, the relationships in Ember Data applications will have + an inverse. For example, imagine the following models are + defined: - /** - The adapter to use to communicate to a backend server or other persistence layer. + ```javascript + App.Post = DS.Model.extend({ + comments: DS.hasMany('comment') + }); - This can be specified as an instance, a class, or a property path that specifies - where the adapter can be located. + App.Comment = DS.Model.extend({ + post: DS.belongsTo('post') + }); + ``` - @property adapter - @type {DS.Adapter|String} - */ - adapter: Ember.computed(function(){ - if (!Ember.testing) { - Ember.debug("A custom DS.Adapter was not provided as the 'Adapter' property of your application's Store. The default (DS.RESTAdapter) will be used."); - } + If you created a new instance of `App.Post` and added + a `App.Comment` record to its `comments` has-many + relationship, you would expect the comment's `post` + property to be set to the post that contained + the has-many. - return 'DS.RESTAdapter'; - }).property(), + We call the record to which a relationship belongs the + relationship's _owner_. + @class ManyArray + @namespace DS + @extends DS.RecordArray +*/ +DS.ManyArray = DS.RecordArray.extend({ + init: function() { + this._super.apply(this, arguments); + this._changesToSync = Ember.OrderedSet.create(); + }, /** - Returns a JSON representation of the record using the adapter's - serialization strategy. This method exists primarily to enable - a record, which has access to its store (but not the store's - adapter) to provide a `serialize()` convenience. - - The available options are: - - * `includeId`: `true` if the record's ID should be included in - the JSON representation + The property name of the relationship - @method serialize + @property {String} name @private - @param {DS.Model} record the record to serialize - @param {Object} options an options hash */ - serialize: function(record, options) { - return this.adapterForType(record.constructor).serialize(record, options); - }, + name: null, /** - This property returns the adapter, after resolving a possible - property path. + The record to which this relationship belongs. - If the supplied `adapter` was a class, or a String property - path resolved to a class, this property will instantiate the - class. + @property {DS.Model} owner + @private + */ + owner: null, - This property is cacheable, so the same instance of a specified - adapter class should be used for the lifetime of the store. + /** + `true` if the relationship is polymorphic, `false` otherwise. - @property _adapter + @property {Boolean} isPolymorphic @private - @returns DS.Adapter */ - _adapter: Ember.computed(function() { - var adapter = get(this, 'adapter'); - if (typeof adapter === 'string') { - adapter = get(this, adapter, false) || get(Ember.lookup, adapter); - } + isPolymorphic: false, - if (DS.Adapter.detect(adapter)) { - adapter = adapter.create(); - } + // LOADING STATE - return adapter; - }).property('adapter'), + isLoaded: false, /** - A monotonically increasing number to be used to uniquely identify - data and records. - - It starts at 1 so other parts of the code can test for truthiness - when provided a `clientId` instead of having to explicitly test - for undefined. + Used for async `hasMany` arrays + to keep track of when they will resolve. - @property clientIdCounter + @property {Ember.RSVP.Promise} promise @private */ - clientIdCounter: 1, - - // ..................... - // . CREATE NEW RECORD . - // ..................... + promise: null, /** - Create a new record in the current store. The properties passed - to this method are set on the newly created record. + @method loadingRecordsCount + @param {Number} count + @private + */ + loadingRecordsCount: function(count) { + this.loadingRecordsCount = count; + }, - Note: The third `transaction` property is for internal use only. - If you want to create a record inside of a given transaction, - use `transaction.createRecord()` instead of `store.createRecord()`. + /** + @method loadedRecord + @private + */ + loadedRecord: function() { + this.loadingRecordsCount--; + if (this.loadingRecordsCount === 0) { + set(this, 'isLoaded', true); + this.trigger('didLoad'); + } + }, - @method createRecord - @param {subclass of DS.Model} type - @param {Object} properties a hash of properties to set on the - newly created record. - @returns DS.Model + /** + @method fetch + @private */ - createRecord: function(type, properties, transaction) { - properties = properties || {}; + fetch: function() { + var records = get(this, 'content'), + store = get(this, 'store'), + owner = get(this, 'owner'), + resolver = Ember.RSVP.defer("DS: ManyArray#fetch " + get(this, 'type')); - // Create a new instance of the model `type` and put it - // into the specified `transaction`. If no transaction is - // specified, the default transaction will be used. - var record = type._create({ - store: this - }); + var unloadedRecords = records.filterProperty('isEmpty', true); + store.fetchMany(unloadedRecords, owner, resolver); + }, - transaction = transaction || get(this, 'defaultTransaction'); + // Overrides Ember.Array's replace method to implement + replaceContent: function(index, removed, added) { + // Map the array of record objects into an array of client ids. + added = map(added, function(record) { + Ember.assert("You cannot add '" + record.constructor.typeKey + "' records to this relationship (only '" + this.type.typeKey + "' allowed)", !this.type || record instanceof this.type); + return record; + }, this); - // adoptRecord is an internal API that allows records to move - // into a transaction without assertions designed for app - // code. It is used here to ensure that regardless of new - // restrictions on the use of the public `transaction.add()` - // API, we will always be able to insert new records into - // their transaction. - transaction.adoptRecord(record); + this._super(index, removed, added); + }, - // `id` is a special property that may not be a `DS.attr` - var id = properties.id; + arrangedContentDidChange: function() { + Ember.run.once(this, 'fetch'); + }, - // If the passed properties do not include a primary key, - // give the adapter an opportunity to generate one. Typically, - // client-side ID generators will use something like uuid.js - // to avoid conflicts. + arrayContentWillChange: function(index, removed, added) { + var owner = get(this, 'owner'), + name = get(this, 'name'); + + if (!owner._suspendedRelationships) { + // This code is the first half of code that continues inside + // of arrayContentDidChange. It gets or creates a change from + // the child object, adds the current owner as the old + // parent if this is the first time the object was removed + // from a ManyArray, and sets `newParent` to null. + // + // Later, if the object is added to another ManyArray, + // the `arrayContentDidChange` will set `newParent` on + // the change. + for (var i=index; i "created.uncommitted" - - The `DS.Model` states are themselves stateless. What we mean is that, - though each instance of a record also has a unique instance of a - `DS.StateManager`, the hierarchical states that each of *those* points - to is a shared data structure. For performance reasons, instead of each - record getting its own copy of the hierarchy of states, each state - manager points to this global, immutable shared instance. How does a - state know which record it should be acting on? We pass a reference to - the current state manager as the first parameter to every method invoked - on a state. - - The state manager passed as the first parameter is where you should stash - state about the record if needed; you should never store data on the state - object itself. If you need access to the record being acted on, you can - retrieve the state manager's `record` property. For example, if you had - an event handler `myEvent`: + ```javascript + record.get('currentState.stateName'); + //=> "root.created.uncommitted" + ``` + + The hierarchy of valid states that ship with ember data looks like + this: + + ```text + * root + * deleted + * saved + * uncommitted + * inFlight + * empty + * loaded + * created + * uncommitted + * inFlight + * saved + * updated + * uncommitted + * inFlight + * loading + ``` - myEvent: function(manager) { - var record = manager.get('record'); - record.doSomething(); - } + The `DS.Model` states are themselves stateless. What we mean is + that, the hierarchical states that each of *those* points to is a + shared data structure. For performance reasons, instead of each + record getting its own copy of the hierarchy of states, each record + points to this global, immutable shared instance. How does a state + know which record it should be acting on? We pass the record + instance into the state's event handlers as the first argument. - For more information about state managers in general, see the Ember.js - documentation on `Ember.StateManager`. + The record passed as the first parameter is where you should stash + state about the record if needed; you should never store data on the state + object itself. - ### Events, Flags, and Transitions + ### Events and Flags - A state may implement zero or more events, flags, or transitions. + A state may implement zero or more events and flags. #### Events Events are named functions that are invoked when sent to a record. The - state manager will first look for a method with the given name on the - current state. If no method is found, it will search the current state's - parent, and then its grandparent, and so on until reaching the top of - the hierarchy. If the root is reached without an event handler being found, - an exception will be raised. This can be very helpful when debugging new - features. + record will first look for a method with the given name on the + current state. If no method is found, it will search the current + state's parent, and then its grandparent, and so on until reaching + the top of the hierarchy. If the root is reached without an event + handler being found, an exception will be raised. This can be very + helpful when debugging new features. Here's an example implementation of a state with a `myEvent` event handler: - aState: DS.State.create({ - myEvent: function(manager, param) { - console.log("Received myEvent with "+param); - } - }) + ```javascript + aState: DS.State.create({ + myEvent: function(manager, param) { + console.log("Received myEvent with", param); + } + }) + ``` To trigger this event: - record.send('myEvent', 'foo'); - //=> "Received myEvent with foo" + ```javascript + record.send('myEvent', 'foo'); + //=> "Received myEvent with foo" + ``` Note that an optional parameter can be sent to a record's `send()` method, which will be passed as the second parameter to the event handler. Events should transition to a different state if appropriate. This can be - done by calling the state manager's `transitionTo()` method with a path to the + done by calling the record's `transitionTo()` method with a path to the desired state. The state manager will attempt to resolve the state path relative to the current state. If no state is found at that path, it will attempt to resolve it relative to the current state's parent, and then its @@ -3179,12 +3756,12 @@ var get = Ember.get, set = Ember.set, like this: * created - * start <-- currentState + * uncommitted <-- currentState * inFlight * updated * inFlight - If we are currently in the `start` state, calling + If we are currently in the `uncommitted` state, calling `transitionTo('inFlight')` would transition to the `created.inFlight` state, while calling `transitionTo('updated.inFlight')` would transition to the `updated.inFlight` state. @@ -3199,16 +3776,20 @@ var get = Ember.get, set = Ember.set, state in a more user-friendly way than examining its state path. For example, instead of doing this: - var statePath = record.get('stateManager.currentPath'); - if (statePath === 'created.inFlight') { - doSomething(); - } + ```javascript + var statePath = record.get('stateManager.currentPath'); + if (statePath === 'created.inFlight') { + doSomething(); + } + ``` You can say: - if (record.get('isNew') && record.get('isSaving')) { - doSomething(); - } + ```javascript + if (record.get('isNew') && record.get('isSaving')) { + doSomething(); + } + ``` If your state does not set a value for a given flag, the value will be inherited from its parent (or the first place in the state hierarchy @@ -3218,23 +3799,18 @@ var get = Ember.get, set = Ember.set, in addition to the area below, you will also need to declare it in the `DS.Model` class. - #### Transitions - Transitions are like event handlers but are called automatically upon - entering or exiting a state. To implement a transition, just call a method - either `enter` or `exit`: + * [isEmpty](DS.Model.html#property_isEmpty) + * [isLoading](DS.Model.html#property_isLoading) + * [isLoaded](DS.Model.html#property_isLoaded) + * [isDirty](DS.Model.html#property_isDirty) + * [isSaving](DS.Model.html#property_isSaving) + * [isDeleted](DS.Model.html#property_isDeleted) + * [isNew](DS.Model.html#property_isNew) + * [isValid](DS.Model.html#property_isValid) - myState: DS.State.create({ - // Gets called automatically when entering - // this state. - enter: function(manager) { - console.log("Entered myState"); - } - }) - - Note that enter and exit events are called once per transition. If the - current state changes, but changes to another child state of the parent, - the transition event on the parent will not be triggered. + @namespace DS + @class RootState */ var hasDefinedProperties = function(object) { @@ -3249,23 +3825,16 @@ var hasDefinedProperties = function(object) { return false; }; -var didChangeData = function(record) { - record.materializeData(); -}; - -var willSetProperty = function(record, context) { - context.oldValue = get(record, context.name); - - var change = DS.AttributeChange.createChange(context); - record._changesToSync[context.name] = change; -}; - var didSetProperty = function(record, context) { - var change = record._changesToSync[context.name]; - change.value = get(record, context.name); - change.sync(); -}; + if (context.value === context.originalValue) { + delete record._attributes[context.name]; + record.send('propertyWasReset', context.name); + } else if (context.value !== context.oldValue) { + record.send('becomeDirty'); + } + record.updateRecordArraysLater(); +}; // Implementation notes: // @@ -3278,7 +3847,7 @@ var didSetProperty = function(record, context) { // * isDirty: The record has local changes that have not yet been // saved by the adapter. This includes records that have been // created (but not yet saved) or deleted. -// * isSaving: The record's transaction has been committed, but +// * isSaving: The record has been committed, but // the adapter has not yet acknowledged that the changes have // been persisted to the backend. // * isDeleted: The record was marked for deletion. When `isDeleted` @@ -3322,23 +3891,34 @@ var DirtyState = { // This means that there are local pending changes, but they // have not yet begun to be saved, and are not invalid. uncommitted: { - // EVENTS - willSetProperty: willSetProperty, didSetProperty: didSetProperty, + propertyWasReset: function(record, name) { + var stillDirty = false; + + for (var prop in record._attributes) { + stillDirty = true; + break; + } + + if (!stillDirty) { record.send('rolledBack'); } + }, + + pushedData: Ember.K, + becomeDirty: Ember.K, willCommit: function(record) { record.transitionTo('inFlight'); }, - becameClean: function(record) { - record.withTransaction(function(t) { - t.remove(record); - }); + reloadRecord: function(record, resolve) { + resolve(get(record, 'store').reloadRecord(record)); + }, - record.transitionTo('loaded.materializing'); + rolledBack: function(record) { + record.transitionTo('loaded.saved'); }, becameInvalid: function(record) { @@ -3357,41 +3937,29 @@ var DirtyState = { // FLAGS isSaving: true, - // TRANSITIONS - enter: function(record) { - record.becameInFlight(); - }, - // EVENTS + didSetProperty: didSetProperty, + becomeDirty: Ember.K, + pushedData: Ember.K, - materializingData: function(record) { - set(record, 'lastDirtyType', get(this, 'dirtyType')); - record.transitionTo('materializing'); - }, + // TODO: More robust semantics around save-while-in-flight + willCommit: Ember.K, didCommit: function(record) { var dirtyType = get(this, 'dirtyType'); - record.withTransaction(function(t) { - t.remove(record); - }); - record.transitionTo('saved'); record.send('invokeLifecycleCallbacks', dirtyType); }, - didChangeData: didChangeData, - - becameInvalid: function(record, errors) { - set(record, 'errors', errors); - + becameInvalid: function(record) { record.transitionTo('invalid'); record.send('invokeLifecycleCallbacks'); }, becameError: function(record) { - record.transitionTo('error'); - record.send('invokeLifecycleCallbacks'); + record.transitionTo('uncommitted'); + record.triggerLater('becameError', record); } }, @@ -3402,38 +3970,22 @@ var DirtyState = { // FLAGS isValid: false, - exit: function(record) { - record.withTransaction(function (t) { - t.remove(record); - }); - }, - // EVENTS deleteRecord: function(record) { record.transitionTo('deleted.uncommitted'); record.clearRelationships(); }, - willSetProperty: willSetProperty, - didSetProperty: function(record, context) { - var errors = get(record, 'errors'), - key = context.name; - - set(errors, key, null); - - if (!hasDefinedProperties(errors)) { - record.send('becameValid'); - } + get(record, 'errors').remove(context.name); didSetProperty(record, context); }, becomeDirty: Ember.K, - rollback: function(record) { - record.send('becameValid'); - record.send('rollback'); + rolledBack: function(record) { + get(record, 'errors').clear(); }, becameValid: function(record) { @@ -3441,7 +3993,7 @@ var DirtyState = { }, invokeLifecycleCallbacks: function(record) { - record.trigger('becameInvalid', record); + record.triggerLater('becameInvalid', record); } } }; @@ -3485,6 +4037,10 @@ var createdState = dirtyState({ isNew: true }); +createdState.uncommitted.rolledBack = function(record) { + record.transitionTo('deleted.saved'); +}; + var updatedState = dirtyState({ dirtyType: 'updated' }); @@ -3499,6 +4055,8 @@ createdState.uncommitted.rollback = function(record) { record.transitionTo('deleted.saved'); }; +createdState.uncommitted.propertyWasReset = Ember.K; + updatedState.uncommitted.deleteRecord = function(record) { record.transitionTo('deleted.uncommitted'); record.clearRelationships(); @@ -3509,14 +4067,22 @@ var RootState = { isEmpty: false, isLoading: false, isLoaded: false, - isReloading: false, isDirty: false, isSaving: false, isDeleted: false, - isError: false, isNew: false, isValid: true, + // DEFAULT EVENTS + + // Trying to roll back if you're not in the dirty state + // doesn't change your state. For example, if you're in the + // in-flight state, rolling back the record doesn't move + // you out of the in-flight state. + rolledBack: Ember.K, + + propertyWasReset: Ember.K, + // SUBSTATES // A record begins its lifecycle in the `empty` state. @@ -3528,20 +4094,26 @@ var RootState = { isEmpty: true, // EVENTS - loadingData: function(record) { + loadingData: function(record, promise) { + record._loadingPromise = promise; record.transitionTo('loading'); }, loadedData: function(record) { record.transitionTo('loaded.created.uncommitted'); + + record.suspendRelationshipObservers(function() { + record.notifyPropertyChange('data'); + }); }, pushedData: function(record) { record.transitionTo('loaded.saved'); + record.triggerLater('didLoad'); } }, - // A record enters this state when the store askes + // A record enters this state when the store asks // the adapter for its data. It remains in this state // until the adapter provides the requested data. // @@ -3551,16 +4123,23 @@ var RootState = { // FLAGS isLoading: true, - // EVENTS - loadedData: didChangeData, + exit: function(record) { + record._loadingPromise = null; + }, - materializingData: function(record) { - record.transitionTo('loaded.materializing.firstTime'); + // EVENTS + pushedData: function(record) { + record.transitionTo('loaded.saved'); + record.triggerLater('didLoad'); + set(record, 'isError', false); }, becameError: function(record) { - record.transitionTo('error'); - record.send('invokeLifecycleCallbacks'); + record.triggerLater('becameError', record); + }, + + notFound: function(record) { + record.transitionTo('empty'); } }, @@ -3575,72 +4154,40 @@ var RootState = { // SUBSTATES - materializing: { - // EVENTS - willSetProperty: Ember.K, - didSetProperty: Ember.K, - - didChangeData: didChangeData, - - finishedMaterializing: function(record) { - record.transitionTo('loaded.saved'); - }, - - // SUBSTATES - firstTime: { - // FLAGS - isLoaded: false, + // If there are no local changes to a record, it remains + // in the `saved` state. + saved: { + setup: function(record) { + var attrs = record._attributes, + isDirty = false; - exit: function(record) { - once(function() { - record.trigger('didLoad'); - }); + for (var prop in attrs) { + if (attrs.hasOwnProperty(prop)) { + isDirty = true; + break; + } } - } - }, - - reloading: { - // FLAGS - isReloading: true, - - // TRANSITIONS - enter: function(record) { - var store = get(record, 'store'); - store.reloadRecord(record); - }, - exit: function(record) { - once(record, 'trigger', 'didReload'); + if (isDirty) { + record.adapterDidDirty(); + } }, // EVENTS - loadedData: didChangeData, - - materializingData: function(record) { - record.transitionTo('loaded.materializing'); - } - }, - - // If there are no local changes to a record, it remains - // in the `saved` state. - saved: { - // EVENTS - willSetProperty: willSetProperty, didSetProperty: didSetProperty, - didChangeData: didChangeData, - loadedData: didChangeData, + pushedData: Ember.K, - reloadRecord: function(record) { - record.transitionTo('loaded.reloading'); + becomeDirty: function(record) { + record.transitionTo('updated.uncommitted'); }, - materializingData: function(record) { - record.transitionTo('loaded.materializing'); + willCommit: function(record) { + record.transitionTo('updated.inFlight'); }, - becomeDirty: function(record) { - record.transitionTo('updated.uncommitted'); + reloadRecord: function(record, resolve) { + resolve(get(record, 'store').reloadRecord(record)); }, deleteRecord: function(record) { @@ -3656,22 +4203,13 @@ var RootState = { }, didCommit: function(record) { - record.withTransaction(function(t) { - t.remove(record); - }); - record.send('invokeLifecycleCallbacks', get(record, 'lastDirtyType')); }, - invokeLifecycleCallbacks: function(record, dirtyType) { - if (dirtyType === 'created') { - record.trigger('didCreate', record); - } else { - record.trigger('didUpdate', record); - } + // loaded.saved.notFound would be triggered by a failed + // `reload()` on an unchanged record + notFound: Ember.K - record.trigger('didCommit', record); - } }, // A record is in this state after it has been locally @@ -3697,19 +4235,18 @@ var RootState = { // TRANSITIONS setup: function(record) { - var store = get(record, 'store'); - - store.recordArrayManager.remove(record); + record.updateRecordArrays(); }, // SUBSTATES // When a record is deleted, it enters the `start` - // state. It will exit this state when the record's - // transaction starts to commit. + // state. It will exit this state when the record + // starts to commit. uncommitted: { // EVENTS + willCommit: function(record) { record.transitionTo('inFlight'); }, @@ -3719,16 +4256,14 @@ var RootState = { }, becomeDirty: Ember.K, + deleteRecord: Ember.K, - becameClean: function(record) { - record.withTransaction(function(t) { - t.remove(record); - }); - record.transitionTo('loaded.materializing'); + rolledBack: function(record) { + record.transitionTo('loaded.saved'); } }, - // After a record's transaction is committing, but + // After a record starts committing, but // before the adapter indicates that the deletion // has saved to the server, a record is in the // `inFlight` substate of `deleted`. @@ -3736,20 +4271,19 @@ var RootState = { // FLAGS isSaving: true, - // TRANSITIONS - enter: function(record) { - record.becameInFlight(); - }, - // EVENTS - didCommit: function(record) { - record.withTransaction(function(t) { - t.remove(record); - }); + // TODO: More robust semantics around save-while-in-flight + willCommit: Ember.K, + didCommit: function(record) { record.transitionTo('saved'); record.send('invokeLifecycleCallbacks'); + }, + + becameError: function(record) { + record.transitionTo('uncommitted'); + record.triggerLater('becameError', record); } }, @@ -3766,28 +4300,23 @@ var RootState = { }, invokeLifecycleCallbacks: function(record) { - record.trigger('didDelete', record); - record.trigger('didCommit', record); + record.triggerLater('didDelete', record); + record.triggerLater('didCommit', record); } } }, - // If the adapter indicates that there was an unknown - // error saving a record, the record enters the `error` - // state. - error: { - isError: true, - - // EVENTS - - invokeLifecycleCallbacks: function(record) { - record.trigger('becameError', record); + invokeLifecycleCallbacks: function(record, dirtyType) { + if (dirtyType === 'created') { + record.triggerLater('didCreate', record); + } else { + record.triggerLater('didUpdate', record); } + + record.triggerLater('didCommit', record); } }; -var hasOwnProp = {}.hasOwnProperty; - function wireState(object, parent, name) { /*jshint proto:true*/ // TODO: Use Object.create and copy instead @@ -3814,1420 +4343,1537 @@ DS.RootState = RootState; (function() { +var get = Ember.get, isEmpty = Ember.isEmpty; + /** - @module ember-data +@module ember-data */ -var LoadPromise = DS.LoadPromise; // system/mixins/load_promise - -var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map, merge = Ember.merge; - -var arrayMap = Ember.ArrayPolyfills.map; - -var retrieveFromCurrentState = Ember.computed(function(key, value) { - return get(get(this, 'currentState'), key); -}).property('currentState').readOnly(); - /** + Holds validation errors for a given record organized by attribute names. - The model class that all Ember Data records descend from. - - @class Model + @class Errors @namespace DS @extends Ember.Object + @uses Ember.Enumerable @uses Ember.Evented - @uses DS.LoadPromise -*/ -DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { - isEmpty: retrieveFromCurrentState, - isLoading: retrieveFromCurrentState, - isLoaded: retrieveFromCurrentState, - isReloading: retrieveFromCurrentState, - isDirty: retrieveFromCurrentState, - isSaving: retrieveFromCurrentState, - isDeleted: retrieveFromCurrentState, - isError: retrieveFromCurrentState, - isNew: retrieveFromCurrentState, - isValid: retrieveFromCurrentState, - dirtyType: retrieveFromCurrentState, - - clientId: null, - id: null, - transaction: null, - currentState: null, - errors: null, - + */ +DS.Errors = Ember.Object.extend(Ember.Enumerable, Ember.Evented, { /** - Create a JSON representation of the record, using the serialization - strategy of the store's adapter. - - @method serialize - @param {Object} options Available options: + Register with target handler - * `includeId`: `true` if the record's ID should be included in the - JSON representation. - - @returns {Object} an object whose values are primitive JSON values only + @method registerHandlers + @param {Object} target + @param {Function} becameInvalid + @param {Function} becameValid */ - serialize: function(options) { - var store = get(this, 'store'); - return store.serialize(this, options); + registerHandlers: function(target, becameInvalid, becameValid) { + this.on('becameInvalid', target, becameInvalid); + this.on('becameValid', target, becameValid); }, /** - Use {{#crossLink "DS.JSONSerializer"}}DS.JSONSerializer{{/crossLink}} to - get the JSON representation of a record. + @property errorsByAttributeName + @type {Ember.MapWithDefault} + @private + */ + errorsByAttributeName: Ember.reduceComputed("content", { + initialValue: function() { + return Ember.MapWithDefault.create({ + defaultValue: function() { + return Ember.A(); + } + }); + }, - @method toJSON - @param {Object} options Available options: + addedItem: function(errors, error) { + errors.get(error.attribute).pushObject(error); - * `includeId`: `true` if the record's ID should be included in the - JSON representation. + return errors; + }, - @returns {Object} A JSON representation of the object. - */ - toJSON: function(options) { - var serializer = DS.JSONSerializer.create(); - return serializer.serialize(this, options); - }, + removedItem: function(errors, error) { + errors.get(error.attribute).removeObject(error); + + return errors; + } + }), /** - Fired when the record is loaded from the server. + Returns errors for a given attribute - @event didLoad + @method errorsFor + @param {String} attribute + @returns {Array} */ - didLoad: Ember.K, + errorsFor: function(attribute) { + return get(this, 'errorsByAttributeName').get(attribute); + }, /** - Fired when the record is reloaded from the server. - - @event didReload */ - didReload: Ember.K, + messages: Ember.computed.mapBy('content', 'message'), /** - Fired when the record is updated. - - @event didUpdate + @property content + @type {Array} + @private */ - didUpdate: Ember.K, + content: Ember.computed(function() { + return Ember.A(); + }), /** - Fired when the record is created. + @method unknownProperty + @private + */ + unknownProperty: function(attribute) { + var errors = this.errorsFor(attribute); + if (isEmpty(errors)) { return null; } + return errors; + }, - @event didCreate + /** + @method nextObject + @private */ - didCreate: Ember.K, + nextObject: function(index, previousObject, context) { + return get(this, 'content').objectAt(index); + }, /** - Fired when the record is deleted. + Total number of errors. - @event didDelete + @property length + @type {Number} + @readOnly */ - didDelete: Ember.K, + length: Ember.computed.oneWay('content.length').readOnly(), /** - Fired when the record becomes invalid. - - @event becameInvalid + @property isEmpty + @type {Boolean} + @readOnly */ - becameInvalid: Ember.K, + isEmpty: Ember.computed.not('length').readOnly(), /** - Fired when the record enters the error state. + Adds error messages to a given attribute and sends + `becameInvalid` event to the record. - @event becameError + @method add + @param {String} attribute + @param {Array|String} messages */ - becameError: Ember.K, + add: function(attribute, messages) { + var wasEmpty = get(this, 'isEmpty'); - data: Ember.computed(function() { - if (!this._data) { - this.setupData(); - } + messages = this._findOrCreateMessages(attribute, messages); + get(this, 'content').addObjects(messages); - return this._data; - }).property(), + this.notifyPropertyChange(attribute); + this.enumerableContentDidChange(); - materializeData: function() { - this.send('materializingData'); + if (wasEmpty && !get(this, 'isEmpty')) { + this.trigger('becameInvalid'); + } + }, - get(this, 'store').materializeData(this); + /** + @method _findOrCreateMessages + @private + */ + _findOrCreateMessages: function(attribute, messages) { + var errors = this.errorsFor(attribute); - this.suspendRelationshipObservers(function() { - this.notifyPropertyChange('data'); + return Ember.makeArray(messages).map(function(message) { + return errors.findBy('message', message) || { + attribute: attribute, + message: message + }; }); }, - _data: null, + /** + Removes all error messages from the given attribute and sends + `becameValid` event to the record if there no more errors left. - init: function() { - set(this, 'currentState', DS.RootState.empty); - this._super(); - this._setup(); - }, + @method remove + @param {String} attribute + */ + remove: function(attribute) { + if (get(this, 'isEmpty')) { return; } - _setup: function() { - this._changesToSync = {}; - }, + var content = get(this, 'content').rejectBy('attribute', attribute); + get(this, 'content').setObjects(content); - send: function(name, context) { - var currentState = get(this, 'currentState'); + this.notifyPropertyChange(attribute); + this.enumerableContentDidChange(); - if (!currentState[name]) { - this._unhandledEvent(currentState, name, context); + if (get(this, 'isEmpty')) { + this.trigger('becameValid'); } - - return currentState[name](this, context); }, - transitionTo: function(name) { - // POSSIBLE TODO: Remove this code and replace with - // always having direct references to state objects - - var pivotName = name.split(".", 1), - currentState = get(this, 'currentState'), - state = currentState; - - do { - if (state.exit) { state.exit(this); } - state = state.parentState; - } while (!state.hasOwnProperty(pivotName)); + /** + Removes all error messages and sends `becameValid` event + to the record. - var path = name.split("."); + @method clear + */ + clear: function() { + if (get(this, 'isEmpty')) { return; } - var setups = [], enters = [], i, l; + get(this, 'content').clear(); + this.enumerableContentDidChange(); - for (i=0, l=path.length; i tuple for a polymorphic association. - return store.referenceForId(id.type, id.id); - } - } - return store.referenceForId(type, id); - }); + @property isSaving + @type {Boolean} + @readOnly + */ + isSaving: retrieveFromCurrentState, + /** + If this property is `true` the record is in the `deleted` state + and has been marked for deletion. When `isDeleted` is true and + `isDirty` is true, the record is deleted locally but the deletion + was not yet persisted. When `isSaving` is true, the change is + in-flight. When both `isDirty` and `isSaving` are false, the + change has persisted. - set(cachedValue, 'content', Ember.A(references)); - } - }, + Example - updateRecordArraysLater: function() { - Ember.run.once(this, this.updateRecordArrays); - }, + ```javascript + var record = store.createRecord(App.Model); + record.get('isDeleted'); // false + record.deleteRecord(); + record.get('isDeleted'); // true + ``` - setupData: function(data) { - this._data = data || { id: null }; + @property isDeleted + @type {Boolean} + @readOnly + */ + isDeleted: retrieveFromCurrentState, + /** + If this property is `true` the record is in the `new` state. A + record will be in the `new` state when it has been created on the + client and the adapter has not yet report that it was successfully + saved. - if (data) { this.pushedData(); } - }, + Example - materializeId: function(id) { - set(this, 'id', id); - }, + ```javascript + var record = store.createRecord(App.Model); + record.get('isNew'); // true - materializeAttributes: function(attributes) { - Ember.assert("Must pass a hash of attributes to materializeAttributes", !!attributes); - merge(this._data, attributes); - }, + record.save().then(function(model) { + model.get('isNew'); // false + }); + ``` - materializeAttribute: function(name, value) { - this._data[name] = value; - }, + @property isNew + @type {Boolean} + @readOnly + */ + isNew: retrieveFromCurrentState, + /** + If this property is `true` the record is in the `valid` state. A + record will be in the `valid` state when no client-side + validations have failed and the adapter did not report any + server-side validation failures. - materializeHasMany: function(name, tuplesOrReferencesOrOpaque) { - var tuplesOrReferencesOrOpaqueType = typeof tuplesOrReferencesOrOpaque; + @property isValid + @type {Boolean} + @readOnly + */ + isValid: retrieveFromCurrentState, + /** + If the record is in the dirty state this property will report what + kind of change has caused it to move into the dirty + state. Possible values are: - if (tuplesOrReferencesOrOpaque && tuplesOrReferencesOrOpaqueType !== 'string' && tuplesOrReferencesOrOpaque.length > 1) { - Ember.assert('materializeHasMany expects tuples, references or opaque token, not ' + tuplesOrReferencesOrOpaque[0], tuplesOrReferencesOrOpaque[0].hasOwnProperty('id') && tuplesOrReferencesOrOpaque[0].type); - } + - `created` The record has been created by the client and not yet saved to the adapter. + - `updated` The record has been updated by the client and not yet saved to the adapter. + - `deleted` The record has been deleted by the client and not yet saved to the adapter. - if( tuplesOrReferencesOrOpaqueType === "string" ) { - this._data[name] = tuplesOrReferencesOrOpaque; - } else { - var references = tuplesOrReferencesOrOpaque; + Example - if (tuplesOrReferencesOrOpaque && Ember.isArray(tuplesOrReferencesOrOpaque)) { - references = this._convertTuplesToReferences(tuplesOrReferencesOrOpaque); - } + ```javascript + var record = store.createRecord(App.Model); + record.get('dirtyType'); // 'created' + ``` - this._data[name] = references; - } - }, + @property dirtyType + @type {String} + @readOnly + */ + dirtyType: retrieveFromCurrentState, - materializeBelongsTo: function(name, tupleOrReference) { - if (tupleOrReference) { Ember.assert('materializeBelongsTo expects a tuple or a reference, not a ' + tupleOrReference, !tupleOrReference || (tupleOrReference.hasOwnProperty('id') && tupleOrReference.hasOwnProperty('type'))); } + /** + If `true` the adapter reported that it was unable to save local + changes to the backend. This may also result in the record having + its `isValid` property become false if the adapter reported that + server-side validations failed. - this._data[name] = tupleOrReference; - }, + Example - _convertTuplesToReferences: function(tuplesOrReferences) { - return map(tuplesOrReferences, function(tupleOrReference) { - return this._convertTupleToReference(tupleOrReference); - }, this); - }, + ```javascript + record.get('isError'); // false + record.set('foo', 'invalid value'); + record.save().then(null, function() { + record.get('isError'); // true + }); + ``` - _convertTupleToReference: function(tupleOrReference) { - var store = get(this, 'store'); - if(tupleOrReference.clientId) { - return tupleOrReference; - } else { - return store.referenceForId(tupleOrReference.type, tupleOrReference.id); - } - }, + @property isError + @type {Boolean} + @readOnly + */ + isError: false, + /** + If `true` the store is attempting to reload the record form the adapter. - rollback: function() { - this._setup(); - this.send('becameClean'); + Example - this.suspendRelationshipObservers(function() { - this.notifyPropertyChange('data'); - }); - }, + ```javascript + record.get('isReloading'); // false + record.reload(); + record.get('isReloading'); // true + ``` - toStringExtension: function() { - return get(this, 'id'); - }, + @property isReloading + @type {Boolean} + @readOnly + */ + isReloading: false, /** - The goal of this method is to temporarily disable specific observers - that take action in response to application changes. - - This allows the system to make changes (such as materialization and - rollback) that should not trigger secondary behavior (such as setting an - inverse relationship or marking records as dirty). - - The specific implementation will likely change as Ember proper provides - better infrastructure for suspending groups of observers, and if Array - observation becomes more unified with regular observers. + The `clientId` property is a transient numerical identifier + generated at runtime by the data store. It is important + primarily because newly created objects may not yet have an + externally generated id. - @method suspendRelationshipObservers + @property clientId @private - @param callback - @param binding + @type {Number|String} */ - suspendRelationshipObservers: function(callback, binding) { - var observers = get(this.constructor, 'relationshipNames').belongsTo; - var self = this; + clientId: null, + /** + All ember models have an id property. This is an identifier + managed by an external source. These are always coerced to be + strings before being used internally. Note when declaring the + attributes for a model it is an error to declare an id + attribute. - try { - this._suspendedRelationships = true; - Ember._suspendObservers(self, observers, null, 'belongsToDidChange', function() { - Ember._suspendBeforeObservers(self, observers, null, 'belongsToWillChange', function() { - callback.call(binding || self); - }); - }); - } finally { - this._suspendedRelationships = false; - } - }, + ```javascript + var record = store.createRecord(App.Model); + record.get('id'); // null - becameInFlight: function() { - }, + store.find('model', 1).then(function(model) { + model.get('id'); // '1' + }); + ``` + @property id + @type {String} + */ + id: null, + transaction: null, /** - @method resolveOn + @property currentState @private - @param successEvent + @type {Object} */ - resolveOn: function(successEvent) { - var model = this; - - return new Ember.RSVP.Promise(function(resolve, reject) { - function success() { - this.off('becameError', error); - this.off('becameInvalid', error); - resolve(this); - } - function error() { - this.off(successEvent, success); - reject(this); - } + currentState: null, + /** + When the record is in the `invalid` state this object will contain + any errors returned by the adapter. When present the errors hash + typically contains keys corresponding to the invalid property names + and values which are an array of error messages. - model.one(successEvent, success); - model.one('becameError', error); - model.one('becameInvalid', error); + ```javascript + record.get('errors.length'); // 0 + record.set('foo', 'invalid value'); + record.save().then(null, function() { + record.get('errors').get('foo'); // ['foo should be a number.'] }); - }, - - /** - Save the record. + ``` - @method save + @property errors + @type {Object} */ - save: function() { - this.get('store').scheduleSave(this); - - return this.resolveOn('didCommit'); - }, + errors: null, /** - Reload the record from the adapter. + Create a JSON representation of the record, using the serialization + strategy of the store's adapter. - This will only work if the record has already finished loading - and has not yet been modified (`isLoaded` but not `isDirty`, - or `isSaving`). + `serialize` takes an optional hash as a parameter, currently + supported options are: - @method reload - */ - reload: function() { - this.send('reloadRecord'); + - `includeId`: `true` if the record's ID should be included in the + JSON representation. - return this.resolveOn('didReload'); + @method serialize + @param {Object} options + @returns {Object} an object whose values are primitive JSON values only + */ + serialize: function(options) { + var store = get(this, 'store'); + return store.serialize(this, options); }, - // FOR USE DURING COMMIT PROCESS - - adapterDidUpdateAttribute: function(attributeName, value) { - - // If a value is passed in, update the internal attributes and clear - // the attribute cache so it picks up the new value. Otherwise, - // collapse the current value into the internal attributes because - // the adapter has acknowledged it. - if (value !== undefined) { - get(this, 'data')[attributeName] = value; - this.notifyPropertyChange(attributeName); - } else { - value = get(this, attributeName); - get(this, 'data')[attributeName] = value; - } + /** + Use [DS.JSONSerializer](DS.JSONSerializer.html) to + get the JSON representation of a record. - this.updateRecordArraysLater(); - }, + `toJSON` takes an optional hash as a parameter, currently + supported options are: - adapterDidInvalidate: function(errors) { - this.send('becameInvalid', errors); - }, + - `includeId`: `true` if the record's ID should be included in the + JSON representation. - adapterDidError: function() { - this.send('becameError'); + @method toJSON + @param {Object} options + @returns {Object} A JSON representation of the object. + */ + toJSON: function(options) { + // container is for lazy transform lookups + var serializer = DS.JSONSerializer.create({ container: this.container }); + return serializer.serialize(this, options); }, /** - Override the default event firing from Ember.Evented to - also call methods with the given name. - - @method trigger - @private - @param name - */ - trigger: function(name) { - Ember.tryInvoke(this, name, [].slice.call(arguments, 1)); - this._super.apply(this, arguments); - } -}); - -// Helper function to generate store aliases. -// This returns a function that invokes the named alias -// on the default store, but injects the class as the -// first parameter. -var storeAlias = function(methodName) { - return function() { - var store = get(DS, 'defaultStore'), - args = [].slice.call(arguments); - - args.unshift(this); - Ember.assert("Your application does not have a 'Store' property defined. Attempts to call '" + methodName + "' on model classes will fail. Please provide one as with 'YourAppName.Store = DS.Store.extend()'", !!store); - return store[methodName].apply(store, args); - }; -}; - -DS.Model.reopenClass({ - - /** - Alias DS.Model's `create` method to `_create`. This allows us to create DS.Model - instances from within the store, but if end users accidentally call `create()` - (instead of `createRecord()`), we can raise an error. + Fired when the record is loaded from the server. - @method _create - @private - @static + @event didLoad */ - _create: DS.Model.create, + didLoad: Ember.K, /** - Override the class' `create()` method to raise an error. This prevents end users - from inadvertently calling `create()` instead of `createRecord()`. The store is - still able to create instances by calling the `_create()` method. + Fired when the record is updated. - @method create - @private - @static + @event didUpdate */ - create: function() { - throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set."); - }, + didUpdate: Ember.K, /** - See `DS.Store.find()`. + Fired when the record is created. - @method find - @param {Object|String|Array|null} query A query to find records by. + @event didCreate */ - find: storeAlias('find'), + didCreate: Ember.K, /** - See `DS.Store.all()`. + Fired when the record is deleted. - @method all - @return {DS.RecordArray} + @event didDelete */ - all: storeAlias('all'), + didDelete: Ember.K, /** - See `DS.Store.findQuery()`. + Fired when the record becomes invalid. - @method query - @param {Object} query an opaque query to be used by the adapter - @return {DS.AdapterPopulatedRecordArray} + @event becameInvalid */ - query: storeAlias('findQuery'), + becameInvalid: Ember.K, /** - See `DS.Store.filter()`. + Fired when the record enters the error state. - @method filter - @param {Function} filter - @return {DS.FilteredRecordArray} + @event becameError */ - filter: storeAlias('filter'), + becameError: Ember.K, /** - See `DS.Store.createRecord()`. - - @method createRecord - @param {Object} properties a hash of properties to set on the - newly created record. - @return DS.Model + @property data + @private + @type {Object} */ - createRecord: storeAlias('createRecord') -}); + data: Ember.computed(function() { + this._data = this._data || {}; + return this._data; + }).property(), -})(); + _data: null, + init: function() { + set(this, 'currentState', DS.RootState.empty); + var errors = DS.Errors.create(); + errors.registerHandlers(this, function() { + this.send('becameInvalid'); + }, function() { + this.send('becameValid'); + }); + set(this, 'errors', errors); + this._super(); + this._setup(); + }, + _setup: function() { + this._changesToSync = {}; + this._deferredTriggers = []; + this._data = {}; + this._attributes = {}; + this._inFlightAttributes = {}; + this._relationships = {}; + }, -(function() { -/** - @module ember-data -*/ + /** + @method send + @private + @param {String} name + @param {Object} context + */ + send: function(name, context) { + var currentState = get(this, 'currentState'); -var get = Ember.get; + if (!currentState[name]) { + this._unhandledEvent(currentState, name, context); + } -/** - @class Model - @namespace DS -*/ -DS.Model.reopenClass({ - attributes: Ember.computed(function() { - var map = Ember.Map.create(); + return currentState[name](this, context); + }, - this.eachComputedProperty(function(name, meta) { - if (meta.isAttribute) { - Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.toString(), name !== 'id'); + /** + @method transitionTo + @private + @param {String} name + */ + transitionTo: function(name) { + // POSSIBLE TODO: Remove this code and replace with + // always having direct references to state objects - meta.name = name; - map.set(name, meta); - } - }); + var pivotName = name.split(".", 1), + currentState = get(this, 'currentState'), + state = currentState; - return map; - }) -}); + do { + if (state.exit) { state.exit(this); } + state = state.parentState; + } while (!state.hasOwnProperty(pivotName)); + var path = name.split("."); -DS.Model.reopen({ - eachAttribute: function(callback, binding) { - get(this.constructor, 'attributes').forEach(function(name, meta) { - callback.call(binding, name, meta); - }, binding); - }, + var setups = [], enters = [], i, l; - attributeWillChange: Ember.beforeObserver(function(record, key) { - var reference = get(record, '_reference'), - store = get(record, 'store'); + for (i=0, l=path.length; i 1) { - Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.constructor.toString(), key !== 'id'); - } else { - value = getAttr(this, options, key); + if (context !== undefined) { + errorMessage += "Called with " + Ember.inspect(context) + "."; } - return value; - // `data` is never set directly. However, it may be - // invalidated from the state manager's setData - // event. - }).property('data').meta(meta); -}; - + throw new Ember.Error(errorMessage); + }, -})(); + withTransaction: function(fn) { + var transaction = get(this, 'transaction'); + if (transaction) { fn(transaction); } + }, + /** + @method loadingData + @private + @param {Promise} promise + */ + loadingData: function(promise) { + this.send('loadingData', promise); + }, + /** + @method loadedData + @private + */ + loadedData: function() { + this.send('loadedData'); + }, -(function() { -/** - @module ember-data -*/ + /** + @method notFound + @private + */ + notFound: function() { + this.send('notFound'); + }, -})(); + /** + @method pushedData + @private + */ + pushedData: function() { + this.send('pushedData'); + }, + /** + Marks the record as deleted but does not save it. You must call + `save` afterwards if you want to persist it. You might use this + method if you want to allow the user to still `rollback()` a + delete after it was made. + Example -(function() { -/** - @module ember-data -*/ + ```javascript + App.ModelDeleteRoute = Ember.Route.extend({ + actions: { + softDelete: function() { + this.get('model').deleteRecord(); + }, + confirm: function() { + this.get('model').save(); + }, + undo: function() { + this.get('model').rollback(); + } + } + }); + ``` -/** - An AttributeChange object is created whenever a record's - attribute changes value. It is used to track changes to a - record between transaction commits. + @method deleteRecord + */ + deleteRecord: function() { + this.send('deleteRecord'); + }, - @class AttributeChange - @namespace DS - @private - @constructor -*/ -var AttributeChange = DS.AttributeChange = function(options) { - this.reference = options.reference; - this.store = options.store; - this.name = options.name; - this.oldValue = options.oldValue; -}; + /** + Same as `deleteRecord`, but saves the record immediately. -AttributeChange.createChange = function(options) { - return new AttributeChange(options); -}; + Example -AttributeChange.prototype = { - sync: function() { - this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue); + ```javascript + App.ModelDeleteRoute = Ember.Route.extend({ + actions: { + delete: function() { + var controller = this.controller; + this.get('model').destroyRecord().then(function() { + controller.transitionToRoute('model.index'); + }); + } + } + }); + ``` - // TODO: Use this object in the commit process - this.destroy(); + @method destroyRecord + @return {Promise} a promise that will be resolved when the adapter returns + successfully or rejected if the adapter returns with an error. + */ + destroyRecord: function() { + this.deleteRecord(); + return this.save(); }, /** - If the AttributeChange is destroyed (either by being rolled back - or being committed), remove it from the list of pending changes - on the record. - - @method destroy + @method unloadRecord + @private */ - destroy: function() { - var record = this.reference.record; + unloadRecord: function() { + Ember.assert("You can only unload a loaded, non-dirty record.", !get(this, 'isDirty')); - delete record._changesToSync[this.name]; - } -}; + this.send('unloadRecord'); + }, -})(); + /** + @method clearRelationships + @private + */ + clearRelationships: function() { + this.eachRelationship(function(name, relationship) { + if (relationship.kind === 'belongsTo') { + set(this, name, null); + } else if (relationship.kind === 'hasMany') { + var hasMany = this._relationships[relationship.name]; + if (hasMany) { hasMany.clear(); } + } + }, this); + }, + /** + @method updateRecordArrays + @private + */ + updateRecordArrays: function() { + this._updatingRecordArraysLater = false; + get(this, 'store').dataWasUpdated(this.constructor, this); + }, + /** + Returns an object, whose keys are changed properties, and value is + an [oldProp, newProp] array. -(function() { -/** - @module ember-data -*/ + Example -var get = Ember.get, set = Ember.set; -var forEach = Ember.EnumerableUtils.forEach; + ```javascript + App.Mascot = DS.Model.extend({ + name: attr('string') + }); -/** - @class RelationshipChange - @namespace DS - @private - @construtor -*/ -DS.RelationshipChange = function(options) { - this.parentReference = options.parentReference; - this.childReference = options.childReference; - this.firstRecordReference = options.firstRecordReference; - this.firstRecordKind = options.firstRecordKind; - this.firstRecordName = options.firstRecordName; - this.secondRecordReference = options.secondRecordReference; - this.secondRecordKind = options.secondRecordKind; - this.secondRecordName = options.secondRecordName; - this.changeType = options.changeType; - this.store = options.store; + var person = store.createRecord('person'); + person.changedAttributes(); // {} + person.set('name', 'Tomster'); + person.changedAttributes(); // {name: [undefined, 'Tomster']} + ``` - this.committed = {}; -}; + @method changedAttributes + @return {Object} an object, whose keys are changed properties, + and value is an [oldProp, newProp] array. + */ + changedAttributes: function() { + var oldData = get(this, '_data'), + newData = get(this, '_attributes'), + diffData = {}, + prop; -/** - @class RelationshipChangeAdd - @namespace DS - @private - @construtor -*/ -DS.RelationshipChangeAdd = function(options){ - DS.RelationshipChange.call(this, options); -}; + for (prop in newData) { + diffData[prop] = [oldData[prop], newData[prop]]; + } -/** - @class RelationshipChangeRemove - @namespace DS - @private - @construtor -*/ -DS.RelationshipChangeRemove = function(options){ - DS.RelationshipChange.call(this, options); -}; + return diffData; + }, -DS.RelationshipChange.create = function(options) { - return new DS.RelationshipChange(options); -}; + /** + @method adapterWillCommit + @private + */ + adapterWillCommit: function() { + this.send('willCommit'); + }, -DS.RelationshipChangeAdd.create = function(options) { - return new DS.RelationshipChangeAdd(options); -}; + /** + If the adapter did not return a hash in response to a commit, + merge the changed attributes and relationships into the existing + saved data. -DS.RelationshipChangeRemove.create = function(options) { - return new DS.RelationshipChangeRemove(options); -}; + @method adapterDidCommit + */ + adapterDidCommit: function(data) { + set(this, 'isError', false); -DS.OneToManyChange = {}; -DS.OneToNoneChange = {}; -DS.ManyToNoneChange = {}; -DS.OneToOneChange = {}; -DS.ManyToManyChange = {}; + if (data) { + this._data = data; + } else { + Ember.mixin(this._data, this._inFlightAttributes); + } -DS.RelationshipChange._createChange = function(options){ - if(options.changeType === "add"){ - return DS.RelationshipChangeAdd.create(options); - } - if(options.changeType === "remove"){ - return DS.RelationshipChangeRemove.create(options); - } -}; + this._inFlightAttributes = {}; + this.send('didCommit'); + this.updateRecordArraysLater(); -DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide){ - var knownKey = knownSide.key, key, otherKind; - var knownKind = knownSide.kind; + if (!data) { return; } - var inverse = recordType.inverseFor(knownKey); + this.suspendRelationshipObservers(function() { + this.notifyPropertyChange('data'); + }); + }, - if (inverse){ - key = inverse.name; - otherKind = inverse.kind; - } + /** + @method adapterDidDirty + @private + */ + adapterDidDirty: function() { + this.send('becomeDirty'); + this.updateRecordArraysLater(); + }, - if (!inverse){ - return knownKind === "belongsTo" ? "oneToNone" : "manyToNone"; - } - else{ - if(otherKind === "belongsTo"){ - return knownKind === "belongsTo" ? "oneToOne" : "manyToOne"; + dataDidChange: Ember.observer(function() { + this.reloadHasManys(); + }, 'data'), + + reloadHasManys: function() { + var relationships = get(this.constructor, 'relationshipsByName'); + this.updateRecordArraysLater(); + relationships.forEach(function(name, relationship) { + if (this._data.links && this._data.links[name]) { return; } + if (relationship.kind === 'hasMany') { + this.hasManyDidChange(relationship.key); + } + }, this); + }, + + hasManyDidChange: function(key) { + var hasMany = this._relationships[key]; + + if (hasMany) { + var records = this._data[key] || []; + + set(hasMany, 'content', Ember.A(records)); + set(hasMany, 'isLoaded', true); + hasMany.trigger('didLoad'); } - else{ - return knownKind === "belongsTo" ? "oneToMany" : "manyToMany"; + }, + + /** + @method updateRecordArraysLater + @private + */ + updateRecordArraysLater: function() { + // quick hack (something like this could be pushed into run.once + if (this._updatingRecordArraysLater) { return; } + this._updatingRecordArraysLater = true; + + Ember.run.schedule('actions', this, this.updateRecordArrays); + }, + + /** + @method setupData + @private + @param {Object} data + @param {Boolean} partial the data should be merged into + the existing data, not replace it. + */ + setupData: function(data, partial) { + if (partial) { + Ember.merge(this._data, data); + } else { + this._data = data; } - } -}; + var relationships = this._relationships; -DS.RelationshipChange.createChange = function(firstRecordReference, secondRecordReference, store, options){ - // Get the type of the child based on the child's client ID - var firstRecordType = firstRecordReference.type, changeType; - changeType = DS.RelationshipChange.determineRelationshipType(firstRecordType, options); - if (changeType === "oneToMany"){ - return DS.OneToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToOne"){ - return DS.OneToManyChange.createChange(secondRecordReference, firstRecordReference, store, options); - } - else if (changeType === "oneToNone"){ - return DS.OneToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToNone"){ - return DS.ManyToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "oneToOne"){ - return DS.OneToOneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToMany"){ - return DS.ManyToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); - } -}; + this.eachRelationship(function(name, rel) { + if (data.links && data.links[name]) { return; } + if (rel.options.async) { relationships[name] = null; } + }); -DS.OneToNoneChange.createChange = function(childReference, parentReference, store, options) { - var key = options.key; - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - store: store, - changeType: options.changeType, - firstRecordName: key, - firstRecordKind: "belongsTo" - }); + if (data) { this.pushedData(); } - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + this.suspendRelationshipObservers(function() { + this.notifyPropertyChange('data'); + }); + }, - return change; -}; + materializeId: function(id) { + set(this, 'id', id); + }, -DS.ManyToNoneChange.createChange = function(childReference, parentReference, store, options) { - var key = options.key; - var change = DS.RelationshipChange._createChange({ - parentReference: childReference, - childReference: parentReference, - secondRecordReference: childReference, - store: store, - changeType: options.changeType, - secondRecordName: options.key, - secondRecordKind: "hasMany" - }); + materializeAttributes: function(attributes) { + Ember.assert("Must pass a hash of attributes to materializeAttributes", !!attributes); + merge(this._data, attributes); + }, - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - return change; -}; + materializeAttribute: function(name, value) { + this._data[name] = value; + }, + /** + @method updateHasMany + @private + @param {String} name + @param {Array} records + */ + updateHasMany: function(name, records) { + this._data[name] = records; + this.hasManyDidChange(name); + }, -DS.ManyToManyChange.createChange = function(childReference, parentReference, store, options) { - // If the name of the belongsTo side of the relationship is specified, - // use that - // If the type of the parent is specified, look it up on the child's type - // definition. - var key = options.key; + /** + @method updateBelongsTo + @private + @param {String} name + @param {DS.Model} record + */ + updateBelongsTo: function(name, record) { + this._data[name] = record; + }, - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "hasMany", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); + /** + If the model `isDirty` this function will discard any unsaved + changes - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + Example + ```javascript + record.get('name'); // 'Untitled Document' + record.set('name', 'Doc 1'); + record.get('name'); // 'Doc 1' + record.rollback(); + record.get('name'); // 'Untitled Document' + ``` - return change; -}; + @method rollback + */ + rollback: function() { + this._attributes = {}; -DS.OneToOneChange.createChange = function(childReference, parentReference, store, options) { - var key; + if (get(this, 'isError')) { + this._inFlightAttributes = {}; + set(this, 'isError', false); + } - // If the name of the belongsTo side of the relationship is specified, - // use that - // If the type of the parent is specified, look it up on the child's type - // definition. - if (options.parentType) { - key = options.parentType.inverseFor(options.key).name; - } else if (options.key) { - key = options.key; - } else { - Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); - } + if (!get(this, 'isValid')) { + this._inFlightAttributes = {}; + } - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "belongsTo", - store: store, - changeType: options.changeType, - firstRecordName: key - }); + this.send('rolledBack'); - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + this.suspendRelationshipObservers(function() { + this.notifyPropertyChange('data'); + }); + }, + toStringExtension: function() { + return get(this, 'id'); + }, - return change; -}; + /** + The goal of this method is to temporarily disable specific observers + that take action in response to application changes. -DS.OneToOneChange.maintainInvariant = function(options, store, childReference, key){ - if (options.changeType === "add" && store.recordIsMaterialized(childReference)) { - var child = store.recordForReference(childReference); - var oldParent = get(child, key); - if (oldParent){ - var correspondingChange = DS.OneToOneChange.createChange(childReference, oldParent.get('_reference'), store, { - parentType: options.parentType, - hasManyName: options.hasManyName, - changeType: "remove", - key: options.key + This allows the system to make changes (such as materialization and + rollback) that should not trigger secondary behavior (such as setting an + inverse relationship or marking records as dirty). + + The specific implementation will likely change as Ember proper provides + better infrastructure for suspending groups of observers, and if Array + observation becomes more unified with regular observers. + + @method suspendRelationshipObservers + @private + @param callback + @param binding + */ + suspendRelationshipObservers: function(callback, binding) { + var observers = get(this.constructor, 'relationshipNames').belongsTo; + var self = this; + + try { + this._suspendedRelationships = true; + Ember._suspendObservers(self, observers, null, 'belongsToDidChange', function() { + Ember._suspendBeforeObservers(self, observers, null, 'belongsToWillChange', function() { + callback.call(binding || self); }); - store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange); - correspondingChange.sync(); + }); + } finally { + this._suspendedRelationships = false; } - } -}; + }, -DS.OneToManyChange.createChange = function(childReference, parentReference, store, options) { - var key; + /** + Save the record and persist any changes to the record to an + extenal source via the adapter. - // If the name of the belongsTo side of the relationship is specified, - // use that - // If the type of the parent is specified, look it up on the child's type - // definition. - if (options.parentType) { - key = options.parentType.inverseFor(options.key).name; - DS.OneToManyChange.maintainInvariant( options, store, childReference, key ); - } else if (options.key) { - key = options.key; - } else { - Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); - } + Example - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); + ```javascript + record.set('name', 'Tomster'); + record.save().then(function(){ + // Success callback + }, function() { + // Error callback + }); + ``` + @method save + @return {Promise} a promise that will be resolved when the adapter returns + successfully or rejected if the adapter returns with an error. + */ + save: function() { + var promiseLabel = "DS: Model#save " + this; + var resolver = Ember.RSVP.defer(promiseLabel); - store.addRelationshipChangeFor(childReference, key, parentReference, change.getSecondRecordName(), change); + this.get('store').scheduleSave(this, resolver); + this._inFlightAttributes = this._attributes; + this._attributes = {}; + return DS.PromiseObject.create({ promise: resolver.promise }); + }, - return change; -}; + /** + Reload the record from the adapter. + This will only work if the record has already finished loading + and has not yet been modified (`isLoaded` but not `isDirty`, + or `isSaving`). -DS.OneToManyChange.maintainInvariant = function(options, store, childReference, key){ - var child = childReference.record; + Example - if (options.changeType === "add" && child) { - var oldParent = get(child, key); - if (oldParent){ - var correspondingChange = DS.OneToManyChange.createChange(childReference, oldParent.get('_reference'), store, { - parentType: options.parentType, - hasManyName: options.hasManyName, - changeType: "remove", - key: options.key - }); - store.addRelationshipChangeFor(childReference, key, options.parentReference, correspondingChange.getSecondRecordName(), correspondingChange); - correspondingChange.sync(); - } - } -}; + ```javascript + App.ModelViewRoute = Ember.Route.extend({ + actions: { + reload: function() { + this.get('model').reload(); + } + } + }); + ``` -DS.OneToManyChange.ensureSameTransaction = function(changes){ - var records = Ember.A(); - forEach(changes, function(change){ - records.addObject(change.getSecondRecord()); - records.addObject(change.getFirstRecord()); - }); + @method reload + @return {Promise} a promise that will be resolved with the record when the + adapter returns successfully or rejected if the adapter returns + with an error. + */ + reload: function() { + set(this, 'isReloading', true); - return DS.Transaction.ensureSameTransaction(records); -}; + var record = this; -/** - @class RelationshipChange - @namespace DS -*/ -DS.RelationshipChange.prototype = { + var promiseLabel = "DS: Model#reload of " + this; + var promise = new Ember.RSVP.Promise(function(resolve){ + record.send('reloadRecord', resolve); + }, promiseLabel).then(function() { + record.set('isReloading', false); + record.set('isError', false); + return record; + }, function(reason) { + record.set('isError', true); + throw reason; + }, "DS: Model#reload complete, update flags"); - getSecondRecordName: function() { - var name = this.secondRecordName, parent; + return DS.PromiseObject.create({ promise: promise }); + }, - if (!name) { - parent = this.secondRecordReference; - if (!parent) { return; } + // FOR USE DURING COMMIT PROCESS - var childType = this.firstRecordReference.type; - var inverse = childType.inverseFor(this.firstRecordName); - this.secondRecordName = inverse.name; + adapterDidUpdateAttribute: function(attributeName, value) { + + // If a value is passed in, update the internal attributes and clear + // the attribute cache so it picks up the new value. Otherwise, + // collapse the current value into the internal attributes because + // the adapter has acknowledged it. + if (value !== undefined) { + this._data[attributeName] = value; + this.notifyPropertyChange(attributeName); + } else { + this._data[attributeName] = this._inFlightAttributes[attributeName]; } - return this.secondRecordName; + this.updateRecordArraysLater(); }, /** - Get the name of the relationship on the belongsTo side. - - @method getFirstRecordName - @return {String} + @method adapterDidInvalidate + @private */ - getFirstRecordName: function() { - var name = this.firstRecordName; - return name; + adapterDidInvalidate: function(errors) { + var recordErrors = get(this, 'errors'); + function addError(name) { + if (errors[name]) { + recordErrors.add(name, errors[name]); + } + } + + this.eachAttribute(addError); + this.eachRelationship(addError); }, /** - @method destroy + @method adapterDidError @private */ - destroy: function() { - var childReference = this.childReference, - belongsToName = this.getFirstRecordName(), - hasManyName = this.getSecondRecordName(), - store = this.store; - - store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); + adapterDidError: function() { + this.send('becameError'); + set(this, 'isError', true); }, /** - @method getByReference + Override the default event firing from Ember.Evented to + also call methods with the given name. + + @method trigger @private - @param reference + @param name */ - getByReference: function(reference) { - // return null or undefined if the original reference was null or undefined - if (!reference) { return reference; } - - if (reference.record) { - return reference.record; - } + trigger: function(name) { + Ember.tryInvoke(this, name, [].slice.call(arguments, 1)); + this._super.apply(this, arguments); }, - getSecondRecord: function(){ - return this.getByReference(this.secondRecordReference); + triggerLater: function() { + if (this._deferredTriggers.push(arguments) !== 1) { return; } + Ember.run.schedule('actions', this, '_triggerDeferredTriggers'); }, + _triggerDeferredTriggers: function() { + for (var i=0, l=this._deferredTriggers.length; i')` from " + this.toString(), name !== 'id'); + + meta.name = name; + map.set(name, meta); + } + }); + + return map; + }), + + /** + A map whose keys are the attributes of the model (properties + described by DS.attr) and whose values are type of transformation + applied to each attribute. This map does not include any + attributes that do not have an transformation type. + + Example + + ```javascript + App.Person = DS.Model.extend({ + firstName: attr(), + lastName: attr('string'), + birthday: attr('date') + }); + + var transformedAttributes = Ember.get(App.Person, 'transformedAttributes') + + transformedAttributes.forEach(function(field, type) { + console.log(field, type); + }); - /** - Make sure that all three parts of the relationship change are part of - the same transaction. If any of the three records is clean and in the - default transaction, and the rest are in a different transaction, move - them all into that transaction. + // prints: + // lastName string + // birthday date + ``` - @method ensureSameTransaction - @private + @property transformedAttributes + @static + @type {Ember.Map} + @readOnly */ - ensureSameTransaction: function() { - var child = this.getFirstRecord(), - parentRecord = this.getSecondRecord(); + transformedAttributes: Ember.computed(function() { + var map = Ember.Map.create(); - var transaction = DS.Transaction.ensureSameTransaction([child, parentRecord]); + this.eachAttribute(function(key, meta) { + if (meta.type) { + map.set(key, meta.type); + } + }); - this.transaction = transaction; - return transaction; - }, + return map; + }), - callChangeEvents: function(){ - var child = this.getFirstRecord(), - parentRecord = this.getSecondRecord(); + /** + Iterates through the attributes of the model, calling the passed function on each + attribute. - var dirtySet = new Ember.OrderedSet(); + The callback method you provide should have the following signature (all + parameters are optional): - // TODO: This implementation causes a race condition in key-value - // stores. The fix involves buffering changes that happen while - // a record is loading. A similar fix is required for other parts - // of ember-data, and should be done as new infrastructure, not - // a one-off hack. [tomhuda] - if (parentRecord && get(parentRecord, 'isLoaded')) { - this.store.recordHasManyDidChange(dirtySet, parentRecord, this); - } + ```javascript + function(name, meta); + ``` - if (child) { - this.store.recordBelongsToDidChange(dirtySet, child, this); - } + - `name` the name of the current property in the iteration + - `meta` the meta object for the attribute property in the iteration - dirtySet.forEach(function(record) { - record.adapterDidDirty(); + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. + + Example + + ```javascript + App.Person = DS.Model.extend({ + firstName: attr('string'), + lastName: attr('string'), + birthday: attr('date') }); - }, - coalesce: function(){ - var relationshipPairs = this.store.relationshipChangePairsFor(this.firstRecordReference); - forEach(relationshipPairs, function(pair){ - var addedChange = pair["add"]; - var removedChange = pair["remove"]; - if(addedChange && removedChange) { - addedChange.destroy(); - removedChange.destroy(); - } + App.Person.eachAttribute(function(name, meta) { + console.log(name, meta); }); - } -}; -DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({})); -DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({})); + // prints: + // firstName {type: "string", isAttribute: true, options: Object, parentType: function, name: "firstName"} + // lastName {type: "string", isAttribute: true, options: Object, parentType: function, name: "lastName"} + // birthday {type: "date", isAttribute: true, options: Object, parentType: function, name: "birthday"} + ``` -DS.RelationshipChangeAdd.prototype.changeType = "add"; -DS.RelationshipChangeAdd.prototype.sync = function() { - var secondRecordName = this.getSecondRecordName(), - firstRecordName = this.getFirstRecordName(), - firstRecord = this.getFirstRecord(), - secondRecord = this.getSecondRecord(); + @method eachAttribute + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @static + */ + eachAttribute: function(callback, binding) { + get(this, 'attributes').forEach(function(name, meta) { + callback.call(binding, name, meta); + }, binding); + }, - //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); - //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); + /** + Iterates through the transformedAttributes of the model, calling + the passed function on each attribute. Note the callback will not be + called for any attributes that do not have an transformation type. - this.ensureSameTransaction(); + The callback method you provide should have the following signature (all + parameters are optional): - this.callChangeEvents(); + ```javascript + function(name, type); + ``` - if (secondRecord && firstRecord) { - if(this.secondRecordKind === "belongsTo"){ - secondRecord.suspendRelationshipObservers(function(){ - set(secondRecord, secondRecordName, firstRecord); - }); + - `name` the name of the current property in the iteration + - `type` a string containing the name of the type of transformed + applied to the attribute - } - else if(this.secondRecordKind === "hasMany"){ - secondRecord.suspendRelationshipObservers(function(){ - get(secondRecord, secondRecordName).addObject(firstRecord); - }); - } - } + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. - if (firstRecord && secondRecord && get(firstRecord, firstRecordName) !== secondRecord) { - if(this.firstRecordKind === "belongsTo"){ - firstRecord.suspendRelationshipObservers(function(){ - set(firstRecord, firstRecordName, secondRecord); - }); - } - else if(this.firstRecordKind === "hasMany"){ - firstRecord.suspendRelationshipObservers(function(){ - get(firstRecord, firstRecordName).addObject(secondRecord); - }); - } - } + Example - this.coalesce(); -}; + ```javascript + App.Person = DS.Model.extend({ + firstName: attr(), + lastName: attr('string'), + birthday: attr('date') + }); -DS.RelationshipChangeRemove.prototype.changeType = "remove"; -DS.RelationshipChangeRemove.prototype.sync = function() { - var secondRecordName = this.getSecondRecordName(), - firstRecordName = this.getFirstRecordName(), - firstRecord = this.getFirstRecord(), - secondRecord = this.getSecondRecord(); + App.Person.eachTransformedAttribute(function(name, type) { + console.log(name, type); + }); - //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); - //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); + // prints: + // lastName string + // birthday date + ``` - this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); + @method eachTransformedAttribute + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @static + */ + eachTransformedAttribute: function(callback, binding) { + get(this, 'transformedAttributes').forEach(function(name, type) { + callback.call(binding, name, type); + }); + } +}); - this.callChangeEvents(); - if (secondRecord && firstRecord) { - if(this.secondRecordKind === "belongsTo"){ - secondRecord.suspendRelationshipObservers(function(){ - set(secondRecord, secondRecordName, null); - }); - } - else if(this.secondRecordKind === "hasMany"){ - secondRecord.suspendRelationshipObservers(function(){ - get(secondRecord, secondRecordName).removeObject(firstRecord); - }); - } +DS.Model.reopen({ + eachAttribute: function(callback, binding) { + this.constructor.eachAttribute(callback, binding); } +}); - if (firstRecord && get(firstRecord, firstRecordName)) { - if(this.firstRecordKind === "belongsTo"){ - firstRecord.suspendRelationshipObservers(function(){ - set(firstRecord, firstRecordName, null); - }); - } - else if(this.firstRecordKind === "hasMany"){ - firstRecord.suspendRelationshipObservers(function(){ - get(firstRecord, firstRecordName).removeObject(secondRecord); - }); - } +function getDefaultValue(record, options, key) { + if (typeof options.defaultValue === "function") { + return options.defaultValue(); + } else { + return options.defaultValue; } +} - this.coalesce(); -}; - -})(); - +function hasValue(record, key) { + return record._attributes.hasOwnProperty(key) || + record._inFlightAttributes.hasOwnProperty(key) || + record._data.hasOwnProperty(key); +} +function getValue(record, key) { + if (record._attributes.hasOwnProperty(key)) { + return record._attributes[key]; + } else if (record._inFlightAttributes.hasOwnProperty(key)) { + return record._inFlightAttributes[key]; + } else { + return record._data[key]; + } +} -(function() { /** - @module ember-data -*/ + `DS.attr` defines an attribute on a [DS.Model](DS.Model.html). + By default, attributes are passed through as-is, however you can specify an + optional type to have the value automatically transformed. + Ember Data ships with four basic transform types: `string`, `number`, + `boolean` and `date`. You can define your own transforms by subclassing + [DS.Transform](DS.Transform.html). -})(); + `DS.attr` takes an optional hash as a second parameter, currently + supported options are: + - `defaultValue`: Pass a string or a function to be called to set the attribute + to a default value if none is supplied. + Example -(function() { -var get = Ember.get, set = Ember.set, - isNone = Ember.isNone; + ```javascript + var attr = DS.attr; -/** - @module ember-data -*/ + App.User = DS.Model.extend({ + username: attr('string'), + email: attr('string'), + verified: attr('boolean', {defaultValue: false}) + }); + ``` -DS.belongsTo = function(type, options) { - Ember.assert("The first argument DS.belongsTo must be a model type or string, like DS.belongsTo(App.Person)", !!type && (typeof type === 'string' || DS.Model.detect(type))); + @namespace + @method attr + @for DS + @param {String} type the attribute type + @param {Object} options a hash of options + @return {Attribute} +*/ +DS.attr = function(type, options) { options = options || {}; - var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; + var meta = { + type: type, + isAttribute: true, + options: options + }; return Ember.computed(function(key, value) { - var data = get(this, 'data'), - store = get(this, 'store'), belongsTo; - - if (typeof type === 'string') { - if (type.indexOf(".") === -1) { - type = store.modelFor(type); - } else { - type = get(Ember.lookup, type); - } - } - - if (arguments.length === 2) { - Ember.assert("You can only add a record of " + type.toString() + " to this relationship", !value || type.detectInstance(value)); - return value === undefined ? null : value; - } - - belongsTo = data[key]; - - if (belongsTo instanceof DS.Model) { return belongsTo; } - - // TODO (tomdale) The value of the belongsTo in the data hash can be - // one of: - // 1. null/undefined - // 2. a record reference - // 3. a tuple returned by the serializer's polymorphism code - // - // We should really normalize #3 to be the same as #2 to reduce the - // complexity here. + if (arguments.length > 1) { + Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.constructor.toString(), key !== 'id'); + var oldValue = this._attributes[key] || this._inFlightAttributes[key] || this._data[key]; - if (isNone(belongsTo)) { - return null; - } + this.send('didSetProperty', { + name: key, + oldValue: oldValue, + originalValue: this._data[key], + value: value + }); - // The data has been normalized to a record reference, so - // just ask the store for the record for that reference, - // materializing it if necessary. - if (belongsTo.clientId) { - return store.recordForReference(belongsTo); + this._attributes[key] = value; + return value; + } else if (hasValue(this, key)) { + return getValue(this, key); + } else { + return getDefaultValue(this, options, key); } - // The data has been normalized into a type/id pair by the - // serializer's polymorphism code. - return store.findById(belongsTo.type, belongsTo.id); + // `data` is never set directly. However, it may be + // invalidated from the state manager's setData + // event. }).property('data').meta(meta); }; -/* - These observers observe all `belongsTo` relationships on the record. See - `relationships/ext` to see how these observers get their dependencies. - - @class Model - @namespace DS -*/ -DS.Model.reopen({ - - /** - @method belongsToWillChange - @private - @static - @param record - @param key - */ - belongsToWillChange: Ember.beforeObserver(function(record, key) { - if (get(record, 'isLoaded')) { - var oldParent = get(record, key); - - var childReference = get(record, '_reference'), - store = get(record, 'store'); - if (oldParent){ - var change = DS.RelationshipChange.createChange(childReference, get(oldParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "remove" }); - change.sync(); - this._changesToSync[key] = change; - } - } - }), - - /** - @method belongsToDidChange - @private - @static - @param record - @param key - */ - belongsToDidChange: Ember.immediateObserver(function(record, key) { - if (get(record, 'isLoaded')) { - var newParent = get(record, key); - if(newParent){ - var childReference = get(record, '_reference'), - store = get(record, 'store'); - var change = DS.RelationshipChange.createChange(childReference, get(newParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "add" }); - change.sync(); - if(this._changesToSync[key]){ - DS.OneToManyChange.ensureSameTransaction([change, this._changesToSync[key]], store); - } - } - } - delete this._changesToSync[key]; - }) -}); })(); @@ -5238,523 +5884,509 @@ DS.Model.reopen({ @module ember-data */ -var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; - -var hasRelationship = function(type, options) { - options = options || {}; - - var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' }; +})(); - return Ember.computed(function(key, value) { - var data = get(this, 'data'), - store = get(this, 'store'), - ids, relationship; - if (typeof type === 'string') { - if (type.indexOf(".") === -1) { - type = store.modelFor(type); - } else { - type = get(Ember.lookup, type); - } - } - //ids can be references or opaque token - //(e.g. `{url: '/relationship'}`) that will be passed to the adapter - ids = data[key]; +(function() { +/** + @module ember-data +*/ - relationship = store.findMany(type, ids, this, meta); - set(relationship, 'owner', this); - set(relationship, 'name', key); - set(relationship, 'isPolymorphic', options.polymorphic); +/** + An AttributeChange object is created whenever a record's + attribute changes value. It is used to track changes to a + record between transaction commits. - return relationship; - }).property().meta(meta); + @class AttributeChange + @namespace DS + @private + @constructor +*/ +var AttributeChange = DS.AttributeChange = function(options) { + this.record = options.record; + this.store = options.store; + this.name = options.name; + this.value = options.value; + this.oldValue = options.oldValue; }; -DS.hasMany = function(type, options) { - Ember.assert("The type passed to DS.hasMany must be defined", !!type); - return hasRelationship(type, options); +AttributeChange.createChange = function(options) { + return new AttributeChange(options); }; -function clearUnmaterializedHasMany(record, relationship) { - var data = get(record, 'data'); - - var references = data[relationship.key]; - - if (!references) { return; } - - var inverse = record.constructor.inverseFor(relationship.key); - - if (inverse) { - forEach(references, function(reference) { - var childRecord; +AttributeChange.prototype = { + sync: function() { + if (this.value !== this.oldValue) { + this.record.send('becomeDirty'); + this.record.updateRecordArraysLater(); + } - if (childRecord = reference.record) { - record.suspendRelationshipObservers(function() { - set(childRecord, inverse.name, null); - }); - } - }); - } -} + // TODO: Use this object in the commit process + this.destroy(); + }, -DS.Model.reopen({ - clearHasMany: function(relationship) { - var hasMany = this.cacheFor(relationship.name); + /** + If the AttributeChange is destroyed (either by being rolled back + or being committed), remove it from the list of pending changes + on the record. - if (hasMany) { - hasMany.clear(); - } else { - clearUnmaterializedHasMany(this, relationship); - } + @method destroy + */ + destroy: function() { + delete this.record._changesToSync[this.name]; } -}); +}; })(); (function() { -var get = Ember.get, set = Ember.set; - /** @module ember-data */ -/* - This file defines several extensions to the base `DS.Model` class that - add support for one-to-many relationships. -*/ +var get = Ember.get, set = Ember.set; +var forEach = Ember.EnumerableUtils.forEach; /** - @class Model + @class RelationshipChange @namespace DS + @private + @constructor */ -DS.Model.reopen({ - - /** - This Ember.js hook allows an object to be notified when a property - is defined. - - In this case, we use it to be notified when an Ember Data user defines a - belongs-to relationship. In that case, we need to set up observers for - each one, allowing us to track relationship changes and automatically - reflect changes in the inverse has-many array. - - This hook passes the class being set up, as well as the key and value - being defined. So, for example, when the user does this: - - DS.Model.extend({ - parent: DS.belongsTo(App.User) - }); - - This hook would be called with "parent" as the key and the computed - property returned by `DS.belongsTo` as the value. - - @method didDefineProperty - @param proto - @param key - @param value - */ - didDefineProperty: function(proto, key, value) { - // Check if the value being set is a computed property. - if (value instanceof Ember.Descriptor) { - - // If it is, get the metadata for the relationship. This is - // populated by the `DS.belongsTo` helper when it is creating - // the computed property. - var meta = value.meta(); - - if (meta.isRelationship && meta.kind === 'belongsTo') { - Ember.addObserver(proto, key, null, 'belongsToDidChange'); - Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange'); - } - - if (meta.isAttribute) { - Ember.addObserver(proto, key, null, 'attributeDidChange'); - Ember.addBeforeObserver(proto, key, null, 'attributeWillChange'); - } - - meta.parentType = proto.constructor; - } - } -}); - -/* - These DS.Model extensions add class methods that provide relationship - introspection abilities about relationships. - - A note about the computed properties contained here: +DS.RelationshipChange = function(options) { + this.parentRecord = options.parentRecord; + this.childRecord = options.childRecord; + this.firstRecord = options.firstRecord; + this.firstRecordKind = options.firstRecordKind; + this.firstRecordName = options.firstRecordName; + this.secondRecord = options.secondRecord; + this.secondRecordKind = options.secondRecordKind; + this.secondRecordName = options.secondRecordName; + this.changeType = options.changeType; + this.store = options.store; - **These properties are effectively sealed once called for the first time.** - To avoid repeatedly doing expensive iteration over a model's fields, these - values are computed once and then cached for the remainder of the runtime of - your application. + this.committed = {}; +}; - If your application needs to modify a class after its initial definition - (for example, using `reopen()` to add additional attributes), make sure you - do it before using your model with the store, which uses these properties - extensively. +/** + @class RelationshipChangeAdd + @namespace DS + @private + @constructor */ +DS.RelationshipChangeAdd = function(options){ + DS.RelationshipChange.call(this, options); +}; -DS.Model.reopenClass({ - /** - For a given relationship name, returns the model type of the relationship. - - For example, if you define a model like this: - - App.Post = DS.Model.extend({ - comments: DS.hasMany(App.Comment) - }); +/** + @class RelationshipChangeRemove + @namespace DS + @private + @constructor +*/ +DS.RelationshipChangeRemove = function(options){ + DS.RelationshipChange.call(this, options); +}; - Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`. +DS.RelationshipChange.create = function(options) { + return new DS.RelationshipChange(options); +}; - @method typeForRelationship - @static - @param {String} name the name of the relationship - @return {subclass of DS.Model} the type of the relationship, or undefined - */ - typeForRelationship: function(name) { - var relationship = get(this, 'relationshipsByName').get(name); - return relationship && relationship.type; - }, +DS.RelationshipChangeAdd.create = function(options) { + return new DS.RelationshipChangeAdd(options); +}; - inverseFor: function(name) { - var inverseType = this.typeForRelationship(name); +DS.RelationshipChangeRemove.create = function(options) { + return new DS.RelationshipChangeRemove(options); +}; - if (!inverseType) { return null; } +DS.OneToManyChange = {}; +DS.OneToNoneChange = {}; +DS.ManyToNoneChange = {}; +DS.OneToOneChange = {}; +DS.ManyToManyChange = {}; - var options = this.metaForProperty(name).options; +DS.RelationshipChange._createChange = function(options){ + if(options.changeType === "add"){ + return DS.RelationshipChangeAdd.create(options); + } + if(options.changeType === "remove"){ + return DS.RelationshipChangeRemove.create(options); + } +}; - if (options.inverse === null) { return null; } - - var inverseName, inverseKind; - if (options.inverse) { - inverseName = options.inverse; - inverseKind = Ember.get(inverseType, 'relationshipsByName').get(inverseName).kind; - } else { - var possibleRelationships = findPossibleInverses(this, inverseType); +DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide){ + var knownKey = knownSide.key, key, otherKind; + var knownKind = knownSide.kind; - if (possibleRelationships.length === 0) { return null; } + var inverse = recordType.inverseFor(knownKey); - Ember.assert("You defined the '" + name + "' relationship on " + this + ", but multiple possible inverse relationships of type " + this + " were found on " + inverseType + ".", possibleRelationships.length === 1); + if (inverse){ + key = inverse.name; + otherKind = inverse.kind; + } - inverseName = possibleRelationships[0].name; - inverseKind = possibleRelationships[0].kind; + if (!inverse){ + return knownKind === "belongsTo" ? "oneToNone" : "manyToNone"; + } + else{ + if(otherKind === "belongsTo"){ + return knownKind === "belongsTo" ? "oneToOne" : "manyToOne"; } - - function findPossibleInverses(type, inverseType, possibleRelationships) { - possibleRelationships = possibleRelationships || []; - - var relationshipMap = get(inverseType, 'relationships'); - if (!relationshipMap) { return; } - - var relationships = relationshipMap.get(type); - if (relationships) { - possibleRelationships.push.apply(possibleRelationships, relationshipMap.get(type)); - } - - if (type.superclass) { - findPossibleInverses(type.superclass, inverseType, possibleRelationships); - } - - return possibleRelationships; + else{ + return knownKind === "belongsTo" ? "oneToMany" : "manyToMany"; } + } - return { - type: inverseType, - name: inverseName, - kind: inverseKind - }; - }, - - /** - The model's relationships as a map, keyed on the type of the - relationship. The value of each entry is an array containing a descriptor - for each relationship with that type, describing the name of the relationship - as well as the type. - - For example, given the following model definition: - - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - posts: DS.hasMany(App.Post) - }); +}; - This computed property would return a map describing these - relationships, like this: +DS.RelationshipChange.createChange = function(firstRecord, secondRecord, store, options){ + // Get the type of the child based on the child's client ID + var firstRecordType = firstRecord.constructor, changeType; + changeType = DS.RelationshipChange.determineRelationshipType(firstRecordType, options); + if (changeType === "oneToMany"){ + return DS.OneToManyChange.createChange(firstRecord, secondRecord, store, options); + } + else if (changeType === "manyToOne"){ + return DS.OneToManyChange.createChange(secondRecord, firstRecord, store, options); + } + else if (changeType === "oneToNone"){ + return DS.OneToNoneChange.createChange(firstRecord, secondRecord, store, options); + } + else if (changeType === "manyToNone"){ + return DS.ManyToNoneChange.createChange(firstRecord, secondRecord, store, options); + } + else if (changeType === "oneToOne"){ + return DS.OneToOneChange.createChange(firstRecord, secondRecord, store, options); + } + else if (changeType === "manyToMany"){ + return DS.ManyToManyChange.createChange(firstRecord, secondRecord, store, options); + } +}; - var relationships = Ember.get(App.Blog, 'relationships'); - relationships.get(App.User); - //=> [ { name: 'users', kind: 'hasMany' }, - // { name: 'owner', kind: 'belongsTo' } ] - relationships.get(App.Post); - //=> [ { name: 'posts', kind: 'hasMany' } ] +DS.OneToNoneChange.createChange = function(childRecord, parentRecord, store, options) { + var key = options.key; + var change = DS.RelationshipChange._createChange({ + parentRecord: parentRecord, + childRecord: childRecord, + firstRecord: childRecord, + store: store, + changeType: options.changeType, + firstRecordName: key, + firstRecordKind: "belongsTo" + }); - @property relationships - @static - @type Ember.Map - @readOnly - */ - relationships: Ember.computed(function() { - var map = new Ember.MapWithDefault({ - defaultValue: function() { return []; } - }); + store.addRelationshipChangeFor(childRecord, key, parentRecord, null, change); - // Loop through each computed property on the class - this.eachComputedProperty(function(name, meta) { + return change; +}; - // If the computed property is a relationship, add - // it to the map. - if (meta.isRelationship) { - if (typeof meta.type === 'string') { - meta.type = Ember.get(Ember.lookup, meta.type); - } +DS.ManyToNoneChange.createChange = function(childRecord, parentRecord, store, options) { + var key = options.key; + var change = DS.RelationshipChange._createChange({ + parentRecord: childRecord, + childRecord: parentRecord, + secondRecord: childRecord, + store: store, + changeType: options.changeType, + secondRecordName: options.key, + secondRecordKind: "hasMany" + }); - var relationshipsForType = map.get(meta.type); + store.addRelationshipChangeFor(childRecord, key, parentRecord, null, change); + return change; +}; - relationshipsForType.push({ name: name, kind: meta.kind }); - } - }); - return map; - }), +DS.ManyToManyChange.createChange = function(childRecord, parentRecord, store, options) { + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + var key = options.key; - /** - A hash containing lists of the model's relationships, grouped - by the relationship kind. For example, given a model with this - definition: + var change = DS.RelationshipChange._createChange({ + parentRecord: parentRecord, + childRecord: childRecord, + firstRecord: childRecord, + secondRecord: parentRecord, + firstRecordKind: "hasMany", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), + store.addRelationshipChangeFor(childRecord, key, parentRecord, null, change); - posts: DS.hasMany(App.Post) - }); - This property would contain the following: + return change; +}; - var relationshipNames = Ember.get(App.Blog, 'relationshipNames'); - relationshipNames.hasMany; - //=> ['users', 'posts'] - relationshipNames.belongsTo; - //=> ['owner'] +DS.OneToOneChange.createChange = function(childRecord, parentRecord, store, options) { + var key; - @property relationshipNames - @static - @type Object - @readOnly - */ - relationshipNames: Ember.computed(function() { - var names = { hasMany: [], belongsTo: [] }; + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + if (options.parentType) { + key = options.parentType.inverseFor(options.key).name; + } else if (options.key) { + key = options.key; + } else { + Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); + } - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - names[meta.kind].push(name); - } - }); + var change = DS.RelationshipChange._createChange({ + parentRecord: parentRecord, + childRecord: childRecord, + firstRecord: childRecord, + secondRecord: parentRecord, + firstRecordKind: "belongsTo", + secondRecordKind: "belongsTo", + store: store, + changeType: options.changeType, + firstRecordName: key + }); - return names; - }), + store.addRelationshipChangeFor(childRecord, key, parentRecord, null, change); - /** - An array of types directly related to a model. Each type will be - included once, regardless of the number of relationships it has with - the model. - For example, given a model with this definition: + return change; +}; - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - posts: DS.hasMany(App.Post) +DS.OneToOneChange.maintainInvariant = function(options, store, childRecord, key){ + if (options.changeType === "add" && store.recordIsMaterialized(childRecord)) { + var oldParent = get(childRecord, key); + if (oldParent){ + var correspondingChange = DS.OneToOneChange.createChange(childRecord, oldParent, store, { + parentType: options.parentType, + hasManyName: options.hasManyName, + changeType: "remove", + key: options.key }); + store.addRelationshipChangeFor(childRecord, key, options.parentRecord , null, correspondingChange); + correspondingChange.sync(); + } + } +}; - This property would contain the following: +DS.OneToManyChange.createChange = function(childRecord, parentRecord, store, options) { + var key; - var relatedTypes = Ember.get(App.Blog, 'relatedTypes'); - //=> [ App.User, App.Post ] + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + if (options.parentType) { + key = options.parentType.inverseFor(options.key).name; + DS.OneToManyChange.maintainInvariant( options, store, childRecord, key ); + } else if (options.key) { + key = options.key; + } else { + Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); + } - @property relatedTypes - @static - @type Ember.Array - @readOnly - */ - relatedTypes: Ember.computed(function() { - var type, - types = Ember.A(); + var change = DS.RelationshipChange._createChange({ + parentRecord: parentRecord, + childRecord: childRecord, + firstRecord: childRecord, + secondRecord: parentRecord, + firstRecordKind: "belongsTo", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); - // Loop through each computed property on the class, - // and create an array of the unique types involved - // in relationships - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - type = meta.type; + store.addRelationshipChangeFor(childRecord, key, parentRecord, change.getSecondRecordName(), change); - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } - Ember.assert("You specified a hasMany (" + meta.type + ") on " + meta.parentType + " but " + meta.type + " was not found.", type); + return change; +}; - if (!types.contains(type)) { - Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type); - types.push(type); - } - } - }); - return types; - }), +DS.OneToManyChange.maintainInvariant = function(options, store, childRecord, key){ + if (options.changeType === "add" && childRecord) { + var oldParent = get(childRecord, key); + if (oldParent){ + var correspondingChange = DS.OneToManyChange.createChange(childRecord, oldParent, store, { + parentType: options.parentType, + hasManyName: options.hasManyName, + changeType: "remove", + key: options.key + }); + store.addRelationshipChangeFor(childRecord, key, options.parentRecord, correspondingChange.getSecondRecordName(), correspondingChange); + correspondingChange.sync(); + } + } +}; - /** - A map whose keys are the relationships of a model and whose values are - relationship descriptors. +/** + @class RelationshipChange + @namespace DS +*/ +DS.RelationshipChange.prototype = { - For example, given a model with this - definition: + getSecondRecordName: function() { + var name = this.secondRecordName, parent; - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), + if (!name) { + parent = this.secondRecord; + if (!parent) { return; } - posts: DS.hasMany(App.Post) - }); + var childType = this.firstRecord.constructor; + var inverse = childType.inverseFor(this.firstRecordName); + this.secondRecordName = inverse.name; + } - This property would contain the following: + return this.secondRecordName; + }, - var relationshipsByName = Ember.get(App.Blog, 'relationshipsByName'); - relationshipsByName.get('users'); - //=> { key: 'users', kind: 'hasMany', type: App.User } - relationshipsByName.get('owner'); - //=> { key: 'owner', kind: 'belongsTo', type: App.User } + /** + Get the name of the relationship on the belongsTo side. - @property relationshipsByName - @static - @type Ember.Map - @readOnly + @method getFirstRecordName + @return {String} */ - relationshipsByName: Ember.computed(function() { - var map = Ember.Map.create(), type; - - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - meta.key = name; - type = meta.type; - - if (typeof type === 'string') { - if (type.match(/^[^A-Z]/)) { - type = this.store.modelFor(type); - } else { - type = get(this, type, false) || get(Ember.lookup, type); - } + getFirstRecordName: function() { + var name = this.firstRecordName; + return name; + }, - meta.type = type; - } + /** + @method destroy + @private + */ + destroy: function() { + var childRecord = this.childRecord, + belongsToName = this.getFirstRecordName(), + hasManyName = this.getSecondRecordName(), + store = this.store; - map.set(name, meta); - } - }); + store.removeRelationshipChangeFor(childRecord, belongsToName, this.parentRecord, hasManyName, this.changeType); + }, - return map; - }), + getSecondRecord: function(){ + return this.secondRecord; + }, /** - A map whose keys are the fields of the model and whose values are strings - describing the kind of the field. A model's fields are the union of all of its - attributes and relationships. - - For example: + @method getFirstRecord + @private + */ + getFirstRecord: function() { + return this.firstRecord; + }, - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), + coalesce: function(){ + var relationshipPairs = this.store.relationshipChangePairsFor(this.firstRecord); + forEach(relationshipPairs, function(pair){ + var addedChange = pair["add"]; + var removedChange = pair["remove"]; + if(addedChange && removedChange) { + addedChange.destroy(); + removedChange.destroy(); + } + }); + } +}; - posts: DS.hasMany(App.Post), +DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({})); +DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({})); - title: DS.attr('string') - }); +// the object is a value, and not a promise +function isValue(object) { + return typeof object === 'object' && (!object.then || typeof object.then !== 'function'); +} - var fields = Ember.get(App.Blog, 'fields'); - fields.forEach(function(field, kind) { - console.log(field, kind); - }); +DS.RelationshipChangeAdd.prototype.changeType = "add"; +DS.RelationshipChangeAdd.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); - // prints: - // users, hasMany - // owner, belongsTo - // posts, hasMany - // title, attribute + //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); + //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); - @property fields - @static - @type Ember.Map - @readOnly - */ - fields: Ember.computed(function() { - var map = Ember.Map.create(); + if (secondRecord instanceof DS.Model && firstRecord instanceof DS.Model) { + if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ + set(secondRecord, secondRecordName, firstRecord); + }); - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - map.set(name, meta.kind); - } else if (meta.isAttribute) { - map.set(name, 'attribute'); - } - }); + } + else if(this.secondRecordKind === "hasMany"){ + secondRecord.suspendRelationshipObservers(function(){ + var relationship = get(secondRecord, secondRecordName); + if (isValue(relationship)) { relationship.addObject(firstRecord); } + }); + } + } - return map; - }), + if (firstRecord instanceof DS.Model && secondRecord instanceof DS.Model && get(firstRecord, firstRecordName) !== secondRecord) { + if(this.firstRecordKind === "belongsTo"){ + firstRecord.suspendRelationshipObservers(function(){ + set(firstRecord, firstRecordName, secondRecord); + }); + } + else if(this.firstRecordKind === "hasMany"){ + firstRecord.suspendRelationshipObservers(function(){ + var relationship = get(firstRecord, firstRecordName); + if (isValue(relationship)) { relationship.addObject(secondRecord); } + }); + } + } - /** - Given a callback, iterates over each of the relationships in the model, - invoking the callback with the name of each relationship and its relationship - descriptor. + this.coalesce(); +}; - @method eachRelationship - @static - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelationship: function(callback, binding) { - get(this, 'relationshipsByName').forEach(function(name, relationship) { - callback.call(binding, name, relationship); - }); - }, +DS.RelationshipChangeRemove.prototype.changeType = "remove"; +DS.RelationshipChangeRemove.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); - /** - Given a callback, iterates over each of the types related to a model, - invoking the callback with the related type's class. Each type will be - returned just once, regardless of how many different relationships it has - with a model. + //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); + //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); - @method eachRelatedType - @static - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelatedType: function(callback, binding) { - get(this, 'relatedTypes').forEach(function(type) { - callback.call(binding, type); - }); + if (secondRecord instanceof DS.Model && firstRecord instanceof DS.Model) { + if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ + set(secondRecord, secondRecordName, null); + }); + } + else if(this.secondRecordKind === "hasMany"){ + secondRecord.suspendRelationshipObservers(function(){ + var relationship = get(secondRecord, secondRecordName); + if (isValue(relationship)) { relationship.removeObject(firstRecord); } + }); + } } -}); - -DS.Model.reopen({ - /** - Given a callback, iterates over each of the relationships in the model, - invoking the callback with the name of each relationship and its relationship - descriptor. - @method eachRelationship - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelationship: function(callback, binding) { - this.constructor.eachRelationship(callback, binding); + if (firstRecord instanceof DS.Model && get(firstRecord, firstRecordName)) { + if(this.firstRecordKind === "belongsTo"){ + firstRecord.suspendRelationshipObservers(function(){ + set(firstRecord, firstRecordName, null); + }); + } + else if(this.firstRecordKind === "hasMany"){ + firstRecord.suspendRelationshipObservers(function(){ + var relationship = get(firstRecord, firstRecordName); + if (isValue(relationship)) { relationship.removeObject(secondRecord); } + }); + } } -}); + + this.coalesce(); +}; })(); @@ -5770,215 +6402,179 @@ DS.Model.reopen({ (function() { +var get = Ember.get, set = Ember.set, + isNone = Ember.isNone; + /** @module ember-data */ -var get = Ember.get, set = Ember.set; -var once = Ember.run.once; -var forEach = Ember.EnumerableUtils.forEach; - -/** - @class RecordArrayManager - @namespace DS - @private - @extends Ember.Object -*/ -DS.RecordArrayManager = Ember.Object.extend({ - init: function() { - this.filteredRecordArrays = Ember.MapWithDefault.create({ - defaultValue: function() { return []; } - }); +function asyncBelongsTo(type, options, meta) { + return Ember.computed(function(key, value) { + var data = get(this, 'data'), + store = get(this, 'store'), + promiseLabel = "DS: Async belongsTo " + this + " : " + key; - this.changedReferences = []; - }, + if (arguments.length === 2) { + Ember.assert("You can only add a '" + type + "' record to this relationship", !value || value instanceof store.modelFor(type)); + return value === undefined ? null : DS.PromiseObject.create({ promise: Ember.RSVP.resolve(value, promiseLabel) }); + } - referenceDidChange: function(reference) { - this.changedReferences.push(reference); - once(this, this.updateRecordArrays); - }, + var link = data.links && data.links[key], + belongsTo = data[key]; - recordArraysForReference: function(reference) { - reference.recordArrays = reference.recordArrays || Ember.OrderedSet.create(); - return reference.recordArrays; - }, + if(!isNone(belongsTo)) { + var promise = store.fetchRecord(belongsTo) || Ember.RSVP.resolve(belongsTo, promiseLabel); + return DS.PromiseObject.create({ promise: promise}); + } else if (link) { + var resolver = Ember.RSVP.defer("DS: Async belongsTo (link) " + this + " : " + key); + store.findBelongsTo(this, link, meta, resolver); + return DS.PromiseObject.create({ promise: resolver.promise }); + } else { + return null; + } + }).property('data').meta(meta); +} - /** - This method is invoked whenever data is loaded into the store - by the adapter or updated by the adapter, or when an attribute - changes on a record. +/** + `DS.belongsTo` is used to define One-To-One and One-To-Many + relationships on a [DS.Model](DS.Model.html). - It updates all filters that a record belongs to. - To avoid thrashing, it only runs once per run loop per record. + `DS.belongsTo` takes an optional hash as a second parameter, currently + supported options are: - @method updateRecordArrays - @param {Class} type - @param {Number|String} clientId - */ - updateRecordArrays: function() { - forEach(this.changedReferences, function(reference) { - var type = reference.type, - recordArrays = this.filteredRecordArrays.get(type), - filter; - - forEach(recordArrays, function(array) { - filter = get(array, 'filterFunction'); - this.updateRecordArray(array, filter, type, reference); - }, this); + - `async`: A boolean value used to explicitly declare this to be an async relationship. + - `inverse`: A string used to identify the inverse property on a + related model in a One-To-Many relationship. See [Explicit Inverses](#toc_explicit-inverses) - // loop through all manyArrays containing an unloaded copy of this - // clientId and notify them that the record was loaded. - var manyArrays = reference.loadingRecordArrays; + #### One-To-One + To declare a one-to-one relationship between two models, use + `DS.belongsTo`: - if (manyArrays) { - for (var i=0, l=manyArrays.length; i [ { name: 'users', kind: 'hasMany' }, + // { name: 'owner', kind: 'belongsTo' } ] + relationships.get(App.Post); + //=> [ { name: 'posts', kind: 'hasMany' } ] + ``` - @method serialize - @param {DS.Model} record the record to serialize - @param {Object} [options] a hash of options - @returns {any} the serialized form of the record + @property relationships + @static + @type Ember.Map + @readOnly */ - serialize: function(record, options) { - options = options || {}; + relationships: Ember.computed(function() { + var map = new Ember.MapWithDefault({ + defaultValue: function() { return []; } + }); - var serialized = this.createSerializedForm(), id; + // Loop through each computed property on the class + this.eachComputedProperty(function(name, meta) { - if (options.includeId) { - if (id = get(record, 'id')) { - this._addId(serialized, record.constructor, id); - } - } + // If the computed property is a relationship, add + // it to the map. + if (meta.isRelationship) { + if (typeof meta.type === 'string') { + meta.type = this.store.modelFor(meta.type); + } - if (options.includeType) { - this.addType(serialized, record.constructor); - } + var relationshipsForType = map.get(meta.type); - this.addAttributes(serialized, record); - this.addRelationships(serialized, record); + relationshipsForType.push({ name: name, kind: meta.kind }); + } + }); - return serialized; - }, + return map; + }), /** - Given an attribute type and value, convert the value into the - serialized form using the transform registered for that type. + A hash containing lists of the model's relationships, grouped + by the relationship kind. For example, given a model with this + definition: - @method serializeValue - @private - @param {any} value the value to convert to the serialized form - @param {String} attributeType the registered type (e.g. `string` - or `boolean`) - @returns {any} the serialized form of the value + ```javascript + App.Blog = DS.Model.extend({ + users: DS.hasMany('user'), + owner: DS.belongsTo('user'), + + posts: DS.hasMany('post') + }); + ``` + + This property would contain the following: + + ```javascript + var relationshipNames = Ember.get(App.Blog, 'relationshipNames'); + relationshipNames.hasMany; + //=> ['users', 'posts'] + relationshipNames.belongsTo; + //=> ['owner'] + ``` + + @property relationshipNames + @static + @type Object + @readOnly */ - serializeValue: function(value, attributeType) { - var transform = this.transforms ? this.transforms[attributeType] : null; + relationshipNames: Ember.computed(function() { + var names = { hasMany: [], belongsTo: [] }; + + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + names[meta.kind].push(name); + } + }); - Ember.assert("You tried to use an attribute type (" + attributeType + ") that has not been registered", transform); - return transform.serialize(value); - }, + return names; + }), /** - A hook you can use to normalize IDs before adding them to the - serialized representation. - - Because the store coerces all IDs to strings for consistency, - this is the opportunity for the serializer to, for example, - convert numerical IDs back into number form. + An array of types directly related to a model. Each type will be + included once, regardless of the number of relationships it has with + the model. - Null or undefined ids will resolve to a null value. + For example, given a model with this definition: - @method serializeId - @param {String} id the id from the record - @returns {any} the serialized representation of the id - */ - serializeId: function(id) { - if(Ember.isEmpty(id)) { return null; } - if(isNaN(+id)) { return id; } - return +id; - }, + ```javascript + App.Blog = DS.Model.extend({ + users: DS.hasMany('user'), + owner: DS.belongsTo('user'), - /** - A hook you can use to change how attributes are added to the serialized - representation of a record. + posts: DS.hasMany('post') + }); + ``` - By default, `addAttributes` simply loops over all of the attributes of the - passed record, maps the attribute name to the key for the serialized form, - and invokes any registered transforms on the value. It then invokes the - more granular `addAttribute` with the key and transformed value. + This property would contain the following: - Since you can override `keyForAttributeName`, `addAttribute`, and register - custom transforms, you should rarely need to override this hook. + ```javascript + var relatedTypes = Ember.get(App.Blog, 'relatedTypes'); + //=> [ App.User, App.Post ] + ``` - @method addAttributes - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize + @property relatedTypes + @static + @type Ember.Array + @readOnly */ - addAttributes: function(data, record) { - record.eachAttribute(function(name, attribute) { - this._addAttribute(data, record, name, attribute.type); - }, this); - }, - - /** - A hook you can use to customize how the key/value pair is added to - the serialized data. + relatedTypes: Ember.computed(function() { + var type, + types = Ember.A(); - @method addAttribute - @param {any} serialized the serialized form being built - @param {String} key the key to add to the serialized data - @param {any} value the value to add to the serialized data - */ - addAttribute: mustImplement('addAttribute'), + // Loop through each computed property on the class, + // and create an array of the unique types involved + // in relationships + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + type = meta.type; - /** - A hook you can use to customize how the record's id is added to - the serialized data. + if (typeof type === 'string') { + type = get(this, type, false) || this.store.modelFor(type); + } - The `addId` hook is called with: + Ember.assert("You specified a hasMany (" + meta.type + ") on " + meta.parentType + " but " + meta.type + " was not found.", type); - * the serialized representation being built - * the resolved primary key (taking configurations and the - `primaryKey` hook into consideration) - * the serialized id (after calling the `serializeId` hook) + if (!types.contains(type)) { + Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type); + types.push(type); + } + } + }); - @method addId - @param {any} data the serialized representation that is being built - @param {String} key the resolved primary key - @param {id} id the serialized id - */ - addId: mustImplement('addId'), + return types; + }), /** - A hook you can use to customize how the record's type is added to - the serialized data. - - The `addType` hook is called with: - - * the serialized representation being built - * the serialized id (after calling the `serializeId` hook) - - @method addType - @param {any} data the serialized representation that is being built - @param {DS.Model subclass} type the type of the record - */ - addType: Ember.K, + A map whose keys are the relationships of a model and whose values are + relationship descriptors. - /** - Creates an empty hash that will be filled in by the hooks called from the - `serialize()` method. + For example, given a model with this + definition: - @method createSerializedForm - @return {Object} - */ - createSerializedForm: function() { - return {}; - }, + ```javascript + App.Blog = DS.Model.extend({ + users: DS.hasMany('user'), + owner: DS.belongsTo('user'), - /** - A hook you can use to change how relationships are added to the serialized - representation of a record. + posts: DS.hasMany('post') + }); + ``` - By default, `addRelationships` loops over all of the relationships of the - passed record, maps the relationship names to the key for the serialized form, - and then invokes the public `addBelongsTo` and `addHasMany` hooks. + This property would contain the following: - Since you can override `keyForBelongsTo`, `keyForHasMany`, `addBelongsTo`, - `addHasMany`, and register mappings, you should rarely need to override this - hook. + ```javascript + var relationshipsByName = Ember.get(App.Blog, 'relationshipsByName'); + relationshipsByName.get('users'); + //=> { key: 'users', kind: 'hasMany', type: App.User } + relationshipsByName.get('owner'); + //=> { key: 'owner', kind: 'belongsTo', type: App.User } + ``` - @method addRelationships - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize + @property relationshipsByName + @static + @type Ember.Map + @readOnly */ - addRelationships: function(data, record) { - record.eachRelationship(function(name, relationship) { - if (relationship.kind === 'belongsTo') { - this._addBelongsTo(data, record, name, relationship); - } else if (relationship.kind === 'hasMany') { - this._addHasMany(data, record, name, relationship); - } - }, this); - }, - - /** - A hook you can use to add a `belongsTo` relationship to the - serialized representation. + relationshipsByName: Ember.computed(function() { + var map = Ember.Map.create(), type; - The specifics of this hook are very adapter-specific, so there - is no default implementation. You can see `DS.JSONSerializer` - for an example of an implementation of the `addBelongsTo` hook. + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + meta.key = name; + type = meta.type; - The `belongsTo` relationship object has the following properties: + if (!type && meta.kind === 'hasMany') { + type = Ember.String.singularize(name); + } else if (!type) { + type = name; + } - * **type** a subclass of DS.Model that is the type of the - relationship. This is the first parameter to DS.belongsTo - * **options** the options passed to the call to DS.belongsTo - * **kind** always `belongsTo` + if (typeof type === 'string') { + meta.type = this.store.modelFor(type); + } - Additional properties may be added in the future. + map.set(name, meta); + } + }); - @method addBelongsTo - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - @param {String} key the key for the serialized object - @param {Object} relationship an object representing the relationship - */ - addBelongsTo: mustImplement('addBelongsTo'), + return map; + }), /** - A hook you can use to add a `hasMany` relationship to the - serialized representation. + A map whose keys are the fields of the model and whose values are strings + describing the kind of the field. A model's fields are the union of all of its + attributes and relationships. - The specifics of this hook are very adapter-specific, so there - is no default implementation. You may not need to implement this, - for example, if your backend only expects relationships on the - child of a one to many relationship. + For example: - The `hasMany` relationship object has the following properties: + ```javascript - * **type** a subclass of DS.Model that is the type of the - relationship. This is the first parameter to DS.hasMany - * **options** the options passed to the call to DS.hasMany - * **kind** always `hasMany` + App.Blog = DS.Model.extend({ + users: DS.hasMany('user'), + owner: DS.belongsTo('user'), - Additional properties may be added in the future. + posts: DS.hasMany('post'), - @method addHasMany - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - @param {String} key the key for the serialized object - @param {Object} relationship an object representing the relationship - */ - addHasMany: mustImplement('addHasMany'), + title: DS.attr('string') + }); - /* - NAMING CONVENTIONS + var fields = Ember.get(App.Blog, 'fields'); + fields.forEach(function(field, kind) { + console.log(field, kind); + }); - The most commonly overridden APIs of the serializer are - the naming convention methods: + // prints: + // users, hasMany + // owner, belongsTo + // posts, hasMany + // title, attribute + ``` - * `keyForAttributeName`: converts a camelized attribute name - into a key in the adapter-provided data hash. For example, - if the model's attribute name was `firstName`, and the - server used underscored names, you would return `first_name`. - * `primaryKey`: returns the key that should be used to - extract the id from the adapter-provided data hash. It is - also used when serializing a record. + @property fields + @static + @type Ember.Map + @readOnly */ + fields: Ember.computed(function() { + var map = Ember.Map.create(); - /** - A hook you can use in your serializer subclass to customize - how an unmapped attribute name is converted into a key. - - By default, this method returns the `name` parameter. - - For example, if the attribute names in your JSON are underscored, - you will want to convert them into JavaScript conventional - camelcase: - - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - - keyForAttributeName: function(type, name) { - return name.camelize(); + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + map.set(name, meta.kind); + } else if (meta.isAttribute) { + map.set(name, 'attribute'); } }); - ``` - @method keyForAttributeName - @param {DS.Model subclass} type the type of the record with - the attribute name `name` - @param {String} name the attribute name to convert into a key + return map; + }), + + /** + Given a callback, iterates over each of the relationships in the model, + invoking the callback with the name of each relationship and its relationship + descriptor. - @returns {String} the key + @method eachRelationship + @static + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound */ - keyForAttributeName: function(type, name) { - return name; + eachRelationship: function(callback, binding) { + get(this, 'relationshipsByName').forEach(function(name, relationship) { + callback.call(binding, name, relationship); + }); }, /** - A hook you can use in your serializer to specify a conventional - primary key. - - By default, this method will return the string `id`. - - In general, you should not override this hook to specify a special - primary key for an individual type; use `configure` instead. - - For example, if your primary key is always `__id__`: + Given a callback, iterates over each of the types related to a model, + invoking the callback with the related type's class. Each type will be + returned just once, regardless of how many different relationships it has + with a model. - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - primaryKey: function(type) { - return '__id__'; - } + @method eachRelatedType + @static + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelatedType: function(callback, binding) { + get(this, 'relatedTypes').forEach(function(type) { + callback.call(binding, type); }); - ``` - - In another example, if the primary key always includes the - underscored version of the type before the string `id`: + } +}); - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - primaryKey: function(type) { - // If the type is `BlogPost`, this will return - // `blog_post_id`. - var typeString = type.toString().split(".")[1].underscore(); - return typeString + "_id"; - } - }); - ``` +DS.Model.reopen({ + /** + Given a callback, iterates over each of the relationships in the model, + invoking the callback with the name of each relationship and its relationship + descriptor. - @method primaryKey - @param {DS.Model subclass} type - @returns {String} the primary key for the type + @method eachRelationship + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound */ - primaryKey: function(type) { - return "id"; - }, + eachRelationship: function(callback, binding) { + this.constructor.eachRelationship(callback, binding); + } +}); - /** - A hook you can use in your serializer subclass to customize - how an unmapped `belongsTo` relationship is converted into - a key. +})(); - By default, this method calls `keyForAttributeName`, so if - your naming convention is uniform across attributes and - relationships, you can use the default here and override - just `keyForAttributeName` as needed. - For example, if the `belongsTo` names in your JSON always - begin with `BT_` (e.g. `BT_posts`), you can strip out the - `BT_` prefix:" - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - keyForBelongsTo: function(type, name) { - return name.match(/^BT_(.*)$/)[1].camelize(); - } - }); - ``` +(function() { +/** + @module ember-data +*/ - @method keyForBelongsTo - @param {DS.Model subclass} type the type of the record with - the `belongsTo` relationship. - @param {String} name the relationship name to convert into a key +})(); - @returns {String} the key - */ - keyForBelongsTo: function(type, name) { - return this.keyForAttributeName(type, name); - }, - /** - A hook you can use in your serializer subclass to customize - how an unmapped `hasMany` relationship is converted into - a key. - By default, this method calls `keyForAttributeName`, so if - your naming convention is uniform across attributes and - relationships, you can use the default here and override - just `keyForAttributeName` as needed. +(function() { +/** + @module ember-data +*/ - For example, if the `hasMany` names in your JSON always - begin with the "table name" for the current type (e.g. - `post_comments`), you can strip out the prefix:" +var get = Ember.get, set = Ember.set; +var forEach = Ember.EnumerableUtils.forEach; - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - keyForHasMany: function(type, name) { - // if your App.BlogPost has many App.BlogComment, the key from - // the server would look like: `blog_post_blog_comments` - // - // 1. Convert the type into a string and underscore the - // second part (App.BlogPost -> blog_post) - // 2. Extract the part after `blog_post_` (`blog_comments`) - // 3. Underscore it, to become `blogComments` - var typeString = type.toString().split(".")[1].underscore(); - return name.match(new RegExp("^" + typeString + "_(.*)$"))[1].camelize(); - } +/** + @class RecordArrayManager + @namespace DS + @private + @extends Ember.Object +*/ +DS.RecordArrayManager = Ember.Object.extend({ + init: function() { + this.filteredRecordArrays = Ember.MapWithDefault.create({ + defaultValue: function() { return []; } }); - ``` - - @method keyForHasMany - @param {DS.Model subclass} type the type of the record with - the `belongsTo` relationship. - @param {String} name the relationship name to convert into a key - @returns {String} the key - */ - keyForHasMany: function(type, name) { - return this.keyForAttributeName(type, name); + this.changedRecords = []; }, - //......................... - //. MATERIALIZATION HOOKS - //......................... + recordDidChange: function(record) { + if (this.changedRecords.push(record) !== 1) { return; } - materialize: function(record, serialized, prematerialized) { - var id; - if (Ember.isNone(get(record, 'id'))) { - if (prematerialized && prematerialized.hasOwnProperty('id')) { - id = prematerialized.id; - } else { - id = this.extractId(record.constructor, serialized); - } - record.materializeId(id); - } + Ember.run.schedule('actions', this, this.updateRecordArrays); + }, - this.materializeAttributes(record, serialized, prematerialized); - this.materializeRelationships(record, serialized, prematerialized); + recordArraysForRecord: function(record) { + record._recordArrays = record._recordArrays || Ember.OrderedSet.create(); + return record._recordArrays; }, - deserializeValue: function(value, attributeType) { - var transform = this.transforms ? this.transforms[attributeType] : null; + /** + This method is invoked whenever data is loaded into the store by the + adapter or updated by the adapter, or when a record has changed. + + It updates all record arrays that a record belongs to. - Ember.assert("You tried to use an attribute type (" + attributeType + ") that has not been registered", transform); - return transform.deserialize(value); - }, + To avoid thrashing, it only runs at most once per run loop. - materializeAttributes: function(record, serialized, prematerialized) { - record.eachAttribute(function(name, attribute) { - if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeAttribute(name, prematerialized[name]); + @method updateRecordArrays + @param {Class} type + @param {Number|String} clientId + */ + updateRecordArrays: function() { + forEach(this.changedRecords, function(record) { + if (get(record, 'isDeleted')) { + this._recordWasDeleted(record); } else { - this.materializeAttribute(record, serialized, name, attribute.type); + this._recordWasChanged(record); } }, this); + + this.changedRecords.length = 0; }, - materializeAttribute: function(record, serialized, attributeName, attributeType) { - var value = this.extractAttribute(record.constructor, serialized, attributeName); - value = this.deserializeValue(value, attributeType); + _recordWasDeleted: function (record) { + var recordArrays = record._recordArrays; - record.materializeAttribute(attributeName, value); - }, + if (!recordArrays) { return; } - materializeRelationships: function(record, serialized, prematerialized) { - record.eachRelationship(function(name, relationship) { - if (relationship.kind === 'hasMany') { - if (prematerialized && prematerialized.hasOwnProperty(name)) { - var tuplesOrReferencesOrOpaque = this._convertPrematerializedHasMany(relationship.type, prematerialized[name]); - record.materializeHasMany(name, tuplesOrReferencesOrOpaque); - } else { - this.materializeHasMany(name, record, serialized, relationship, prematerialized); - } - } else if (relationship.kind === 'belongsTo') { - if (prematerialized && prematerialized.hasOwnProperty(name)) { - var tupleOrReference = this._convertTuple(relationship.type, prematerialized[name]); - record.materializeBelongsTo(name, tupleOrReference); - } else { - this.materializeBelongsTo(name, record, serialized, relationship, prematerialized); - } - } - }, this); + forEach(recordArrays, function(array) { + array.removeRecord(record); + }); }, - materializeHasMany: function(name, record, hash, relationship) { + _recordWasChanged: function (record) { var type = record.constructor, - key = this._keyForHasMany(type, relationship.key), - idsOrTuples = this.extractHasMany(type, hash, key), - tuples = idsOrTuples; + recordArrays = this.filteredRecordArrays.get(type), + filter; - if(idsOrTuples && Ember.isArray(idsOrTuples)) { - tuples = this._convertTuples(relationship.type, idsOrTuples); - } + forEach(recordArrays, function(array) { + filter = get(array, 'filterFunction'); + this.updateRecordArray(array, filter, type, record); + }, this); - record.materializeHasMany(name, tuples); - }, + // loop through all manyArrays containing an unloaded copy of this + // clientId and notify them that the record was loaded. + var manyArrays = record._loadingRecordArrays; - materializeBelongsTo: function(name, record, hash, relationship) { - var type = record.constructor, - key = this._keyForBelongsTo(type, relationship.key), - idOrTuple, - tuple = null; + if (manyArrays) { + for (var i=0, l=manyArrays.length; i 'low' - Server Response / Load: { myTask: {priority: 0} } + ```js + DS.RESTAdapter.reopen({ + namespace: 'api/1' + }); + ``` + Requests for `App.Person` would now target `/api/1/people/1`. - @method registerEnumTransform - @param {String} type of the transform - @param {Array} array of String objects to use for the enumerated values. - This is an ordered list and the index values will be used for the transform. - */ - registerEnumTransform: function(attributeType, objects) { - get(this, 'serializer').registerEnumTransform(attributeType, objects); - }, + ### Host customization - /** - If the globally unique IDs for your records should be generated on the client, - implement the `generateIdForRecord()` method. This method will be invoked - each time you create a new record, and the value returned from it will be - assigned to the record's `primaryKey`. + An adapter can target other hosts by setting the `host` property. - Most traditional REST-like HTTP APIs will not use this method. Instead, the ID - of the record will be set by the server, and your adapter will update the store - with the new ID when it calls `didCreateRecord()`. Only implement this method if - you intend to generate record IDs on the client-side. + ```js + DS.RESTAdapter.reopen({ + host: 'https://api.example.com' + }); + ``` - The `generateIdForRecord()` method will be invoked with the requesting store as - the first parameter and the newly created record as the second parameter: + ### Headers customization - generateIdForRecord: function(store, record) { - var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision(); - return uuid; - } + Some APIs require HTTP headers, e.g. to provide an API key. An array of + headers can be added to the adapter which are passed with every request: - @method generateIdForRecord - @param {DS.Store} store - @param {DS.Model} record - */ - generateIdForRecord: null, + ```js + DS.RESTAdapter.reopen({ + headers: { + "API_KEY": "secret key", + "ANOTHER_HEADER": "Some header value" + } + }); + ``` - /** - Proxies to the serializer's `materialize` method. + @class RESTAdapter + @constructor + @namespace DS + @extends DS.Adapter +*/ +DS.RESTAdapter = DS.Adapter.extend({ + defaultSerializer: '-rest', - @method materialize - @param {DS.Model} record - @param {Object} data - @param {Object} prematerialized - */ - materialize: function(record, data, prematerialized) { - get(this, 'serializer').materialize(record, data, prematerialized); - }, /** - Proxies to the serializer's `serialize` method. + Endpoint paths can be prefixed with a `namespace` by setting the namespace + property on the adapter: - @method serialize - @param {DS.Model} record - @param {Object} options - */ - serialize: function(record, options) { - return get(this, 'serializer').serialize(record, options); - }, + ```javascript + DS.RESTAdapter.reopen({ + namespace: 'api/1' + }); + ``` - /** - Proxies to the serializer's `extractId` method. + Requests for `App.Post` would now target `/api/1/post/`. - @method extractId - @param {DS.Model} type the model class - @param {Object} data + @property namespace + @type {String} */ - extractId: function(type, data) { - return get(this, 'serializer').extractId(type, data); - }, /** - @method groupByType - @private - @param enumerable - */ - groupByType: function(enumerable) { - var map = Ember.MapWithDefault.create({ - defaultValue: function() { return Ember.OrderedSet.create(); } - }); + An adapter can target other hosts by setting the `host` property. - forEach(enumerable, function(item) { - map.get(item.constructor).add(item); + ```javascript + DS.RESTAdapter.reopen({ + host: 'https://api.example.com' }); + ``` - return map; - }, - - /** - The commit method is called when a transaction is being committed. - The `commitDetails` is a map with each record type and a list of - committed, updated and deleted records. - - By default, this just calls the adapter's `save` method. - If you need more advanced handling of commits, e.g., only sending - certain records to the server, you can overwrite this method. + Requests for `App.Post` would now target `https://api.example.com/post/`. - @method commit - @params {DS.Store} store - @params {Ember.Map} commitDetails see `DS.Transaction#commitDetails`. + @property host + @type {String} */ - commit: function(store, commitDetails) { - this.save(store, commitDetails); - }, /** - Iterates over each set of records provided in the commit details and - filters with `DS.Adapter#shouldSave` and then calls `createRecords`, - `updateRecords`, and `deleteRecords` for each set as approriate. - - @method save - @params {DS.Store} store - @params {Ember.Map} commitDetails see `DS.Transaction#commitDetails`. - */ - save: function(store, commitDetails) { - var adapter = this; - - function filter(records) { - var filteredSet = Ember.OrderedSet.create(); - - records.forEach(function(record) { - if (adapter.shouldSave(record)) { - filteredSet.add(record); - } - }); + Some APIs require HTTP headers, e.g. to provide an API key. An array of + headers can be added to the adapter which are passed with every request: - return filteredSet; - } + ```javascript + DS.RESTAdapter.reopen({ + headers: { + "API_KEY": "secret key", + "ANOTHER_HEADER": "Some header value" + } + }); + ``` - this.groupByType(commitDetails.created).forEach(function(type, set) { - this.createRecords(store, type, filter(set)); - }, this); + @property headers + @type {Object} + */ - this.groupByType(commitDetails.updated).forEach(function(type, set) { - this.updateRecords(store, type, filter(set)); - }, this); + /** + Called by the store in order to fetch the JSON for a given + type and ID. - this.groupByType(commitDetails.deleted).forEach(function(type, set) { - this.deleteRecords(store, type, filter(set)); - }, this); - }, + The `find` method makes an Ajax request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. - /** - Called on each record before saving. If false is returned, the record - will not be saved. + This method performs an HTTP `GET` request with the id provided as part of the query string. - @method shouldSave - @property {DS.Model} record - @return {Boolean} `true` to save, `false` to not. Defaults to true. + @method find + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {String} id + @returns {Promise} promise */ - shouldSave: function(record) { - return true; + find: function(store, type, id) { + return this.ajax(this.buildURL(type.typeKey, id), 'GET'); }, /** - Implement this method in a subclass to handle the creation of - new records. - - Serializes the record and send it to the server. + Called by the store in order to fetch a JSON array for all + of the records for a given type. - This implementation should call the adapter's `didCreateRecord` - method on success or `didError` method on failure. + The `findAll` method makes an Ajax (HTTP GET) request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. - @method createRecord - @property {DS.Store} store - @property {DS.Model} type the DS.Model class of the record - @property {DS.Model} record + @private + @method findAll + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {String} sinceToken + @returns {Promise} promise */ - createRecord: Ember.required(Function), - - /** - Creates multiple records at once. + findAll: function(store, type, sinceToken) { + var query; - By default, it loops over the supplied array and calls `createRecord` - on each. May be overwritten to improve performance and reduce the number - of server requests. + if (sinceToken) { + query = { since: sinceToken }; + } - @method createRecords - @property {DS.Store} store - @property {DS.Model} type the DS.Model class of the records - @property {Array[DS.Model]} records - */ - createRecords: function(store, type, records) { - records.forEach(function(record) { - this.createRecord(store, type, record); - }, this); + return this.ajax(this.buildURL(type.typeKey), 'GET', { data: query }); }, /** - Implement this method in a subclass to handle the updating of - a record. + Called by the store in order to fetch a JSON array for + the records that match a particular query. - Serializes the record update and send it to the server. - - @method updateRecord - @property {DS.Store} store - @property {DS.Model} type the DS.Model class of the record - @property {DS.Model} record - */ - updateRecord: Ember.required(Function), - - /** - Updates multiple records at once. + The `findQuery` method makes an Ajax (HTTP GET) request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. - By default, it loops over the supplied array and calls `updateRecord` - on each. May be overwritten to improve performance and reduce the number - of server requests. + The `query` argument is a simple JavaScript object that will be passed directly + to the server as parameters. - @method updateRecords - @property {DS.Store} store - @property {DS.Model} type the DS.Model class of the records - @property {Array[DS.Model]} records + @private + @method findQuery + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} query + @returns {Promise} promise */ - updateRecords: function(store, type, records) { - records.forEach(function(record) { - this.updateRecord(store, type, record); - }, this); + findQuery: function(store, type, query) { + return this.ajax(this.buildURL(type.typeKey), 'GET', { data: query }); }, /** - Implement this method in a subclass to handle the deletion of - a record. - - Sends a delete request for the record to the server. + Called by the store in order to fetch a JSON array for + the unloaded records in a has-many relationship that were originally + specified as IDs. - @method deleteRecord - @property {DS.Store} store - @property {DS.Model} type the DS.Model class of the record - @property {DS.Model} record - */ - deleteRecord: Ember.required(Function), + For example, if the original payload looks like: - /** - Delete multiple records at once. + ```js + { + "id": 1, + "title": "Rails is omakase", + "comments": [ 1, 2, 3 ] + } + ``` - By default, it loops over the supplied array and calls `deleteRecord` - on each. May be overwritten to improve performance and reduce the number - of server requests. + The IDs will be passed as a URL-encoded Array of IDs, in this form: - @method deleteRecords - @property {DS.Store} store - @property {DS.Model} type the DS.Model class of the records - @property {Array[DS.Model]} records - */ - deleteRecords: function(store, type, records) { - records.forEach(function(record) { - this.deleteRecord(store, type, record); - }, this); - }, + ``` + ids[]=1&ids[]=2&ids[]=3 + ``` - /** - Find multiple records at once. + Many servers, such as Rails and PHP, will automatically convert this URL-encoded array + into an Array for you on the server-side. If you want to encode the + IDs, differently, just override this (one-line) method. - By default, it loops over the provided ids and calls `find` on each. - May be overwritten to improve performance and reduce the number of - server requests. + The `findMany` method makes an Ajax (HTTP GET) request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. @method findMany - @property {DS.Store} store - @property {DS.Model} type the DS.Model class of the records - @property {Array} ids + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Array} ids + @returns {Promise} promise */ findMany: function(store, type, ids) { - ids.forEach(function(id) { - this.find(store, type, id); - }, this); - } -}); - -DS.Adapter.reopenClass({ + return this.ajax(this.buildURL(type.typeKey), 'GET', { data: { ids: ids } }); + }, /** - Registers a custom attribute transform for the adapter class - - The `transform` property is an object with a `serialize` and - `deserialize` property. These are each functions that respectively - serialize the data to send to the backend or deserialize it for - use on the client. + Called by the store in order to fetch a JSON array for + the unloaded records in a has-many relationship that were originally + specified as a URL (inside of `links`). - @method registerTransform - @static - @property {DS.String} attributeType - @property {Object} transform - */ - registerTransform: function(attributeType, transform) { - var registeredTransforms = this._registeredTransforms || {}; + For example, if your original payload looks like this: - registeredTransforms[attributeType] = transform; + ```js + { + "post": { + "id": 1, + "title": "Rails is omakase", + "links": { "comments": "/posts/1/comments" } + } + } + ``` - this._registeredTransforms = registeredTransforms; - }, + This method will be called with the parent record and `/posts/1/comments`. - /** - Registers a custom enumerable transform for the adapter class + The `findHasMany` method will make an Ajax (HTTP GET) request to the originally specified URL. + If the URL is host-relative (starting with a single slash), the + request will use the host specified on the adapter (if any). - @method registerEnumTransform - @static - @property {DS.String} attributeType - @property objects + @method findHasMany + @param {DS.Store} store + @param {DS.Model} record + @param {String} url + @returns {Promise} promise */ - registerEnumTransform: function(attributeType, objects) { - var registeredEnumTransforms = this._registeredEnumTransforms || {}; + findHasMany: function(store, record, url) { + var host = get(this, 'host'), + id = get(record, 'id'), + type = record.constructor.typeKey; - registeredEnumTransforms[attributeType] = objects; + if (host && url.charAt(0) === '/' && url.charAt(1) !== '/') { + url = host + url; + } - this._registeredEnumTransforms = registeredEnumTransforms; + return this.ajax(this.urlPrefix(url, this.buildURL(type, id)), 'GET'); }, /** - Set adapter attributes for a DS.Model class. + Called by the store in order to fetch a JSON array for + the unloaded records in a belongs-to relationship that were originally + specified as a URL (inside of `links`). - @method map - @static - @property {DS.Model} type the DS.Model class - @property {Object} attributes - */ - map: DS._Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) { - var existingValue = map.get(key); + For example, if your original payload looks like this: - merge(existingValue, newValue); - }), + ```js + { + "person": { + "id": 1, + "name": "Tom Dale", + "links": { "group": "/people/1/group" } + } + } + ``` - /** - Set configuration options for a DS.Model class. + This method will be called with the parent record and `/people/1/group`. - @method configure - @static - @property {DS.Model} type the DS.Model class - @property {Object} configuration - */ - configure: DS._Mappable.generateMapFunctionFor('configurations', function(key, newValue, map) { - var existingValue = map.get(key); + The `findBelongsTo` method will make an Ajax (HTTP GET) request to the originally specified URL. - // If a mapping configuration is provided, peel it off and apply it - // using the DS.Adapter.map API. - var mappings = newValue && newValue.mappings; - if (mappings) { - this.map(key, mappings); - delete newValue.mappings; - } + @method findBelongsTo + @param {DS.Store} store + @param {DS.Model} record + @param {String} url + @returns {Promise} promise + */ + findBelongsTo: function(store, record, url) { + var id = get(record, 'id'), + type = record.constructor.typeKey; - merge(existingValue, newValue); - }), + return this.ajax(this.urlPrefix(url, this.buildURL(type, id)), 'GET'); + }, /** - Resolved conflicts in configuration settings. + Called by the store when a newly created record is + saved via the `save` method on a model record instance. - Calls `Ember.merge` by default. - - @method resolveMapConflict - @static - @property oldValue - @property newValue - */ - resolveMapConflict: function(oldValue, newValue) { - merge(newValue, oldValue); + The `createRecord` method serializes the record and makes an Ajax (HTTP POST) request + to a URL computed by `buildURL`. - return newValue; - } -}); + See `serialize` for information on how to customize the serialized form + of a record. -})(); + @method createRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @returns {Promise} promise + */ + createRecord: function(store, type, record) { + var data = {}; + var serializer = store.serializerFor(type.typeKey); + serializer.serializeIntoHash(data, type, record, { includeId: true }); + return this.ajax(this.buildURL(type.typeKey), "POST", { data: data }); + }, -(function() { -/** - @module ember-data -*/ + /** + Called by the store when an existing record is saved + via the `save` method on a model record instance. -var get = Ember.get, set = Ember.set; + The `updateRecord` method serializes the record and makes an Ajax (HTTP PUT) request + to a URL computed by `buildURL`. -/** - @class FixtureSerializer - @namespace DS - @extends DS.Serializer -*/ -DS.FixtureSerializer = DS.Serializer.extend({ + See `serialize` for information on how to customize the serialized form + of a record. - /** - @method deserializeValue - @param value - @param attributeType + @method updateRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @returns {Promise} promise */ - deserializeValue: function(value, attributeType) { - return value; - }, + updateRecord: function(store, type, record) { + var data = {}; + var serializer = store.serializerFor(type.typeKey); - /** - @method serializeValue - @param value - @param attributeType - */ - serializeValue: function(value, attributeType) { - return value; - }, + serializer.serializeIntoHash(data, type, record); - /** - @method addId - @param data - @param key - @param id - */ - addId: function(data, key, id) { - data[key] = id; - }, + var id = get(record, 'id'); - /** - @method addAttribute - @param hash - @param key - @param value - */ - addAttribute: function(hash, key, value) { - hash[key] = value; + return this.ajax(this.buildURL(type.typeKey, id), "PUT", { data: data }); }, /** - @method addBelongsTo - @param hash - @param record - @param key - @param relationship - */ - addBelongsTo: function(hash, record, key, relationship) { - var id = get(record, relationship.key+'.id'); - if (!Ember.isNone(id)) { hash[key] = id; } - }, + Called by the store when a record is deleted. - /** - @method addHasMany - @param hash - @param record - @param key - @param relationship + The `deleteRecord` method makes an Ajax (HTTP DELETE) request to a URL computed by `buildURL`. + + @method deleteRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @returns {Promise} promise */ - addHasMany: function(hash, record, key, relationship) { - var ids = get(record, relationship.key).map(function(item) { - return item.get('id'); - }); + deleteRecord: function(store, type, record) { + var id = get(record, 'id'); - hash[relationship.key] = ids; + return this.ajax(this.buildURL(type.typeKey, id), "DELETE"); }, /** - @method extract - @param loader - @param fixture - @param type - @param record - */ - extract: function(loader, fixture, type, record) { - if (record) { loader.updateId(record, fixture); } - this.extractRecordRepresentation(loader, type, fixture); - }, + Builds a URL for a given type and optional ID. - /** - @method extractMany - @param loader - @param fixtures - @param type - @param records + By default, it pluralizes the type's name (for example, 'post' + becomes 'posts' and 'person' becomes 'people'). To override the + pluralization see [pathForType](#method_pathForType). + + If an ID is specified, it adds the ID to the path generated + for the type, separated by a `/`. + + @method buildURL + @param {String} type + @param {String} id + @returns {String} url */ - extractMany: function(loader, fixtures, type, records) { - var objects = fixtures, references = []; - if (records) { records = records.toArray(); } + buildURL: function(type, id) { + var url = [], + host = get(this, 'host'), + prefix = this.urlPrefix(); - for (var i = 0; i < objects.length; i++) { - if (records) { loader.updateId(records[i], objects[i]); } - var reference = this.extractRecordRepresentation(loader, type, objects[i]); - references.push(reference); - } + if (type) { url.push(this.pathForType(type)); } + if (id) { url.push(id); } - loader.populateArray(references); + if (prefix) { url.unshift(prefix); } + + url = url.join('/'); + if (!host && url) { url = '/' + url; } + + return url; }, /** - @method extractId - @param type - @param hash - */ - extractId: function(type, hash) { - var primaryKey = this._primaryKey(type); - - if (hash.hasOwnProperty(primaryKey)) { - // Ensure that we coerce IDs to strings so that record - // IDs remain consistent between application runs; especially - // if the ID is serialized and later deserialized from the URL, - // when type information will have been lost. - return hash[primaryKey]+''; + @method urlPrefix + @private + @param {String} path + @param {String} parentUrl + @return {String} urlPrefix + */ + urlPrefix: function(path, parentURL) { + var host = get(this, 'host'), + namespace = get(this, 'namespace'), + url = []; + + if (path) { + // Absolute path + if (path.charAt(0) === '/') { + if (host) { + path = path.slice(1); + url.push(host); + } + // Relative path + } else if (!/^http(s)?:\/\//.test(path)) { + url.push(parentURL); + } } else { - return null; + if (host) { url.push(host); } + if (namespace) { url.push(namespace); } } - }, - /** - @method extractAttribute - @param type - @param hash - @param attributeName - */ - extractAttribute: function(type, hash, attributeName) { - var key = this._keyForAttributeName(type, attributeName); - return hash[key]; + if (path) { + url.push(path); + } + + return url.join('/'); }, /** - @method extractHasMany - @param type - @param hash - @param key - */ - extractHasMany: function(type, hash, key) { - return hash[key]; + Determines the pathname for a given type. + + By default, it pluralizes the type's name (for example, + 'post' becomes 'posts' and 'person' becomes 'people'). + + ### Pathname customization + + For example if you have an object LineItem with an + endpoint of "/line_items/". + + ```js + DS.RESTAdapter.reopen({ + pathForType: function(type) { + var decamelized = Ember.String.decamelize(type); + return Ember.String.pluralize(decamelized); + }; + }); + ``` + + @method pathForType + @param {String} type + @returns {String} path + **/ + pathForType: function(type) { + var camelized = Ember.String.camelize(type); + return Ember.String.pluralize(camelized); }, /** - @method extractBelongsTo - @param type - @param hash - @param key + Takes an ajax response, and returns a relevant error. + + Returning a `DS.InvalidError` from this method will cause the + record to transition into the `invalid` state and make the + `errors` object available on the record. + + ```javascript + App.ApplicationAdapter = DS.RESTAdapter.extend({ + ajaxError: function(jqXHR) { + var error = this._super(jqXHR); + + if (jqXHR && jqXHR.status === 422) { + var jsonErrors = Ember.$.parseJSON(jqXHR.responseText)["errors"]; + + return new DS.InvalidError(jsonErrors); + } else { + return error; + } + } + }); + ``` + + Note: As a correctness optimization, the default implementation of + the `ajaxError` method strips out the `then` method from jquery's + ajax response (jqXHR). This is important because the jqXHR's + `then` method fulfills the promise with itself resulting in a + circular "thenable" chain which may cause problems for some + promise libraries. + + @method ajaxError + @param {Object} jqXHR + @return {Object} jqXHR */ - extractBelongsTo: function(type, hash, key) { - var val = hash[key]; - if (val != null) { - val = val + ''; + ajaxError: function(jqXHR) { + if (jqXHR) { + jqXHR.then = null; } - return val; + + return jqXHR; }, /** - @method extractBelongsToPolymorphic - @method type - @method hash - @method key - */ - extractBelongsToPolymorphic: function(type, hash, key) { - var keyForId = this.keyForPolymorphicId(key), - keyForType, - id = hash[keyForId]; + Takes a URL, an HTTP method and a hash of data, and makes an + HTTP request. - if (id) { - keyForType = this.keyForPolymorphicType(key); - return {id: id, type: hash[keyForType]}; - } + When the server responds with a payload, Ember Data will call into `extractSingle` + or `extractArray` (depending on whether the original query was for one record or + many records). - return null; - }, + By default, `ajax` method has the following behavior: - /** - @method keyForPolymorphicId - @param key + * It sets the response `dataType` to `"json"` + * If the HTTP method is not `"GET"`, it sets the `Content-Type` to be + `application/json; charset=utf-8` + * If the HTTP method is not `"GET"`, it stringifies the data passed in. The + data is the serialized record in the case of a save. + * Registers success and failure handlers. + + @method ajax + @private + @param {String} url + @param {String} type The request type GET, POST, PUT, DELETE etc. + @param {Object} hash + @return {Promise} promise */ - keyForPolymorphicId: function(key) { - return key; + ajax: function(url, type, hash) { + var adapter = this; + + return new Ember.RSVP.Promise(function(resolve, reject) { + hash = adapter.ajaxOptions(url, type, hash); + + hash.success = function(json) { + Ember.run(null, resolve, json); + }; + + hash.error = function(jqXHR, textStatus, errorThrown) { + Ember.run(null, reject, adapter.ajaxError(jqXHR)); + }; + + Ember.$.ajax(hash); + }, "DS: RestAdapter#ajax " + type + " to " + url); }, /** - @method keyForPolymorphicType - @param key - */ - keyForPolymorphicType: function(key) { - return key + '_type'; + @method ajaxOptions + @private + @param {String} url + @param {String} type The request type GET, POST, PUT, DELETE etc. + @param {Object} hash + @return {Object} hash + */ + ajaxOptions: function(url, type, hash) { + hash = hash || {}; + hash.url = url; + hash.type = type; + hash.dataType = 'json'; + hash.context = this; + + if (hash.data && type !== 'GET') { + hash.contentType = 'application/json; charset=utf-8'; + hash.data = JSON.stringify(hash.data); + } + + if (this.headers !== undefined) { + var headers = this.headers; + hash.beforeSend = function (xhr) { + forEach.call(Ember.keys(headers), function(key) { + xhr.setRequestHeader(key, headers[key]); + }); + }; + } + + + return hash; } + }); })(); @@ -9120,1039 +9686,898 @@ DS.FixtureSerializer = DS.Serializer.extend({ @module ember-data */ -var get = Ember.get, fmt = Ember.String.fmt, - indexOf = Ember.EnumerableUtils.indexOf; - -/** - `DS.FixtureAdapter` is an adapter that loads records from memory. - Its primarily used for development and testing. You can also use - `DS.FixtureAdapter` while working on the API but are not ready to - integrate yet. It is a fully functioning adapter. All CRUD methods - are implemented. You can also implement query logic that a remote - system would do. Its possible to do develop your entire application - with `DS.FixtureAdapter`. - - @class FixtureAdapter - @namespace DS - @extends DS.Adapter -*/ -DS.FixtureAdapter = DS.Adapter.extend({ +})(); - simulateRemoteResponse: true, - latency: 50, - serializer: DS.FixtureSerializer, +(function() { +DS.Model.reopen({ /** - Implement this method in order to provide data associated with a type - - @method fixturesForType - @param type - */ - fixturesForType: function(type) { - if (type.FIXTURES) { - var fixtures = Ember.A(type.FIXTURES); - return fixtures.map(function(fixture){ - var fixtureIdType = typeof fixture.id; - if(fixtureIdType !== "number" && fixtureIdType !== "string"){ - throw new Error(fmt('the id property must be defined as a number or string for fixture %@', [fixture])); - } - fixture.id = fixture.id + ''; - return fixture; - }); - } - return null; - }, + Provides info about the model for debugging purposes + by grouping the properties into more semantic groups. - /** - Implement this method in order to query fixtures data + Meant to be used by debugging tools such as the Chrome Ember Extension. - @method queryFixtures - @param fixture - @param query - @param type - */ - queryFixtures: function(fixtures, query, type) { - Ember.assert('Not implemented: You must override the DS.FixtureAdapter::queryFixtures method to support querying the fixture store.'); - }, + - Groups all attributes in "Attributes" group. + - Groups all belongsTo relationships in "Belongs To" group. + - Groups all hasMany relationships in "Has Many" group. + - Groups all flags in "Flags" group. + - Flags relationship CPs as expensive properties. - /** - @method updateFixtures - @param type - @param fixture + @method _debugInfo + @for DS.Model + @private */ - updateFixtures: function(type, fixture) { - if(!type.FIXTURES) { - type.FIXTURES = []; - } - - var fixtures = type.FIXTURES; + _debugInfo: function() { + var attributes = ['id'], + relationships = { belongsTo: [], hasMany: [] }, + expensiveProperties = []; - this.deleteLoadedFixture(type, fixture); + this.eachAttribute(function(name, meta) { + attributes.push(name); + }, this); - fixtures.push(fixture); - }, + this.eachRelationship(function(name, relationship) { + relationships[relationship.kind].push(name); + expensiveProperties.push(name); + }); - /** - Implement this method in order to provide provide json for CRUD methods + var groups = [ + { + name: 'Attributes', + properties: attributes, + expand: true + }, + { + name: 'Belongs To', + properties: relationships.belongsTo, + expand: true + }, + { + name: 'Has Many', + properties: relationships.hasMany, + expand: true + }, + { + name: 'Flags', + properties: ['isLoaded', 'isDirty', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid'] + } + ]; - @method mockJSON - @param type - @param record - */ - mockJSON: function(type, record) { - return this.serialize(record, { includeId: true }); - }, + return { + propertyInfo: { + // include all other mixins / properties (not just the grouped ones) + includeOtherProperties: true, + groups: groups, + // don't pre-calculate unless cached + expensiveProperties: expensiveProperties + } + }; + } - /** - @method generateIdForRecord - @param store - @param record - */ - generateIdForRecord: function(store, record) { - return Ember.guidFor(record); - }, +}); - /** - @method find - @param store - @param type - @param id - */ - find: function(store, type, id) { - var fixtures = this.fixturesForType(type), - fixture; +})(); - Ember.warn("Unable to find fixtures for model type " + type.toString(), fixtures); - if (fixtures) { - fixture = Ember.A(fixtures).findProperty('id', id); - } - if (fixture) { - this.simulateRemoteCall(function() { - this.didFindRecord(store, type, fixture, id); - }, this); - } - }, +(function() { +/** + @module ember-data +*/ - /** - @method findMany - @param store - @param type - @param ids - */ - findMany: function(store, type, ids) { - var fixtures = this.fixturesForType(type); +})(); - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); - if (fixtures) { - fixtures = fixtures.filter(function(item) { - return indexOf(ids, item.id) !== -1; - }); - } - if (fixtures) { - this.simulateRemoteCall(function() { - this.didFindMany(store, type, fixtures); - }, this); - } - }, +(function() { +/** + Ember Data - /** - @method findAll - @param store - @param type - */ - findAll: function(store, type) { - var fixtures = this.fixturesForType(type); + @module ember-data + @main ember-data +*/ - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); +})(); - this.simulateRemoteCall(function() { - this.didFindAll(store, type, fixtures); - }, this); - }, +(function() { +Ember.String.pluralize = function(word) { + return Ember.Inflector.inflector.pluralize(word); +}; - /** - @method findQuery - @param store - @param type - @param query - @param array - */ - findQuery: function(store, type, query, array) { - var fixtures = this.fixturesForType(type); +Ember.String.singularize = function(word) { + return Ember.Inflector.inflector.singularize(word); +}; - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); +})(); - fixtures = this.queryFixtures(fixtures, query, type); - if (fixtures) { - this.simulateRemoteCall(function() { - this.didFindQuery(store, type, fixtures, array); - }, this); - } - }, - /** - @method createRecord - @param store - @param type - @param record - */ - createRecord: function(store, type, record) { - var fixture = this.mockJSON(type, record); +(function() { +var BLANK_REGEX = /^\s*$/; - this.updateFixtures(type, fixture); +function loadUncountable(rules, uncountable) { + for (var i = 0, length = uncountable.length; i < length; i++) { + rules.uncountable[uncountable[i].toLowerCase()] = true; + } +} - this.simulateRemoteCall(function() { - this.didCreateRecord(store, type, record, fixture); - }, this); - }, +function loadIrregular(rules, irregularPairs) { + var pair; - /** - @method updateRecord - @param store - @param type - @param record - */ - updateRecord: function(store, type, record) { - var fixture = this.mockJSON(type, record); + for (var i = 0, length = irregularPairs.length; i < length; i++) { + pair = irregularPairs[i]; - this.updateFixtures(type, fixture); + rules.irregular[pair[0].toLowerCase()] = pair[1]; + rules.irregularInverse[pair[1].toLowerCase()] = pair[0]; + } +} - this.simulateRemoteCall(function() { - this.didUpdateRecord(store, type, record, fixture); - }, this); - }, +/** + Inflector.Ember provides a mechanism for supplying inflection rules for your + application. Ember includes a default set of inflection rules, and provides an + API for providing additional rules. - /** - @method deleteRecord - @param store - @param type - @param record - */ - deleteRecord: function(store, type, record) { - var fixture = this.mockJSON(type, record); + Examples: - this.deleteLoadedFixture(type, fixture); + Creating an inflector with no rules. - this.simulateRemoteCall(function() { - this.didDeleteRecord(store, type, record); - }, this); - }, + ```js + var inflector = new Ember.Inflector(); + ``` - /* - @method deleteLoadedFixture - @private - @param type - @param record - */ - deleteLoadedFixture: function(type, record) { - var existingFixture = this.findExistingFixture(type, record); + Creating an inflector with the default ember ruleset. - if(existingFixture) { - var index = indexOf(type.FIXTURES, existingFixture); - type.FIXTURES.splice(index, 1); - return true; - } - }, + ```js + var inflector = new Ember.Inflector(Ember.Inflector.defaultRules); - /* - @method findExistingFixture - @private - @param type - @param record - */ - findExistingFixture: function(type, record) { - var fixtures = this.fixturesForType(type); - var id = this.extractId(type, record); + inflector.pluralize('cow') //=> 'kine' + inflector.singularize('kine') //=> 'cow' + ``` - return this.findFixtureById(fixtures, id); - }, + Creating an inflector and adding rules later. - /* - @method findFixtureById - @private - @param type - @param record - */ - findFixtureById: function(fixtures, id) { - return Ember.A(fixtures).find(function(r) { - if(''+get(r, 'id') === ''+id) { - return true; - } else { - return false; - } - }); - }, + ```javascript + var inflector = Ember.Inflector.inflector; - /* - @method simulateRemoteCall - @private - @param callback - @param context - */ - simulateRemoteCall: function(callback, context) { - if (get(this, 'simulateRemoteResponse')) { - // Schedule with setTimeout - Ember.run.later(context, callback, get(this, 'latency')); - } else { - // Asynchronous, but at the of the runloop with zero latency - Ember.run.once(context, callback); - } - } -}); + inflector.pluralize('advice') // => 'advices' + inflector.uncountable('advice'); + inflector.pluralize('advice') // => 'advice' -})(); + inflector.pluralize('formula') // => 'formulas' + inflector.irregular('formula', 'formulae'); + inflector.pluralize('formula') // => 'formulae' + // you would not need to add these as they are the default rules + inflector.plural(/$/, 's'); + inflector.singular(/s$/i, ''); + ``` + Creating an inflector with a nondefault ruleset. -(function() { -/** - @module ember-data -*/ + ```javascript + var rules = { + plurals: [ /$/, 's' ], + singular: [ /\s$/, '' ], + irregularPairs: [ + [ 'cow', 'kine' ] + ], + uncountable: [ 'fish' ] + }; -var get = Ember.get; + var inflector = new Ember.Inflector(rules); + ``` -/** - @class RESTSerializer - @namespace DS - @extends DS.Serializer + @class Inflector + @namespace Ember */ -DS.RESTSerializer = DS.JSONSerializer.extend({ +function Inflector(ruleSet) { + ruleSet = ruleSet || {}; + ruleSet.uncountable = ruleSet.uncountable || {}; + ruleSet.irregularPairs = ruleSet.irregularPairs || {}; + + var rules = this.rules = { + plurals: ruleSet.plurals || [], + singular: ruleSet.singular || [], + irregular: {}, + irregularInverse: {}, + uncountable: {} + }; + + loadUncountable(rules, ruleSet.uncountable); + loadIrregular(rules, ruleSet.irregularPairs); +} +Inflector.prototype = { /** - @method keyForAttributeName - @param type - @param name + @method plural + @param {RegExp} regex + @param {String} string */ - keyForAttributeName: function(type, name) { - return Ember.String.decamelize(name); + plural: function(regex, string) { + this.rules.plurals.push([regex, string.toLowerCase()]); }, /** - @method keyForBelongsTo - @param type - @param name + @method singular + @param {RegExp} regex + @param {String} string */ - keyForBelongsTo: function(type, name) { - var key = this.keyForAttributeName(type, name); - - if (this.embeddedType(type, name)) { - return key; - } - - return key + "_id"; + singular: function(regex, string) { + this.rules.singular.push([regex, string.toLowerCase()]); }, /** - @method keyForHasMany - @param type - @param name + @method uncountable + @param {String} regex */ - keyForHasMany: function(type, name) { - var key = this.keyForAttributeName(type, name); - - if (this.embeddedType(type, name)) { - return key; - } - - return this.singularize(key) + "_ids"; + uncountable: function(string) { + loadUncountable(this.rules, [string.toLowerCase()]); }, /** - @method keyForPolymorphicId - @param key + @method irregular + @param {String} singular + @param {String} plural */ - keyForPolymorphicId: function(key) { - return key; + irregular: function (singular, plural) { + loadIrregular(this.rules, [[singular, plural]]); }, /** - @method keyForPolymorphicType - @param key + @method pluralize + @param {String} word */ - keyForPolymorphicType: function(key) { - return key.replace(/_id$/, '_type'); + pluralize: function(word) { + return this.inflect(word, this.rules.plurals, this.rules.irregular); }, /** - @method extractValidationErrors - @param type - @param json + @method singularize + @param {String} word */ - extractValidationErrors: function(type, json) { - var errors = {}; - - get(type, 'attributes').forEach(function(name) { - var key = this._keyForAttributeName(type, name); - if (json['errors'].hasOwnProperty(key)) { - errors[name] = json['errors'][key]; - } - }, this); + singularize: function(word) { + return this.inflect(word, this.rules.singular, this.rules.irregularInverse); + }, - return errors; - } -}); + /** + @protected -})(); + @method inflect + @param {String} word + @param {Object} typeRules + @param {Object} irregular + */ + inflect: function(word, typeRules, irregular) { + var inflection, substitution, result, lowercase, isBlank, + isUncountable, isIrregular, isIrregularInverse, rule; + isBlank = BLANK_REGEX.test(word); + if (isBlank) { + return word; + } -(function() { -/** - @module ember-data -*/ + lowercase = word.toLowerCase(); -var get = Ember.get, set = Ember.set; + isUncountable = this.rules.uncountable[lowercase]; -DS.rejectionHandler = function(reason) { - Ember.Logger.assert([reason, reason.message, reason.stack]); + if (isUncountable) { + return word; + } - throw reason; -}; + isIrregular = irregular && irregular[lowercase]; -/** - The REST adapter allows your store to communicate with an HTTP server by - transmitting JSON via XHR. Most Ember.js apps that consume a JSON API - should use the REST adapter. + if (isIrregular) { + return isIrregular; + } - This adapter is designed around the idea that the JSON exchanged with - the server should be conventional. + for (var i = typeRules.length, min = 0; i > min; i--) { + inflection = typeRules[i-1]; + rule = inflection[0]; - ## JSON Structure + if (rule.test(word)) { + break; + } + } - The REST adapter expects the JSON returned from your server to follow - these conventions. + inflection = inflection || []; - ### Object Root + rule = inflection[0]; + substitution = inflection[1]; - The JSON payload should be an object that contains the record inside a - root property. For example, in response to a `GET` request for - `/posts/1`, the JSON should look like this: + result = word.replace(rule, substitution); - ```js - { - "post": { - title: "I'm Running to Reform the W3C's Tag", - author: "Yehuda Katz" - } + return result; } - ``` +}; - ### Conventional Names +Ember.Inflector = Inflector; - Attribute names in your JSON payload should be the underscored versions of - the attributes in your Ember.js models. +})(); - For example, if you have a `Person` model: - ```js - App.Person = DS.Model.extend({ - firstName: DS.attr('string'), - lastName: DS.attr('string'), - occupation: DS.attr('string') - }); - ``` - The JSON returned should look like this: +(function() { +Ember.Inflector.defaultRules = { + plurals: [ + [/$/, 's'], + [/s$/i, 's'], + [/^(ax|test)is$/i, '$1es'], + [/(octop|vir)us$/i, '$1i'], + [/(octop|vir)i$/i, '$1i'], + [/(alias|status)$/i, '$1es'], + [/(bu)s$/i, '$1ses'], + [/(buffal|tomat)o$/i, '$1oes'], + [/([ti])um$/i, '$1a'], + [/([ti])a$/i, '$1a'], + [/sis$/i, 'ses'], + [/(?:([^f])fe|([lr])f)$/i, '$1$2ves'], + [/(hive)$/i, '$1s'], + [/([^aeiouy]|qu)y$/i, '$1ies'], + [/(x|ch|ss|sh)$/i, '$1es'], + [/(matr|vert|ind)(?:ix|ex)$/i, '$1ices'], + [/^(m|l)ouse$/i, '$1ice'], + [/^(m|l)ice$/i, '$1ice'], + [/^(ox)$/i, '$1en'], + [/^(oxen)$/i, '$1'], + [/(quiz)$/i, '$1zes'] + ], + + singular: [ + [/s$/i, ''], + [/(ss)$/i, '$1'], + [/(n)ews$/i, '$1ews'], + [/([ti])a$/i, '$1um'], + [/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '$1sis'], + [/(^analy)(sis|ses)$/i, '$1sis'], + [/([^f])ves$/i, '$1fe'], + [/(hive)s$/i, '$1'], + [/(tive)s$/i, '$1'], + [/([lr])ves$/i, '$1f'], + [/([^aeiouy]|qu)ies$/i, '$1y'], + [/(s)eries$/i, '$1eries'], + [/(m)ovies$/i, '$1ovie'], + [/(x|ch|ss|sh)es$/i, '$1'], + [/^(m|l)ice$/i, '$1ouse'], + [/(bus)(es)?$/i, '$1'], + [/(o)es$/i, '$1'], + [/(shoe)s$/i, '$1'], + [/(cris|test)(is|es)$/i, '$1is'], + [/^(a)x[ie]s$/i, '$1xis'], + [/(octop|vir)(us|i)$/i, '$1us'], + [/(alias|status)(es)?$/i, '$1'], + [/^(ox)en/i, '$1'], + [/(vert|ind)ices$/i, '$1ex'], + [/(matr)ices$/i, '$1ix'], + [/(quiz)zes$/i, '$1'], + [/(database)s$/i, '$1'] + ], + + irregularPairs: [ + ['person', 'people'], + ['man', 'men'], + ['child', 'children'], + ['sex', 'sexes'], + ['move', 'moves'], + ['cow', 'kine'], + ['zombie', 'zombies'] + ], + + uncountable: [ + 'equipment', + 'information', + 'rice', + 'money', + 'species', + 'series', + 'fish', + 'sheep', + 'jeans', + 'police' + ] +}; - ```js - { - "person": { - "first_name": "Barack", - "last_name": "Obama", - "occupation": "President" - } - } - ``` +})(); - ## Customization - ### Endpoint path customization - Endpoint paths can be prefixed with a `namespace` by setting the namespace - property on the adapter: +(function() { +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { + /** + See {{#crossLink "Ember.String/pluralize"}}{{/crossLink}} - ```js - DS.RESTAdapter.reopen({ - namespace: 'api/1' - }); - ``` - Requests for `App.Person` would now target `/api/1/people/1`. + @method pluralize + @for String + */ + String.prototype.pluralize = function() { + return Ember.String.pluralize(this); + }; - ### Host customization + /** + See {{#crossLink "Ember.String/singularize"}}{{/crossLink}} - An adapter can target other hosts by setting the `url` property. + @method singularize + @for String + */ + String.prototype.singularize = function() { + return Ember.String.singularize(this); + }; +} - ```js - DS.RESTAdapter.reopen({ - url: 'https://api.example.com' - }); - ``` +})(); - ### Headers customization - Some APIs require HTTP headers, eg to provide an API key. An array of - headers can be added to the adapter which are passed with every request: - ```js - DS.RESTAdapter.reopen({ - headers: { - "API_KEY": "secret key", - "ANOTHER_HEADER": "asdsada" - } - }); - ``` +(function() { +Ember.Inflector.inflector = new Ember.Inflector(Ember.Inflector.defaultRules); - @class RESTAdapter - @constructor - @namespace DS - @extends DS.Adapter -*/ -DS.RESTAdapter = DS.Adapter.extend({ - namespace: null, - bulkCommit: false, - since: 'since', +})(); - serializer: DS.RESTSerializer, - /** - Called on each record before saving. If false is returned, the record - will not be saved. - By default, this method returns `true` except when the record is embedded. +(function() { - @method shouldSave - @property {DS.Model} record - @return {Boolean} `true` to save, `false` to not. Defaults to true. - */ - shouldSave: function(record) { - var reference = get(record, '_reference'); +})(); - return !reference.parent; - }, +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + camelize = Ember.String.camelize, + capitalize = Ember.String.capitalize, + decamelize = Ember.String.decamelize, + singularize = Ember.String.singularize, + underscore = Ember.String.underscore; + +DS.ActiveModelSerializer = DS.RESTSerializer.extend({ + // SERIALIZE /** - @method dirtyRecordsForRecordChange - @param {Ember.OrderedSet} dirtySet - @param {DS.Model} record + Converts camelcased attributes to underscored when serializing. + + @method keyForAttribute + @param {String} attribute + @returns String */ - dirtyRecordsForRecordChange: function(dirtySet, record) { - this._dirtyTree(dirtySet, record); + keyForAttribute: function(attr) { + return decamelize(attr); }, /** - @method dirtyRecordsForHasManyChange - @param {Ember.OrderedSet} dirtySet - @param {DS.Model} record - @param {DS.RelationshipChange} relationship - */ - dirtyRecordsForHasManyChange: function(dirtySet, record, relationship) { - var embeddedType = get(this, 'serializer').embeddedType(record.constructor, relationship.secondRecordName); + Underscores relationship names and appends "_id" or "_ids" when serializing + relationship keys. - if (embeddedType === 'always') { - relationship.childReference.parent = relationship.parentReference; - this._dirtyTree(dirtySet, record); + @method keyForRelationship + @param {String} key + @param {String} kind + @returns String + */ + keyForRelationship: function(key, kind) { + key = decamelize(key); + if (kind === "belongsTo") { + return key + "_id"; + } else if (kind === "hasMany") { + return singularize(key) + "_ids"; + } else { + return key; } }, /** - @method _dirtyTree - @private - @param {Ember.OrderedSet} dirtySet - @param {DS.Model} record + Does not serialize hasMany relationships by default. */ - _dirtyTree: function(dirtySet, record) { - dirtySet.add(record); - - get(this, 'serializer').eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { - if (embeddedType !== 'always') { return; } - if (dirtySet.has(embeddedRecord)) { return; } - this._dirtyTree(dirtySet, embeddedRecord); - }, this); + serializeHasMany: Ember.K, - var reference = record.get('_reference'); + /** + Underscores the JSON root keys when serializing. - if (reference.parent) { - var store = get(record, 'store'); - var parent = store.recordForReference(reference.parent); - this._dirtyTree(dirtySet, parent); - } + @method serializeIntoHash + @param {Object} hash + @param {subclass of DS.Model} type + @param {DS.Model} record + @param {Object} options + */ + serializeIntoHash: function(data, type, record, options) { + var root = underscore(decamelize(type.typeKey)); + data[root] = this.serialize(record, options); }, /** - Serializes the record and sends it to the server. + Serializes a polymorphic type as a fully capitalized model name. - By default, the record is serialized with the adapter's `serialize` - method and assigned to a root obtained by the `rootForType` method. + @method serializePolymorphicType + @param {DS.Model} record + @param {Object} json + @param relationship + */ + serializePolymorphicType: function(record, json, relationship) { + var key = relationship.key, + belongsTo = get(record, key); + key = this.keyForAttribute(key); + json[key + "_type"] = capitalize(camelize(belongsTo.constructor.typeKey)); + }, - The url is created with `buildURL` and then called as a 'POST' request - with the adapter's `ajax` method. + // EXTRACT - If successful, the adapter's `didCreateRecord` method is called, - otherwise `didError` + /** + Extracts the model typeKey from underscored root objects. - @method createRecord - @property {DS.Store} store - @property {DS.Model} type the DS.Model class of the record - @property {DS.Model} record + @method typeForRoot + @param {String} root + @returns String the model's typeKey */ - createRecord: function(store, type, record) { - var root = this.rootForType(type); - var adapter = this; - var data = {}; - - data[root] = this.serialize(record, { includeId: true }); - - return this.ajax(this.buildURL(root), "POST", { - data: data - }).then(function(json){ - adapter.didCreateRecord(store, type, record, json); - }, function(xhr) { - adapter.didError(store, type, record, xhr); - throw xhr; - }).then(null, DS.rejectionHandler); + typeForRoot: function(root) { + var camelized = camelize(root); + return singularize(camelized); }, /** - @method createRecords - @param store - @param type - @param records - */ - createRecords: function(store, type, records) { - var adapter = this; + Add extra step to `DS.RESTSerializer.normalize` so links are + normalized. + + If your payload looks like this - if (get(this, 'bulkCommit') === false) { - return this._super(store, type, records); + ```js + { + "post": { + "id": 1, + "title": "Rails is omakase", + "links": { "flagged_comments": "api/comments/flagged" } + } + } + ``` + The normalized version would look like this + + ```js + { + "post": { + "id": 1, + "title": "Rails is omakase", + "links": { "flaggedComments": "api/comments/flagged" } + } } + ``` - var root = this.rootForType(type), - plural = this.pluralize(root); + @method normalize + @param {subclass of DS.Model} type + @param {Object} hash + @param {String} prop + @returns Object + */ - var data = {}; - data[plural] = []; - records.forEach(function(record) { - data[plural].push(this.serialize(record, { includeId: true })); - }, this); + normalize: function(type, hash, prop) { + this.normalizeLinks(hash); - return this.ajax(this.buildURL(root), "POST", { - data: data - }).then(function(json) { - adapter.didCreateRecords(store, type, records, json); - }).then(null, DS.rejectionHandler); + return this._super(type, hash, prop); }, /** - @method updateRecord - @param store - @param type - @param record + Convert `snake_cased` links to `camelCase` + + @method normalizeLinks + @param {Object} hash */ - updateRecord: function(store, type, record) { - var id, root, adapter, data; - id = get(record, 'id'); - root = this.rootForType(type); - adapter = this; + normalizeLinks: function(data){ + if (data.links) { + var links = data.links; - data = {}; - data[root] = this.serialize(record); + for (var link in links) { + var camelizedLink = camelize(link); - return this.ajax(this.buildURL(root, id, record), "PUT",{ - data: data - }).then(function(json){ - adapter.didUpdateRecord(store, type, record, json); - }, function(xhr) { - adapter.didError(store, type, record, xhr); - throw xhr; - }).then(null, DS.rejectionHandler); + if (camelizedLink !== link) { + links[camelizedLink] = links[link]; + delete links[link]; + } + } + } }, /** - @method updateRecords - @param store - @param type - @param records - */ - updateRecords: function(store, type, records) { - var root, plural, adapter, data; + Normalize the polymorphic type from the JSON. - if (get(this, 'bulkCommit') === false) { - return this._super(store, type, records); - } + Normalize: + ```js + { + id: "1" + minion: { type: "evil_minion", id: "12"} + } + ``` - root = this.rootForType(type); - plural = this.pluralize(root); - adapter = this; + To: + ```js + { + id: "1" + minion: { type: "evilMinion", id: "12"} + } + ``` - data = {}; + @method normalizeRelationships + @private + */ + normalizeRelationships: function(type, hash) { + var payloadKey, payload; + + if (this.keyForRelationship) { + type.eachRelationship(function(key, relationship) { + if (relationship.options.polymorphic) { + payloadKey = this.keyForAttribute(key); + payload = hash[payloadKey]; + if (payload && payload.type) { + payload.type = this.typeForRoot(payload.type); + } else if (payload && relationship.kind === "hasMany") { + var self = this; + forEach(payload, function(single) { + single.type = self.typeForRoot(single.type); + }); + } + } else { + payloadKey = this.keyForRelationship(key, relationship.kind); + payload = hash[payloadKey]; + } - data[plural] = []; + hash[key] = payload; - records.forEach(function(record) { - data[plural].push(this.serialize(record, { includeId: true })); - }, this); + if (key !== payloadKey) { + delete hash[payloadKey]; + } + }, this); + } + } +}); - return this.ajax(this.buildURL(root, "bulk"), "PUT", { - data: data - }).then(function(json) { - adapter.didUpdateRecords(store, type, records, json); - }).then(null, DS.rejectionHandler); - }, +})(); - /** - @method deleteRecord - @param store - @param type - @param record - */ - deleteRecord: function(store, type, record) { - var id, root, adapter; - id = get(record, 'id'); - root = this.rootForType(type); - adapter = this; - return this.ajax(this.buildURL(root, id, record), "DELETE").then(function(json){ - adapter.didDeleteRecord(store, type, record, json); - }, function(xhr){ - adapter.didError(store, type, record, xhr); - throw xhr; - }).then(null, DS.rejectionHandler); - }, +(function() { +var get = Ember.get; +var forEach = Ember.EnumerableUtils.forEach; - /** - @method deleteRecords - @param store - @param type - @param records - */ - deleteRecords: function(store, type, records) { - var root, plural, serializer, adapter, data; +/** + The EmbeddedRecordsMixin allows you to add embedded record support to your + serializers. + To set up embedded records, you include the mixin into the serializer and then + define your embedded relations. - if (get(this, 'bulkCommit') === false) { - return this._super(store, type, records); + ```js + App.PostSerializer = DS.ActiveModelSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + comments: {embedded: 'always'} } + }) + ``` - root = this.rootForType(type); - plural = this.pluralize(root); - serializer = get(this, 'serializer'); - adapter = this; - - data = {}; - - data[plural] = []; - records.forEach(function(record) { - data[plural].push(serializer.serializeId( get(record, 'id') )); - }); + Currently only `{embedded: 'always'}` records are supported. - return this.ajax(this.buildURL(root, 'bulk'), "DELETE", { - data: data - }).then(function(json){ - adapter.didDeleteRecords(store, type, records, json); - }).then(null, DS.rejectionHandler); - }, + @class EmbeddedRecordsMixin + @namespace DS +*/ +DS.EmbeddedRecordsMixin = Ember.Mixin.create({ /** - @method find - @param store - @param type - @param id - */ - find: function(store, type, id) { - var root = this.rootForType(type), adapter = this; - - return this.ajax(this.buildURL(root, id), "GET"). - then(function(json){ - adapter.didFindRecord(store, type, json, id); - }).then(null, DS.rejectionHandler); - }, + Serialize has-may relationship when it is configured as embedded objects. - /** - @method findAll - @param store - @param type - @param since + @method serializeHasMany */ - findAll: function(store, type, since) { - var root, adapter; + serializeHasMany: function(record, json, relationship) { + var key = relationship.key, + attrs = get(this, 'attrs'), + embed = attrs && attrs[key] && attrs[key].embedded === 'always'; + + if (embed) { + json[this.keyForAttribute(key)] = get(record, key).map(function(relation) { + var data = relation.serialize(), + primaryKey = get(this, 'primaryKey'); - root = this.rootForType(type); - adapter = this; + data[primaryKey] = get(relation, primaryKey); - return this.ajax(this.buildURL(root), "GET",{ - data: this.sinceQuery(since) - }).then(function(json) { - adapter.didFindAll(store, type, json); - }).then(null, DS.rejectionHandler); + return data; + }, this); + } }, /** - @method findQuery - @param store - @param type - @param query - @param recordArray + Extract embedded objects out of the payload for a single object + and add them as sideloaded objects instead. + + @method extractSingle */ - findQuery: function(store, type, query, recordArray) { - var root = this.rootForType(type), - adapter = this; + extractSingle: function(store, primaryType, payload, recordId, requestType) { + var root = this.keyForAttribute(primaryType.typeKey), + partial = payload[root]; - return this.ajax(this.buildURL(root), "GET", { - data: query - }).then(function(json){ - adapter.didFindQuery(store, type, json, recordArray); - }).then(null, DS.rejectionHandler); + updatePayloadWithEmbedded(store, this, primaryType, partial, payload); + + return this._super(store, primaryType, payload, recordId, requestType); }, /** - @method findMany - @param store - @param type - @param ids - @param owner + Extract embedded objects out of a standard payload + and add them as sideloaded objects instead. + + @method extractArray */ - findMany: function(store, type, ids, owner) { - var root = this.rootForType(type), - adapter = this; + extractArray: function(store, type, payload) { + var root = this.keyForAttribute(type.typeKey), + partials = payload[Ember.String.pluralize(root)]; - ids = this.serializeIds(ids); + forEach(partials, function(partial) { + updatePayloadWithEmbedded(store, this, type, partial, payload); + }, this); - return this.ajax(this.buildURL(root), "GET", { - data: {ids: ids} - }).then(function(json) { - adapter.didFindMany(store, type, json); - }).then(null, DS.rejectionHandler); - }, + return this._super(store, type, payload); + } +}); - /** - This method serializes a list of IDs using `serializeId` +function updatePayloadWithEmbedded(store, serializer, type, partial, payload) { + var attrs = get(serializer, 'attrs'); - @method serializeIds - @private - @param ids - @return {Array} an array of serialized IDs - */ - serializeIds: function(ids) { - var serializer = get(this, 'serializer'); + if (!attrs) { + return; + } - return Ember.EnumerableUtils.map(ids, function(id) { - return serializer.serializeId(id); - }); - }, + type.eachRelationship(function(key, relationship) { + var expandedKey, embeddedTypeKey, attribute, ids, + config = attrs[key], + serializer = store.serializerFor(relationship.type.typeKey), + primaryKey = get(serializer, "primaryKey"); - /** - @method didError - @private - @param store - @param type - @param record - @param xhr - */ - didError: function(store, type, record, xhr) { - if (xhr.status === 422) { - var json = JSON.parse(xhr.responseText), - serializer = get(this, 'serializer'), - errors = serializer.extractValidationErrors(type, json); - - store.recordWasInvalid(record, errors); - } else { - this._super.apply(this, arguments); + if (relationship.kind !== "hasMany") { + return; } - }, - - /** - @method ajax - @private - @param url - @param type - @param hash - */ - ajax: function(url, type, hash) { - var adapter = this; - return new Ember.RSVP.Promise(function(resolve, reject) { - hash = hash || {}; - hash.url = url; - hash.type = type; - hash.dataType = 'json'; - hash.context = adapter; - - if (hash.data && type !== 'GET') { - hash.contentType = 'application/json; charset=utf-8'; - hash.data = JSON.stringify(hash.data); - } + if (config && (config.embedded === 'always' || config.embedded === 'load')) { + // underscore forces the embedded records to be side loaded. + // it is needed when main type === relationship.type + embeddedTypeKey = '_' + Ember.String.pluralize(relationship.type.typeKey); + expandedKey = this.keyForRelationship(key, relationship.kind); + attribute = this.keyForAttribute(key); + ids = []; - if (adapter.headers !== undefined) { - var headers = adapter.headers; - hash.beforeSend = function (xhr) { - Ember.keys(headers).forEach(function(key) { - xhr.setRequestHeader(key, headers[key]); - }); - }; + if (!partial[attribute]) { + return; } - hash.success = function(json) { - Ember.run(null, resolve, json); - }; + payload[embeddedTypeKey] = payload[embeddedTypeKey] || []; - hash.error = function(jqXHR, textStatus, errorThrown) { - if (jqXHR) { - jqXHR.then = null; - } + forEach(partial[attribute], function(data) { + var embeddedType = store.modelFor(relationship.type.typeKey); + updatePayloadWithEmbedded(store, serializer, embeddedType, data, payload); + ids.push(data[primaryKey]); + payload[embeddedTypeKey].push(data); + }); - Ember.run(null, reject, jqXHR); - }; + partial[expandedKey] = ids; + delete partial[attribute]; + } + }, serializer); +} +})(); - Ember.$.ajax(hash); - }); - }, - /** - @property url - @default '' - */ - url: "", - /** - @method rootForType - @private - @param type - */ - rootForType: function(type) { - var serializer = get(this, 'serializer'); - return serializer.rootForType(type); - }, +(function() { +/** + @module ember-data +*/ - /** - @method pluralize - @private - @param string - */ - pluralize: function(string) { - var serializer = get(this, 'serializer'); - return serializer.pluralize(string); - }, +var forEach = Ember.EnumerableUtils.forEach; +var decamelize = Ember.String.decamelize, + underscore = Ember.String.underscore, + pluralize = Ember.String.pluralize; - /** - @method buildURL - @private - @param root - @param suffix - @param record - */ - buildURL: function(root, suffix, record) { - var url = [this.url]; +/** + The ActiveModelAdapter is a subclass of the RESTAdapter designed to integrate + with a JSON API that uses an underscored naming convention instead of camelcasing. + It has been designed to work out of the box with the + [active_model_serializers](http://github.com/rails-api/active_model_serializers) + Ruby gem. - Ember.assert("Namespace URL (" + this.namespace + ") must not start with slash", !this.namespace || this.namespace.toString().charAt(0) !== "/"); - Ember.assert("Root URL (" + root + ") must not start with slash", !root || root.toString().charAt(0) !== "/"); - Ember.assert("URL suffix (" + suffix + ") must not start with slash", !suffix || suffix.toString().charAt(0) !== "/"); + This adapter extends the DS.RESTAdapter by making consistent use of the camelization, + decamelization and pluralization methods to normalize the serialized JSON into a + format that is compatible with a conventional Rails backend and Ember Data. - if (!Ember.isNone(this.namespace)) { - url.push(this.namespace); - } + ## JSON Structure - url.push(this.pluralize(root)); - if (suffix !== undefined) { - url.push(suffix); - } + The ActiveModelAdapter expects the JSON returned from your server to follow + the REST adapter conventions substituting underscored keys for camelcased ones. - return url.join("/"); - }, + ### Conventional Names - /** - @method sinceQuery - @private - @param since - */ - sinceQuery: function(since) { - var query = {}; - query[get(this, 'since')] = since; - return since ? query : null; - } -}); + Attribute names in your JSON payload should be the underscored versions of + the attributes in your Ember.js models. -})(); + For example, if you have a `Person` model: + ```js + App.FamousPerson = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + occupation: DS.attr('string') + }); + ``` + The JSON returned should look like this: -(function() { -/** - @module ember-data -*/ + ```js + { + "famous_person": { + "first_name": "Barack", + "last_name": "Obama", + "occupation": "President" + } + } + ``` -})(); + @class ActiveModelAdapter + @constructor + @namespace DS + @extends DS.Adapter +**/ +DS.ActiveModelAdapter = DS.RESTAdapter.extend({ + defaultSerializer: '-active-model', + /** + The ActiveModelAdapter overrides the `pathForType` method to build + underscored URLs by decamelizing and pluralizing the object type name. + ```js + this.pathForType("famousPerson"); + //=> "famous_people" + ``` -(function() { -DS.Model.reopen({ + @method pathForType + @param {String} type + @returns String + */ + pathForType: function(type) { + var decamelized = decamelize(type); + var underscored = underscore(decamelized); + return pluralize(underscored); + }, /** - Provides info about the model for debugging purposes - by grouping the properties into more semantic groups. + The ActiveModelAdapter overrides the `ajaxError` method + to return a DS.InvalidError for all 422 Unprocessable Entity + responses. - Meant to be used by debugging tools such as the Chrome Ember Extension. + A 422 HTTP response from the server generally implies that the request + was well formed but the API was unable to process it because the + content was not semantically correct or meaningful per the API. - - Groups all attributes in "Attributes" group. - - Groups all belongsTo relationships in "Belongs To" group. - - Groups all hasMany relationships in "Has Many" group. - - Groups all flags in "Flags" group. - - Flags relationship CPs as expensive properties. + For more information on 422 HTTP Error code see 11.2 WebDAV RFC 4918 + https://tools.ietf.org/html/rfc4918#section-11.2 + + @method ajaxError + @param jqXHR + @returns error */ - _debugInfo: function() { - var attributes = ['id'], - relationships = { belongsTo: [], hasMany: [] }, - expensiveProperties = []; + ajaxError: function(jqXHR) { + var error = this._super(jqXHR); - this.eachAttribute(function(name, meta) { - attributes.push(name); - }, this); + if (jqXHR && jqXHR.status === 422) { + var response = Ember.$.parseJSON(jqXHR.responseText), + errors = {}; - this.eachRelationship(function(name, relationship) { - relationships[relationship.kind].push(name); - expensiveProperties.push(name); - }); + if (response.errors !== undefined) { + var jsonErrors = response.errors; - var groups = [ - { - name: 'Attributes', - properties: attributes, - expand: true, - }, - { - name: 'Belongs To', - properties: relationships.belongsTo, - expand: true - }, - { - name: 'Has Many', - properties: relationships.hasMany, - expand: true - }, - { - name: 'Flags', - properties: ['isLoaded', 'isDirty', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid'] + forEach(Ember.keys(jsonErrors), function(key) { + errors[Ember.String.camelize(key)] = jsonErrors[key]; + }); } - ]; - return { - propertyInfo: { - // include all other mixins / properties (not just the grouped ones) - includeOtherProperties: true, - groups: groups, - // don't pre-calculate unless cached - expensiveProperties: expensiveProperties - } - }; + return new DS.InvalidError(errors); + } else { + return error; + } } - }); })(); @@ -10160,41 +10585,34 @@ DS.Model.reopen({ (function() { -/** - @module ember-data -*/ })(); (function() { -//Copyright (C) 2011 by Living Social, Inc. - -//Permission is hereby granted, free of charge, to any person obtaining a copy of -//this software and associated documentation files (the "Software"), to deal in -//the Software without restriction, including without limitation the rights to -//use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -//of the Software, and to permit persons to whom the Software is furnished to do -//so, subject to the following conditions: - -//The above copyright notice and this permission notice shall be included in all -//copies or substantial portions of the Software. - -//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -//SOFTWARE. +Ember.onLoad('Ember.Application', function(Application) { + Application.initializer({ + name: "activeModelAdapter", -/** - Ember Data + initialize: function(container, application) { + var proxy = new DS.ContainerProxy(container); + proxy.registerDeprecations([ + {deprecated: 'serializer:_ams', valid: 'serializer:-active-model'}, + {deprecated: 'adapter:_ams', valid: 'adapter:-active-model'} + ]); - @module ember-data - @main ember-data -*/ + application.register('serializer:-active-model', DS.ActiveModelSerializer); + application.register('adapter:-active-model', DS.ActiveModelAdapter); + } + }); +}); + +})(); + + + +(function() { })(); diff --git a/packages/ember.js b/packages/ember.js index 43bc9af..740a41b 100644 --- a/packages/ember.js +++ b/packages/ember.js @@ -1,5 +1,12 @@ -// Version: v1.0.0 -// Last commit: e2ea0cf (2013-08-31 23:47:39 -0700) +/*! + * @overview Ember - JavaScript Application Framework + * @copyright Copyright 2011-2014 Tilde Inc. and contributors + * Portions Copyright 2006-2011 Strobe Inc. + * Portions Copyright 2008-2011 Apple Inc. All rights reserved. + * @license Licensed under MIT license + * See https://raw.github.com/emberjs/ember.js/master/LICENSE + * @version 1.4.0 + */ (function() { @@ -24,7 +31,20 @@ if ('undefined' === typeof Ember) { } } -Ember.ENV = 'undefined' === typeof ENV ? {} : ENV; +// This needs to be kept in sync with the logic in +// `packages/ember-metal/lib/core.js`. +// +// This is duplicated here to ensure that `Ember.ENV` +// is setup even if `Ember` is not loaded yet. +if (Ember.ENV) { + // do nothing if Ember.ENV is already setup +} else if ('undefined' !== typeof EmberENV) { + Ember.ENV = EmberENV; +} else if('undefined' !== typeof ENV) { + Ember.ENV = ENV; +} else { + Ember.ENV = {}; +} if (!('MANDATORY_SETTER' in Ember.ENV)) { Ember.ENV.MANDATORY_SETTER = true; // default to true for debug dist @@ -50,12 +70,7 @@ if (!('MANDATORY_SETTER' in Ember.ENV)) { */ Ember.assert = function(desc, test) { if (!test) { - Ember.Logger.assert(test, desc); - } - - if (Ember.testing && !test) { - // when testing, ensure test failures when assertions fail - throw new Error("Assertion Failed: " + desc); + throw new Ember.Error("Assertion Failed: " + desc); } }; @@ -107,7 +122,7 @@ Ember.deprecate = function(message, test) { if (arguments.length === 1) { test = false; } if (test) { return; } - if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); } + if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Ember.Error(message); } var error; @@ -138,15 +153,21 @@ Ember.deprecate = function(message, test) { /** + Alias an old, deprecated method with its new counterpart. + Display a deprecation warning with the provided message and a stack trace - (Chrome and Firefox only) when the wrapped method is called. + (Chrome and Firefox only) when the assigned method is called. Ember build tools will not remove calls to `Ember.deprecateFunc()`, though no warnings will be shown in production. + ```javascript + Ember.oldMethod = Ember.deprecateFunc("Please use the new, updated method", Ember.newMethod); + ``` + @method deprecateFunc @param {String} message A description of the deprecation. - @param {Function} func The function to be deprecated. + @param {Function} func The new function called to replace its deprecated counterpart. @return {Function} a new function that wrapped the original function with a deprecation warning */ Ember.deprecateFunc = function(message, func) { @@ -159,10 +180,21 @@ Ember.deprecateFunc = function(message, func) { // Inform the developer about the Ember Inspector if not installed. if (!Ember.testing) { - if (typeof window !== 'undefined' && window.chrome && window.addEventListener) { + var isFirefox = typeof InstallTrigger !== 'undefined'; + var isChrome = !!window.chrome && !window.opera; + + if (typeof window !== 'undefined' && (isFirefox || isChrome) && window.addEventListener) { window.addEventListener("load", function() { if (document.body && document.body.dataset && !document.body.dataset.emberExtension) { - Ember.debug('For more advanced debugging, install the Ember Inspector from https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi'); + var downloadURL; + + if(isChrome) { + downloadURL = 'https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi'; + } else if(isFirefox) { + downloadURL = 'https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/' + } + + Ember.debug('For more advanced debugging, install the Ember Inspector from ' + downloadURL); } }, false); } @@ -170,12 +202,19 @@ if (!Ember.testing) { })(); -// Version: v1.0.0 -// Last commit: e2ea0cf (2013-08-31 23:47:39 -0700) +/*! + * @overview Ember - JavaScript Application Framework + * @copyright Copyright 2011-2014 Tilde Inc. and contributors + * Portions Copyright 2006-2011 Strobe Inc. + * Portions Copyright 2008-2011 Apple Inc. All rights reserved. + * @license Licensed under MIT license + * See https://raw.github.com/emberjs/ember.js/master/LICENSE + * @version 1.4.0 + */ (function() { -var define, requireModule; +var define, requireModule, require, requirejs; (function() { var registry = {}, seen = {}; @@ -184,36 +223,52 @@ var define, requireModule; registry[name] = { deps: deps, callback: callback }; }; - requireModule = function(name) { + requirejs = require = requireModule = function(name) { + requirejs._eak_seen = registry; + if (seen[name]) { return seen[name]; } seen[name] = {}; - var mod, deps, callback, reified, exports; - - mod = registry[name]; - - if (!mod) { - throw new Error("Module '" + name + "' not found."); + if (!registry[name]) { + throw new Error("Could not find module " + name); } - deps = mod.deps; - callback = mod.callback; - reified = []; + var mod = registry[name], + deps = mod.deps, + callback = mod.callback, + reified = [], + exports; for (var i=0, l=deps.length; i= + * length, then append to the end of the array. + * @param {Number} amt Number of elements that should be remove from the array, + * starting at *idx* + * @param {Array} objects An array of zero or more objects that should be + * inserted into the array at *idx* + * + * @return {Array} The changed array. + */ replace: function(array, idx, amt, objects) { if (array.replace) { return array.replace(idx, amt, objects); @@ -1832,6 +2016,29 @@ var utils = Ember.EnumerableUtils = { } }, + /** + * Calculates the intersection of two arrays. This method returns a new array + * filled with the records that the two passed arrays share with each other. + * If there is no intersection, an empty array will be returned. + * + * ```javascript + * var array1 = [1, 2, 3, 4, 5]; + * var array2 = [1, 3, 5, 6, 7]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [1, 3, 5] + * + * var array1 = [1, 2, 3]; + * var array2 = [4, 5, 6]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [] + * ``` + * + * @method intersection + * @param {Array} array1 The first array + * @param {Array} array2 The second array + * + * @return {Array} The intersection of the two passed arrays. + */ intersection: function(array1, array2) { var intersection = []; @@ -1938,13 +2145,12 @@ if (Ember.config.overrideAccessors) { } /** - @private - Normalizes a target/path pair to reflect that actual target/path that should be observed, etc. This takes into account passing in global property paths (i.e. a path beginning with a captial letter not defined on the target) and * separators. + @private @method normalizeTuple @for Ember @param {Object} target The current target. May be `null`. @@ -1966,7 +2172,7 @@ var normalizeTuple = Ember.normalizeTuple = function(target, path) { } // must return some kind of path to be valid else other things will break. - if (!path || path.length===0) throw new Error('Invalid Path'); + if (!path || path.length===0) throw new Ember.Error('Path cannot be empty'); return [ target, path ]; }; @@ -2007,7 +2213,6 @@ Ember.getWithDefault = function(root, key, defaultValue) { Ember.get = get; -Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); })(); @@ -2194,8 +2399,6 @@ function removeListener(obj, eventName, target, method) { } /** - @private - Suspend listener during callback. This should only be used by the target of the event listener @@ -2203,6 +2406,7 @@ function removeListener(obj, eventName, target, method) { an object might suspend its property change listener while it is setting that property. + @private @method suspendListener @for Ember @param obj @@ -2231,11 +2435,9 @@ function suspendListener(obj, eventName, target, method, callback) { } /** - @private - Suspends multiple listeners during a callback. - + @private @method suspendListeners @for Ember @param obj @@ -2251,6 +2453,7 @@ function suspendListeners(obj, eventNames, target, method, callback) { } var suspendedActions = [], + actionsList = [], eventName, actions, i, l; for (i=0, l=eventNames.length; i 0 || keyName === 'length', - proto = m.proto, - desc = m.descs[keyName]; + var m = obj[META_KEY], + watching = (m && m.watching[keyName] > 0) || keyName === 'length', + proto = m && m.proto, + desc = m && m.descs[keyName]; if (!watching) { return; } if (proto === obj) { return; } @@ -2546,10 +2752,10 @@ Ember.propertyWillChange = propertyWillChange; @return {void} */ function propertyDidChange(obj, keyName) { - var m = metaFor(obj, false), - watching = m.watching[keyName] > 0 || keyName === 'length', - proto = m.proto, - desc = m.descs[keyName]; + var m = obj[META_KEY], + watching = (m && m.watching[keyName] > 0) || keyName === 'length', + proto = m && m.proto, + desc = m && m.descs[keyName]; if (proto === obj) { return; } @@ -2622,7 +2828,7 @@ function chainsWillChange(obj, keyName, m) { } function chainsDidChange(obj, keyName, m, suppressEvents) { - if (!(m.hasOwnProperty('chainWatchers') && + if (!(m && m.hasOwnProperty('chainWatchers') && m.chainWatchers[keyName])) { return; } @@ -2651,6 +2857,7 @@ Ember.overrideChains = function(obj, keyName, m) { /** @method beginPropertyChanges @chainable + @private */ function beginPropertyChanges() { deferred++; @@ -2660,6 +2867,7 @@ Ember.beginPropertyChanges = beginPropertyChanges; /** @method endPropertyChanges + @private */ function endPropertyChanges() { deferred--; @@ -2736,16 +2944,6 @@ var META_KEY = Ember.META_KEY, property is not defined but the object implements the `setUnknownProperty` method then that will be invoked as well. - If you plan to run on IE8 and older browsers then you should use this - method anytime you want to set a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' - are considered private.) - - On all newer browsers, you only need to use this method to set - properties if the property might not be defined on the object and you want - to respect the `setUnknownProperty` handler. Otherwise you can ignore this - method. - @method set @for Ember @param {Object} obj The object to modify. @@ -2792,7 +2990,7 @@ var set = function set(obj, keyName, value, tolerant) { if (value !== currentValue) { Ember.propertyWillChange(obj, keyName); if (MANDATORY_SETTER) { - if (currentValue === undefined && !(keyName in obj)) { + if ((currentValue === undefined && !(keyName in obj)) || !obj.propertyIsEnumerable(keyName)) { Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter } else { meta.values[keyName] = value; @@ -2823,7 +3021,7 @@ function setPath(root, path, value, tolerant) { keyName = path.slice(path.lastIndexOf('.') + 1); // get the first part of the part - path = path.slice(0, path.length-(keyName.length+1)); + path = (path === keyName) ? keyName : path.slice(0, path.length-(keyName.length+1)); // unless the path is this, look up the first part to // get the root @@ -2832,19 +3030,18 @@ function setPath(root, path, value, tolerant) { } if (!keyName || keyName.length === 0) { - throw new Error('You passed an empty path'); + throw new Ember.Error('Property set failed: You passed an empty path'); } if (!root) { if (tolerant) { return; } - else { throw new Error('Object in path '+path+' could not be found or was destroyed.'); } + else { throw new Ember.Error('Property set failed: object in path "'+path+'" could not be found or was destroyed.'); } } return set(root, keyName, value); } Ember.set = set; -Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); /** Error-tolerant form of `Ember.set`. Will not blow up if any part of the @@ -2862,7 +3059,6 @@ Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now support Ember.trySet = function(root, path, value) { return set(root, path, value, true); }; -Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); })(); @@ -3247,6 +3443,148 @@ MapWithDefault.prototype.copy = function() { +(function() { +function consoleMethod(name) { + var consoleObj, logToConsole; + if (Ember.imports.console) { + consoleObj = Ember.imports.console; + } else if (typeof console !== 'undefined') { + consoleObj = console; + } + + var method = typeof consoleObj === 'object' ? consoleObj[name] : null; + + if (method) { + // Older IE doesn't support apply, but Chrome needs it + if (method.apply) { + logToConsole = function() { + method.apply(consoleObj, arguments); + }; + logToConsole.displayName = 'console.' + name; + return logToConsole; + } else { + return function() { + var message = Array.prototype.join.call(arguments, ', '); + method(message); + }; + } + } +} + +function assertPolyfill(test, message) { + if (!test) { + try { + // attempt to preserve the stack + throw new Ember.Error("assertion failed: " + message); + } catch(error) { + setTimeout(function() { + throw error; + }, 0); + } + } +} + +/** + Inside Ember-Metal, simply uses the methods from `imports.console`. + Override this to provide more robust logging functionality. + + @class Logger + @namespace Ember +*/ +Ember.Logger = { + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.log('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method log + @for Ember.Logger + @param {*} arguments + */ + log: consoleMethod('log') || Ember.K, + + /** + Prints the arguments to the console with a warning icon. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.warn('Something happened!'); // "Something happened!" will be printed to the console with a warning icon. + ``` + + @method warn + @for Ember.Logger + @param {*} arguments + */ + warn: consoleMethod('warn') || Ember.K, + + /** + Prints the arguments to the console with an error icon, red text and a stack trace. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.error('Danger! Danger!'); // "Danger! Danger!" will be printed to the console in red text. + ``` + + @method error + @for Ember.Logger + @param {*} arguments + */ + error: consoleMethod('error') || Ember.K, + + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.info('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method info + @for Ember.Logger + @param {*} arguments + */ + info: consoleMethod('info') || Ember.K, + + /** + Logs the arguments to the console in blue text. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.debug('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method debug + @for Ember.Logger + @param {*} arguments + */ + debug: consoleMethod('debug') || consoleMethod('info') || Ember.K, + + /** + If the value passed into `Ember.Logger.assert` is not truthy it will throw an error with a stack trace. + + ```javascript + Ember.Logger.assert(true); // undefined + Ember.Logger.assert(true === false); // Throws an Assertion failed error. + ``` + + @method assert + @for Ember.Logger + @param {Boolean} bool Value to test + */ + assert: consoleMethod('assert') || assertPolyfill +}; + + +})(); + + + (function() { /** @module ember-metal @@ -3291,8 +3629,6 @@ var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { }; /** - @private - NOTE: This is a low-level method used by other parts of the API. You almost never want to call this method directly. Instead you should use `Ember.mixin()` to define new properties. @@ -3326,6 +3662,7 @@ var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { }).property('firstName', 'lastName')); ``` + @private @method defineProperty @for Ember @param {Object} obj the object to define this property on. This may be a prototype. @@ -3362,7 +3699,8 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { } else { obj[keyName] = undefined; // make enumerable } - } else { + + } else { descs[keyName] = undefined; // shadow descriptor in proto if (desc == null) { value = data; @@ -3484,11 +3822,11 @@ var metaFor = Ember.meta, // utils.js MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, o_defineProperty = Ember.platform.defineProperty; -Ember.watchKey = function(obj, keyName) { +Ember.watchKey = function(obj, keyName, meta) { // can't watch length on Array - it is special... if (keyName === 'length' && typeOf(obj) === 'array') { return; } - var m = metaFor(obj), watching = m.watching; + var m = meta || metaFor(obj), watching = m.watching; // activate watching first time if (!watching[keyName]) { @@ -3502,7 +3840,7 @@ Ember.watchKey = function(obj, keyName) { m.values[keyName] = obj[keyName]; o_defineProperty(obj, keyName, { configurable: true, - enumerable: true, + enumerable: obj.propertyIsEnumerable(keyName), set: Ember.MANDATORY_SETTER_FUNCTION, get: Ember.DEFAULT_GETTER_FUNCTION(keyName) }); @@ -3513,8 +3851,8 @@ Ember.watchKey = function(obj, keyName) { }; -Ember.unwatchKey = function(obj, keyName) { - var m = metaFor(obj), watching = m.watching; +Ember.unwatchKey = function(obj, keyName, meta) { + var m = meta || metaFor(obj), watching = m.watching; if (watching[keyName] === 1) { watching[keyName] = 0; @@ -3526,11 +3864,19 @@ Ember.unwatchKey = function(obj, keyName) { if (MANDATORY_SETTER && keyName in obj) { o_defineProperty(obj, keyName, { configurable: true, - enumerable: true, - writable: true, - value: m.values[keyName] + enumerable: obj.propertyIsEnumerable(keyName), + set: function(val) { + // redefine to set as enumerable + o_defineProperty(obj, keyName, { + configurable: true, + writable: true, + enumerable: true, + value: val + }); + delete m.values[keyName]; + }, + get: Ember.DEFAULT_GETTER_FUNCTION(keyName) }); - delete m.values[keyName]; } } else if (watching[keyName] > 1) { watching[keyName]--; @@ -3549,7 +3895,8 @@ var metaFor = Ember.meta, // utils.js warn = Ember.warn, watchKey = Ember.watchKey, unwatchKey = Ember.unwatchKey, - FIRST_KEY = /^([^\.\*]+)/; + FIRST_KEY = /^([^\.\*]+)/, + META_KEY = Ember.META_KEY; function firstKey(path) { return path.match(FIRST_KEY)[0]; @@ -3583,24 +3930,24 @@ function addChainWatcher(obj, keyName, node) { if (!nodes[keyName]) { nodes[keyName] = []; } nodes[keyName].push(node); - watchKey(obj, keyName); + watchKey(obj, keyName, m); } var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) { if (!obj || 'object' !== typeof obj) { return; } // nothing to do - var m = metaFor(obj, false); - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + var m = obj[META_KEY]; + if (m && !m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - var nodes = m.chainWatchers; + var nodes = m && m.chainWatchers; - if (nodes[keyName]) { + if (nodes && nodes[keyName]) { nodes = nodes[keyName]; for (var i = 0, l = nodes.length; i < l; i++) { if (nodes[i] === node) { nodes.splice(i, 1); } } } - unwatchKey(obj, keyName); + unwatchKey(obj, keyName, m); }; // A ChainNode watches a single key on an object. If you provide a starting @@ -3640,14 +3987,14 @@ var ChainNodePrototype = ChainNode.prototype; function lazyGet(obj, key) { if (!obj) return undefined; - var meta = metaFor(obj, false); + var meta = obj[META_KEY]; // check if object meant only to be a prototype - if (meta.proto === obj) return undefined; + if (meta && meta.proto === obj) return undefined; if (key === "@each") return get(obj, key); // if a CP only return cached value - var desc = meta.descs[key]; + var desc = meta && meta.descs[key]; if (desc && desc._cacheable) { if (key in meta.cache) { return meta.cache[key]; @@ -3859,12 +4206,64 @@ ChainNodePrototype.didChange = function(events) { }; Ember.finishChains = function(obj) { - var m = metaFor(obj, false), chains = m.chains; + // We only create meta if we really have to + var m = obj[META_KEY], chains = m && m.chains; if (chains) { if (chains.value() !== obj) { - m.chains = chains = chains.copy(obj); + metaFor(obj).chains = chains = chains.copy(obj); + } else { + chains.didChange(null); } - chains.didChange(null); + } +}; + +})(); + + + +(function() { +/** + @module ember-metal + */ + +var forEach = Ember.EnumerableUtils.forEach, +BRACE_EXPANSION = /^((?:[^\.]*\.)*)\{(.*)\}$/; + +/** + Expands `pattern`, invoking `callback` for each expansion. + + The only pattern supported is brace-expansion, anything else will be passed + once to `callback` directly. Brace expansion can only appear at the end of a + pattern, for example as the last item in a chain. + + Example + ```js + function echo(arg){ console.log(arg); } + + Ember.expandProperties('foo.bar', echo); //=> 'foo.bar' + Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar' + Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz' + Ember.expandProperties('{foo,bar}.baz', echo); //=> '{foo,bar}.baz' + ``` + + @method + @private + @param {string} pattern The property pattern to expand. + @param {function} callback The callback to invoke. It is invoked once per + expansion, and is passed the expansion. + */ +Ember.expandProperties = function (pattern, callback) { + var match, prefix, list; + + if (match = BRACE_EXPANSION.exec(pattern)) { + prefix = match[1]; + list = match[2]; + + forEach(list.split(','), function (suffix) { + callback(prefix + suffix); + }); + } else { + callback(pattern); } }; @@ -3880,8 +4279,8 @@ var metaFor = Ember.meta, // utils.js // get the chains for the current object. If the current object has // chains inherited from the proto they will be cloned and reconfigured for // the current object. -function chainsFor(obj) { - var m = metaFor(obj), ret = m.chains; +function chainsFor(obj, meta) { + var m = meta || metaFor(obj), ret = m.chains; if (!ret) { ret = m.chains = new ChainNode(null, null, obj); } else if (ret.value() !== obj) { @@ -3890,26 +4289,26 @@ function chainsFor(obj) { return ret; } -Ember.watchPath = function(obj, keyPath) { +Ember.watchPath = function(obj, keyPath, meta) { // can't watch length on Array - it is special... if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - var m = metaFor(obj), watching = m.watching; + var m = meta || metaFor(obj), watching = m.watching; if (!watching[keyPath]) { // activate watching first time watching[keyPath] = 1; - chainsFor(obj).add(keyPath); + chainsFor(obj, m).add(keyPath); } else { watching[keyPath] = (watching[keyPath] || 0) + 1; } }; -Ember.unwatchPath = function(obj, keyPath) { - var m = metaFor(obj), watching = m.watching; +Ember.unwatchPath = function(obj, keyPath, meta) { + var m = meta || metaFor(obj), watching = m.watching; if (watching[keyPath] === 1) { watching[keyPath] = 0; - chainsFor(obj).remove(keyPath); + chainsFor(obj, m).remove(keyPath); } else if (watching[keyPath] > 1) { watching[keyPath]--; } @@ -3941,27 +4340,26 @@ function isKeyName(path) { } /** - @private - Starts watching a property on an object. Whenever the property changes, invokes `Ember.propertyWillChange` and `Ember.propertyDidChange`. This is the primitive used by observers and dependent keys; usually you will never call this method directly but instead use higher level methods like `Ember.addObserver()` + @private @method watch @for Ember @param obj @param {String} keyName */ -Ember.watch = function(obj, keyPath) { +Ember.watch = function(obj, _keyPath, m) { // can't watch length on Array - it is special... - if (keyPath === 'length' && typeOf(obj) === 'array') { return; } + if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } - if (isKeyName(keyPath)) { - watchKey(obj, keyPath); + if (isKeyName(_keyPath)) { + watchKey(obj, _keyPath, m); } else { - watchPath(obj, keyPath); + watchPath(obj, _keyPath, m); } }; @@ -3972,34 +4370,33 @@ Ember.isWatching = function isWatching(obj, key) { Ember.watch.flushPending = Ember.flushPendingChains; -Ember.unwatch = function(obj, keyPath) { +Ember.unwatch = function(obj, _keyPath, m) { // can't watch length on Array - it is special... - if (keyPath === 'length' && typeOf(obj) === 'array') { return; } + if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } - if (isKeyName(keyPath)) { - unwatchKey(obj, keyPath); + if (isKeyName(_keyPath)) { + unwatchKey(obj, _keyPath, m); } else { - unwatchPath(obj, keyPath); + unwatchPath(obj, _keyPath, m); } }; /** - @private - Call on an object when you first beget it from another object. This will setup any chained watchers on the object instance as needed. This method is safe to call multiple times. + @private @method rewatch @for Ember @param obj */ Ember.rewatch = function(obj) { - var m = metaFor(obj, false), chains = m.chains; + var m = obj[META_KEY], chains = m && m.chains; // make sure the object has its own guid. if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { - generateGuid(obj, 'ember'); + generateGuid(obj); } // make sure any chained watchers update. @@ -4072,6 +4469,8 @@ var get = Ember.get, watch = Ember.watch, unwatch = Ember.unwatch; +var expandProperties = Ember.expandProperties; + // .......................................................... // DEPENDENT KEYS // @@ -4120,7 +4519,7 @@ function addDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) + 1; // Watch the depKey - watch(obj, depKey); + watch(obj, depKey, meta); } } @@ -4139,7 +4538,7 @@ function removeDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) - 1; // Watch the depKey - unwatch(obj, depKey); + unwatch(obj, depKey, meta); } } @@ -4230,9 +4629,11 @@ function removeDependentKeys(desc, obj, keyName, meta) { */ function ComputedProperty(func, opts) { this.func = func; + + this._dependentKeys = opts && opts.dependentKeys; + this._cacheable = (opts && opts.cacheable !== undefined) ? opts.cacheable : true; - this._dependentKeys = opts && opts.dependentKeys; this._readOnly = opts && (opts.readOnly !== undefined || !!opts.readOnly); } @@ -4241,6 +4642,7 @@ ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; + /** Properties are cacheable by default. Computed property will automatically cache the return value of your function until one of the dependent keys changes. @@ -4266,11 +4668,11 @@ ComputedPropertyPrototype.cacheable = function(aFlag) { mode the computed property will not automatically cache the return value. ```javascript - MyApp.outsideService = Ember.Object.create({ + MyApp.outsideService = Ember.Object.extend({ value: function() { return OutsideService.getValue(); }.property().volatile() - }); + }).create(); ``` @method volatile @@ -4286,12 +4688,14 @@ ComputedPropertyPrototype.volatile = function() { mode the computed property will throw an error when set. ```javascript - MyApp.person = Ember.Object.create({ + MyApp.Person = Ember.Object.extend({ guid: function() { return 'guid-guid-guid'; }.property().readOnly() }); + MyApp.person = MyApp.Person.create(); + MyApp.person.set('guid', 'new-guid'); // will throw an exception ``` @@ -4309,7 +4713,7 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { arguments containing key paths that this computed property depends on. ```javascript - MyApp.president = Ember.Object.create({ + MyApp.President = Ember.Object.extend({ fullName: Ember.computed(function() { return this.get('firstName') + ' ' + this.get('lastName'); @@ -4317,6 +4721,12 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { // and lastName }).property('firstName', 'lastName') }); + + MyApp.president = MyApp.President.create({ + firstName: 'Barack', + lastName: 'Obama', + }); + MyApp.president.get('fullName'); // Barack Obama ``` @method property @@ -4325,11 +4735,21 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { @chainable */ ComputedPropertyPrototype.property = function() { - var args = []; + var args; + + var addArg = function (property) { + args.push(property); + }; + + args = []; for (var i = 0, l = arguments.length; i < l; i++) { - args.push(arguments[i]); + expandProperties(arguments[i], addArg); } - this._dependentKeys = args; + + + this._dependentKeys = args; + + return this; }; @@ -4456,7 +4876,7 @@ ComputedPropertyPrototype.set = function(obj, keyName, value) { funcArgLength, cachedValue, ret; if (this._readOnly) { - throw new Error('Cannot Set: ' + keyName + ' on: ' + obj.toString() ); + throw new Ember.Error('Cannot Set: ' + keyName + ' on: ' + Ember.inspect(obj)); } this._suspended = obj; @@ -4546,7 +4966,7 @@ Ember.computed = function(func) { } if (typeof func !== "function") { - throw new Error("Computed Property declared without a property function"); + throw new Ember.Error("Computed Property declared without a property function"); } var cp = new ComputedProperty(func); @@ -4569,10 +4989,11 @@ Ember.computed = function(func) { @param {Object} obj the object whose property you want to check @param {String} key the name of the property whose cached value you want to return - @return {*} the cached value + @return {Object} the cached value */ Ember.cacheFor = function cacheFor(obj, key) { - var cache = metaFor(obj, false).cache; + var meta = obj[META_KEY], + cache = meta && meta.cache; if (cache && key in cache) { return cache[key]; @@ -4587,40 +5008,47 @@ function getProperties(self, propertyNames) { return ret; } -function registerComputed(name, macro) { - Ember.computed[name] = function(dependentKey) { - var args = a_slice.call(arguments); - return Ember.computed(dependentKey, function() { - return macro.apply(this, args); - }); +var registerComputed, registerComputedWithProperties; + + + + registerComputed = function (name, macro) { + Ember.computed[name] = function(dependentKey) { + var args = a_slice.call(arguments); + return Ember.computed(dependentKey, function() { + return macro.apply(this, args); + }); + }; }; -} -function registerComputedWithProperties(name, macro) { - Ember.computed[name] = function() { - var properties = a_slice.call(arguments); + registerComputedWithProperties = function(name, macro) { + Ember.computed[name] = function() { + var properties = a_slice.call(arguments); - var computed = Ember.computed(function() { - return macro.apply(this, [getProperties(this, properties)]); - }); + var computed = Ember.computed(function() { + return macro.apply(this, [getProperties(this, properties)]); + }); - return computed.property.apply(computed, properties); + return computed.property.apply(computed, properties); + }; }; -} + + + /** - A computed property that returns true of the value of the dependent + A computed property that returns true if the value of the dependent property is null, an empty string, empty array, or empty function. Note: When using `Ember.computed.empty` to watch an array make sure to - use the `array.length` syntax so the computed can subscribe to transitions + use the `array.[]` syntax so the computed can subscribe to transitions from empty to non-empty states. Example ```javascript var ToDoList = Ember.Object.extend({ - done: Ember.computed.empty('todos.length') + done: Ember.computed.empty('todos.[]') // detect array changes }); var todoList = ToDoList.create({todos: ['Unit Test', 'Documentation', 'Release']}); todoList.get('done'); // false @@ -4639,19 +5067,23 @@ registerComputed('empty', function(dependentKey) { }); /** - A computed property that returns true of the value of the dependent + A computed property that returns true if the value of the dependent property is NOT null, an empty string, empty array, or empty function. + Note: When using `Ember.computed.notEmpty` to watch an array make sure to + use the `array.[]` syntax so the computed can subscribe to transitions + from empty to non-empty states. + Example ```javascript - var Hampster = Ember.Object.extend({ - hasStuff: Ember.computed.notEmpty('backpack') + var Hamster = Ember.Object.extend({ + hasStuff: Ember.computed.notEmpty('backpack.[]') }); - var hampster = Hampster.create({backpack: ['Food', 'Sleeping Bag', 'Tent']}); - hampster.get('hasStuff'); // true - hampster.get('backpack').clear(); // [] - hampster.get('hasStuff'); // false + var hamster = Hamster.create({backpack: ['Food', 'Sleeping Bag', 'Tent']}); + hamster.get('hasStuff'); // true + hamster.get('backpack').clear(); // [] + hamster.get('hasStuff'); // false ``` @method computed.notEmpty @@ -4665,22 +5097,22 @@ registerComputed('notEmpty', function(dependentKey) { }); /** - A computed property that returns true of the value of the dependent + A computed property that returns true if the value of the dependent property is null or undefined. This avoids errors from JSLint complaining about use of ==, which can be technically confusing. Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ isHungry: Ember.computed.none('food') }); - var hampster = Hampster.create(); - hampster.get('isHungry'); // true - hampster.set('food', 'Banana'); - hampster.get('isHungry'); // false - hampster.set('food', null); - hampster.get('isHungry'); // true + var hamster = Hamster.create(); + hamster.get('isHungry'); // true + hamster.set('food', 'Banana'); + hamster.get('isHungry'); // false + hamster.set('food', null); + hamster.get('isHungry'); // true ``` @method computed.none @@ -4704,9 +5136,9 @@ registerComputed('none', function(dependentKey) { isAnonymous: Ember.computed.not('loggedIn') }); var user = User.create({loggedIn: false}); - user.get('isAnonymous'); // false - user.set('loggedIn', true); user.get('isAnonymous'); // true + user.set('loggedIn', true); + user.get('isAnonymous'); // false ``` @method computed.not @@ -4724,23 +5156,23 @@ registerComputed('not', function(dependentKey) { into a boolean value. ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ hasBananas: Ember.computed.bool('numBananas') }); - var hampster = Hampster.create(); - hampster.get('hasBananas'); // false - hampster.set('numBananas', 0); - hampster.get('hasBananas'); // false - hampster.set('numBananas', 1); - hampster.get('hasBananas'); // true - hampster.set('numBananas', null); - hampster.get('hasBananas'); // false + var hamster = Hamster.create(); + hamster.get('hasBananas'); // false + hamster.set('numBananas', 0); + hamster.get('hasBananas'); // false + hamster.set('numBananas', 1); + hamster.get('hasBananas'); // true + hamster.set('numBananas', null); + hamster.get('hasBananas'); // false ``` @method computed.bool @for Ember @param {String} dependentKey - @return {Ember.ComputedProperty} computed property which convert + @return {Ember.ComputedProperty} computed property which converts to boolean the original value for property */ registerComputed('bool', function(dependentKey) { @@ -4762,7 +5194,7 @@ registerComputed('bool', function(dependentKey) { user.get('hasValidEmail'); // false user.set('email', ''); user.get('hasValidEmail'); // false - user.set('email', 'ember_hampster@example.com'); + user.set('email', 'ember_hamster@example.com'); user.get('hasValidEmail'); // true ``` @@ -4775,7 +5207,7 @@ registerComputed('bool', function(dependentKey) { */ registerComputed('match', function(dependentKey, regexp) { var value = get(this, dependentKey); - return typeof value === 'string' ? !!value.match(regexp) : false; + return typeof value === 'string' ? regexp.test(value) : false; }); /** @@ -4785,15 +5217,15 @@ registerComputed('match', function(dependentKey, regexp) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ napTime: Ember.computed.equal('state', 'sleepy') }); - var hampster = Hampster.create(); - hampster.get('napTime'); // false - hampster.set('state', 'sleepy'); - hampster.get('napTime'); // false - hampster.set('state', 'hungry'); - hampster.get('napTime'); // false + var hamster = Hamster.create(); + hamster.get('napTime'); // false + hamster.set('state', 'sleepy'); + hamster.get('napTime'); // true + hamster.set('state', 'hungry'); + hamster.get('napTime'); // false ``` @method computed.equal @@ -4814,15 +5246,15 @@ registerComputed('equal', function(dependentKey, value) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ hasTooManyBananas: Ember.computed.gt('numBananas', 10) }); - var hampster = Hampster.create(); - hampster.get('hasTooManyBananas'); // false - hampster.set('numBananas', 3); - hampster.get('hasTooManyBananas'); // false - hampster.set('numBananas', 11); - hampster.get('hasTooManyBananas'); // true + var hamster = Hamster.create(); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 3); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 11); + hamster.get('hasTooManyBananas'); // true ``` @method computed.gt @@ -4843,15 +5275,15 @@ registerComputed('gt', function(dependentKey, value) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ hasTooManyBananas: Ember.computed.gte('numBananas', 10) }); - var hampster = Hampster.create(); - hampster.get('hasTooManyBananas'); // false - hampster.set('numBananas', 3); - hampster.get('hasTooManyBananas'); // false - hampster.set('numBananas', 10); - hampster.get('hasTooManyBananas'); // true + var hamster = Hamster.create(); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 3); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 10); + hamster.get('hasTooManyBananas'); // true ``` @method computed.gte @@ -4872,15 +5304,15 @@ registerComputed('gte', function(dependentKey, value) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ needsMoreBananas: Ember.computed.lt('numBananas', 3) }); - var hampster = Hampster.create(); - hampster.get('needsMoreBananas'); // true - hampster.set('numBananas', 3); - hampster.get('needsMoreBananas'); // false - hampster.set('numBananas', 2); - hampster.get('needsMoreBananas'); // true + var hamster = Hamster.create(); + hamster.get('needsMoreBananas'); // true + hamster.set('numBananas', 3); + hamster.get('needsMoreBananas'); // false + hamster.set('numBananas', 2); + hamster.get('needsMoreBananas'); // true ``` @method computed.lt @@ -4901,15 +5333,15 @@ registerComputed('lt', function(dependentKey, value) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ needsMoreBananas: Ember.computed.lte('numBananas', 3) }); - var hampster = Hampster.create(); - hampster.get('needsMoreBananas'); // true - hampster.set('numBananas', 5); - hampster.get('needsMoreBananas'); // false - hampster.set('numBananas', 3); - hampster.get('needsMoreBananas'); // true + var hamster = Hamster.create(); + hamster.get('needsMoreBananas'); // true + hamster.set('numBananas', 5); + hamster.get('needsMoreBananas'); // false + hamster.set('numBananas', 3); + hamster.get('needsMoreBananas'); // true ``` @method computed.lte @@ -4927,24 +5359,23 @@ registerComputed('lte', function(dependentKey, value) { A computed property that performs a logical `and` on the original values for the provided dependent properties. - Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ readyForCamp: Ember.computed.and('hasTent', 'hasBackpack') }); - var hampster = Hampster.create(); - hampster.get('readyForCamp'); // false - hampster.set('hasTent', true); - hampster.get('readyForCamp'); // false - hampster.set('hasBackpack', true); - hampster.get('readyForCamp'); // true + var hamster = Hamster.create(); + hamster.get('readyForCamp'); // false + hamster.set('hasTent', true); + hamster.get('readyForCamp'); // false + hamster.set('hasBackpack', true); + hamster.get('readyForCamp'); // true ``` @method computed.and @for Ember - @param {String} dependentKey, [dependentKey...] + @param {String} dependentKey* @return {Ember.ComputedProperty} computed property which performs a logical `and` on the values of all the original values for properties. */ @@ -4958,24 +5389,24 @@ registerComputedWithProperties('and', function(properties) { }); /** - A computed property that which performs a logical `or` on the + A computed property which performs a logical `or` on the original values for the provided dependent properties. Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ readyForRain: Ember.computed.or('hasJacket', 'hasUmbrella') }); - var hampster = Hampster.create(); - hampster.get('readyForRain'); // false - hampster.set('hasJacket', true); - hampster.get('readyForRain'); // true + var hamster = Hamster.create(); + hamster.get('readyForRain'); // false + hamster.set('hasJacket', true); + hamster.get('readyForRain'); // true ``` @method computed.or @for Ember - @param {String} dependentKey, [dependentKey...] + @param {String} dependentKey* @return {Ember.ComputedProperty} computed property which performs a logical `or` on the values of all the original values for properties. */ @@ -4995,18 +5426,18 @@ registerComputedWithProperties('or', function(properties) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ hasClothes: Ember.computed.any('hat', 'shirt') }); - var hampster = Hampster.create(); - hampster.get('hasClothes'); // null - hampster.set('shirt', 'Hawaiian Shirt'); - hampster.get('hasClothes'); // 'Hawaiian Shirt' + var hamster = Hamster.create(); + hamster.get('hasClothes'); // null + hamster.set('shirt', 'Hawaiian Shirt'); + hamster.get('hasClothes'); // 'Hawaiian Shirt' ``` @method computed.any @for Ember - @param {String} dependentKey, [dependentKey...] + @param {String} dependentKey* @return {Ember.ComputedProperty} computed property which returns the first truthy value of given list of properties. */ @@ -5026,19 +5457,19 @@ registerComputedWithProperties('any', function(properties) { Example ```javascript - var Hampster = Ember.Object.extend({ - clothes: Ember.computed.map('hat', 'shirt') + var Hamster = Ember.Object.extend({ + clothes: Ember.computed.collect('hat', 'shirt') }); - var hampster = Hampster.create(); - hampster.get('clothes'); // [null, null] - hampster.set('hat', 'Camp Hat'); - hampster.set('shirt', 'Camp Shirt'); - hampster.get('clothes'); // ['Camp Hat', 'Camp Shirt'] + var hamster = Hamster.create(); + hamster.get('clothes'); // [null, null] + hamster.set('hat', 'Camp Hat'); + hamster.set('shirt', 'Camp Shirt'); + hamster.get('clothes'); // ['Camp Hat', 'Camp Shirt'] ``` - @method computed.map + @method computed.collect @for Ember - @param {String} dependentKey, [dependentKey...] + @param {String} dependentKey* @return {Ember.ComputedProperty} computed property which maps values of all passed properties in to an array. */ @@ -5125,7 +5556,7 @@ Ember.computed.alias = function(dependentKey) { @method computed.oneWay @for Ember @param {String} dependentKey - @return {Ember.ComputedProperty} computed property which creates an + @return {Ember.ComputedProperty} computed property which creates a one way computed property to the original value for property. */ Ember.computed.oneWay = function(dependentKey) { @@ -5134,23 +5565,22 @@ Ember.computed.oneWay = function(dependentKey) { }); }; - /** A computed property that acts like a standard getter and setter, - but retruns the value at the provided `defaultPath` if the + but returns the value at the provided `defaultPath` if the property itself has not been set to a value Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ wishList: Ember.computed.defaultTo('favoriteFood') }); - var hampster = Hampster.create({favoriteFood: 'Banana'}); - hampster.get('wishList'); // 'Banana' - hampster.set('wishList', 'More Unit Tests'); - hampster.get('wishList'); // 'More Unit Tests' - hampster.get('favoriteFood'); // 'Banana' + var hamster = Hamster.create({favoriteFood: 'Banana'}); + hamster.get('wishList'); // 'Banana' + hamster.set('wishList', 'More Unit Tests'); + hamster.get('wishList'); // 'More Unit Tests' + hamster.get('favoriteFood'); // 'Banana' ``` @method computed.defaultTo @@ -5169,7 +5599,6 @@ Ember.computed.defaultTo = function(defaultPath) { }; - })(); @@ -5180,8 +5609,8 @@ Ember.computed.defaultTo = function(defaultPath) { @module ember-metal */ -var AFTER_OBSERVERS = ':change'; -var BEFORE_OBSERVERS = ':before'; +var AFTER_OBSERVERS = ':change', + BEFORE_OBSERVERS = ':before'; function changeEvent(keyName) { return keyName+AFTER_OBSERVERS; @@ -5198,9 +5627,10 @@ function beforeEvent(keyName) { @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.addObserver = function(obj, path, target, method) { - Ember.addListener(obj, changeEvent(path), target, method); - Ember.watch(obj, path); +Ember.addObserver = function(obj, _path, target, method) { + Ember.addListener(obj, changeEvent(_path), target, method); + Ember.watch(obj, _path); + return this; }; @@ -5215,9 +5645,10 @@ Ember.observersFor = function(obj, path) { @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.removeObserver = function(obj, path, target, method) { - Ember.unwatch(obj, path); - Ember.removeListener(obj, changeEvent(path), target, method); +Ember.removeObserver = function(obj, _path, target, method) { + Ember.unwatch(obj, _path); + Ember.removeListener(obj, changeEvent(_path), target, method); + return this; }; @@ -5228,9 +5659,10 @@ Ember.removeObserver = function(obj, path, target, method) { @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.addBeforeObserver = function(obj, path, target, method) { - Ember.addListener(obj, beforeEvent(path), target, method); - Ember.watch(obj, path); +Ember.addBeforeObserver = function(obj, _path, target, method) { + Ember.addListener(obj, beforeEvent(_path), target, method); + Ember.watch(obj, _path); + return this; }; @@ -5269,11 +5701,13 @@ Ember.beforeObserversFor = function(obj, path) { @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.removeBeforeObserver = function(obj, path, target, method) { - Ember.unwatch(obj, path); - Ember.removeListener(obj, beforeEvent(path), target, method); +Ember.removeBeforeObserver = function(obj, _path, target, method) { + Ember.unwatch(obj, _path); + Ember.removeListener(obj, beforeEvent(_path), target, method); + return this; }; + })(); @@ -5505,7 +5939,12 @@ define("backburner", debouncees = [], timers = [], autorun, laterTimer, laterTimerExpiresAt, - global = this; + global = this, + NUMBER = /\d+/; + + function isCoercableNumber(number) { + return typeof number === 'number' || NUMBER.test(number); + } function Backburner(queueNames, options) { this.queueNames = queueNames; @@ -5597,7 +6036,7 @@ define("backburner", method = target[method]; } - var stack = this.DEBUG ? new Error().stack : undefined, + var stack = this.DEBUG ? new Error() : undefined, args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; if (!this.currentInstance) { createAutorun(this); } return this.currentInstance.schedule(queueName, target, method, args, false, stack); @@ -5613,39 +6052,67 @@ define("backburner", method = target[method]; } - var stack = this.DEBUG ? new Error().stack : undefined, + var stack = this.DEBUG ? new Error() : undefined, args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; if (!this.currentInstance) { createAutorun(this); } return this.currentInstance.schedule(queueName, target, method, args, true, stack); }, setTimeout: function() { - var self = this, - wait = pop.call(arguments), - target = arguments[0], - method = arguments[1], - executeAt = (+new Date()) + wait; + var args = slice.call(arguments); + var length = args.length; + var method, wait, target; + var self = this; + var methodOrTarget, methodOrWait, methodOrArgs; - if (!method) { - method = target; - target = null; + if (length === 0) { + return; + } else if (length === 1) { + method = args.shift(); + wait = 0; + } else if (length === 2) { + methodOrTarget = args[0]; + methodOrWait = args[1]; + + if (typeof methodOrWait === 'function' || typeof methodOrTarget[methodOrWait] === 'function') { + target = args.shift(); + method = args.shift(); + wait = 0; + } else if (isCoercableNumber(methodOrWait)) { + method = args.shift(); + wait = args.shift(); + } else { + method = args.shift(); + wait = 0; + } + } else { + var last = args[args.length - 1]; + + if (isCoercableNumber(last)) { + wait = args.pop(); + } + + methodOrTarget = args[0]; + methodOrArgs = args[1]; + + if (typeof methodOrArgs === 'function' || (typeof methodOrArgs === 'string' && + methodOrTarget !== null && + methodOrArgs in methodOrTarget)) { + target = args.shift(); + method = args.shift(); + } else { + method = args.shift(); + } } + var executeAt = (+new Date()) + parseInt(wait, 10); + if (typeof method === 'string') { method = target[method]; } - var fn, args; - if (arguments.length > 2) { - args = slice.call(arguments, 2); - - fn = function() { - method.apply(target, args); - }; - } else { - fn = function() { - method.call(target); - }; + function fn() { + method.apply(target, args); } // find position to insert - TODO: binary search @@ -5656,18 +6123,7 @@ define("backburner", timers.splice(i, 0, executeAt, fn); - if (laterTimer && laterTimerExpiresAt < executeAt) { return fn; } - - if (laterTimer) { - clearTimeout(laterTimer); - laterTimer = null; - } - laterTimer = global.setTimeout(function() { - executeTimers(self); - laterTimer = null; - laterTimerExpiresAt = null; - }, wait); - laterTimerExpiresAt = executeAt; + updateLaterTimer(self, executeAt, wait); return fn; }, @@ -5675,31 +6131,26 @@ define("backburner", throttle: function(target, method /* , args, wait */) { var self = this, args = arguments, - wait = pop.call(args), - throttler; + wait = parseInt(pop.call(args), 10), + throttler, + index, + timer; - for (var i = 0, l = throttlers.length; i < l; i++) { - throttler = throttlers[i]; - if (throttler[0] === target && throttler[1] === method) { return; } // do nothing - } + index = findThrottler(target, method); + if (index > -1) { return throttlers[index]; } // throttled - var timer = global.setTimeout(function() { + timer = global.setTimeout(function() { self.run.apply(self, args); - // remove throttler - var index = -1; - for (var i = 0, l = throttlers.length; i < l; i++) { - throttler = throttlers[i]; - if (throttler[0] === target && throttler[1] === method) { - index = i; - break; - } - } - + var index = findThrottler(target, method); if (index > -1) { throttlers.splice(index, 1); } }, wait); - throttlers.push([target, method, timer]); + throttler = [target, method, timer]; + + throttlers.push(throttler); + + return throttler; }, debounce: function(target, method /* , args, wait, [immediate] */) { @@ -5708,30 +6159,32 @@ define("backburner", immediate = pop.call(args), wait, index, - debouncee; + debouncee, + timer; - if (typeof immediate === "number") { + if (typeof immediate === "number" || typeof immediate === "string") { wait = immediate; immediate = false; } else { wait = pop.call(args); } + wait = parseInt(wait, 10); // Remove debouncee index = findDebouncee(target, method); - if (index !== -1) { + if (index > -1) { debouncee = debouncees[index]; debouncees.splice(index, 1); clearTimeout(debouncee[2]); } - var timer = global.setTimeout(function() { + timer = global.setTimeout(function() { if (!immediate) { self.run.apply(self, args); } - index = findDebouncee(target, method); - if (index) { + var index = findDebouncee(target, method); + if (index > -1) { debouncees.splice(index, 1); } }, wait); @@ -5740,7 +6193,11 @@ define("backburner", self.run.apply(self, args); } - debouncees.push([target, method, timer]); + debouncee = [target, method, timer]; + + debouncees.push(debouncee); + + return debouncee; }, cancelTimers: function() { @@ -5773,19 +6230,47 @@ define("backburner", }, cancel: function(timer) { - if (timer && typeof timer === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce + var timerType = typeof timer; + + if (timer && timerType === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce return timer.queue.cancel(timer); - } else if (typeof timer === 'function') { // we're cancelling a setTimeout + } else if (timerType === 'function') { // we're cancelling a setTimeout for (var i = 0, l = timers.length; i < l; i += 2) { if (timers[i + 1] === timer) { timers.splice(i, 2); // remove the two elements return true; } } + } else if (Object.prototype.toString.call(timer) === "[object Array]"){ // we're cancelling a throttle or debounce + return this._cancelItem(findThrottler, throttlers, timer) || + this._cancelItem(findDebouncee, debouncees, timer); } else { return; // timer was null or not a timer } + }, + + _cancelItem: function(findMethod, array, timer){ + var item, + index; + + if (timer.length < 3) { return false; } + + index = findMethod(timer[0], timer[1]); + + if(index > -1) { + + item = array[index]; + + if(item[2] === timer[2]){ + array.splice(index, 1); + clearTimeout(timer[2]); + return true; + } + } + + return false; } + }; Backburner.prototype.schedule = Backburner.prototype.defer; @@ -5800,6 +6285,20 @@ define("backburner", }); } + function updateLaterTimer(self, executeAt, wait) { + if (!laterTimer || executeAt < laterTimerExpiresAt) { + if (laterTimer) { + clearTimeout(laterTimer); + } + laterTimer = global.setTimeout(function() { + laterTimer = null; + laterTimerExpiresAt = null; + executeTimers(self); + }, wait); + laterTimerExpiresAt = executeAt; + } + } + function executeTimers(self) { var now = +new Date(), time, fns, i, l; @@ -5819,12 +6318,7 @@ define("backburner", }); if (timers.length) { - laterTimer = global.setTimeout(function() { - executeTimers(self); - laterTimer = null; - laterTimerExpiresAt = null; - }, timers[0] - now); - laterTimerExpiresAt = timers[0]; + updateLaterTimer(self, timers[0], timers[0] - now); } } @@ -5843,9 +6337,25 @@ define("backburner", return index; } + function findThrottler(target, method) { + var throttler, + index = -1; + + for (var i = 0, l = throttlers.length; i < l; i++) { + throttler = throttlers[i]; + if (throttler[0] === target && throttler[1] === method) { + index = i; + break; + } + } + + return index; + } + __exports__.Backburner = Backburner; }); + })(); @@ -5869,7 +6379,8 @@ var Backburner = requireModule('backburner').Backburner, onBegin: onBegin, onEnd: onEnd }), - slice = [].slice; + slice = [].slice, + concat = [].concat; // .......................................................... // Ember.run - this is ideally the only public API the dev sees @@ -5919,7 +6430,6 @@ Ember.run = function(target, method) { }; /** - If no run-loop is present, it creates a new one. If a run loop is present it will queue itself to run on the existing run-loops action queue. @@ -5953,10 +6463,10 @@ Ember.run = function(target, method) { May be a function or a string. If you pass a string then it will be looked up on the passed target. @param {Object} [args*] Any additional arguments you wish to pass to the method. - @return {Object} return value from invoking the passed function. Please note, + @return {Object} Return value from invoking the passed function. Please note, when called within an existing loop, no return value is possible. */ -Ember.run.join = function(target, method) { +Ember.run.join = function(target, method /* args */) { if (!Ember.run.currentRunLoop) { return Ember.run.apply(Ember.run, arguments); } @@ -5966,6 +6476,53 @@ Ember.run.join = function(target, method) { Ember.run.schedule.apply(Ember.run, args); }; +/** + Provides a useful utility for when integrating with non-Ember libraries + that provide asynchronous callbacks. + + Ember utilizes a run-loop to batch and coalesce changes. This works by + marking the start and end of Ember-related Javascript execution. + + When using events such as a View's click handler, Ember wraps the event + handler in a run-loop, but when integrating with non-Ember libraries this + can be tedious. + + For example, the following is rather verbose but is the correct way to combine + third-party events and Ember code. + + ```javascript + var that = this; + jQuery(window).on('resize', function(){ + Ember.run(function(){ + that.handleResize(); + }); + }); + ``` + + To reduce the boilerplate, the following can be used to construct a + run-loop-wrapped callback handler. + + ```javascript + jQuery(window).on('resize', Ember.run.bind(this, this.triggerResize)); + ``` + + @method bind + @namespace Ember.run + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} return value from invoking the passed function. Please note, + when called within an existing loop, no return value is possible. +*/ +Ember.run.bind = function(target, method /* args*/) { + var args = arguments; + return function() { + return Ember.run.join.apply(Ember.run, args); + }; +}; + Ember.run.backburner = backburner; var run = Ember.run; @@ -6042,7 +6599,8 @@ Ember.run.end = function() { console.log("scheduled on actions queue"); }); - // Note the functions will be run in order based on the run queues order. Output would be: + // Note the functions will be run in order based on the run queues order. + // Output would be: // scheduled on sync queue // scheduled on actions queue ``` @@ -6134,7 +6692,7 @@ Ember.run.later = function(target, method) { If you pass a string it will be resolved on the target at the time the method is invoked. @param {Object} [args*] Optional arguments to pass to the timeout. - @return {Object} timer + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.once = function(target, method) { checkAutoRun(); @@ -6185,7 +6743,7 @@ Ember.run.once = function(target, method) { If you pass a string it will be resolved on the target at the time the method is invoked. @param {Object} [args*] Optional arguments to pass to the timeout. - @return {Object} timer + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.scheduleOnce = function(queue, target, method) { checkAutoRun(); @@ -6199,7 +6757,8 @@ Ember.run.scheduleOnce = function(queue, target, method) { ```javascript Ember.run.next(myContext, function() { - // code to be executed in the next run loop, which will be scheduled after the current one + // code to be executed in the next run loop, + // which will be scheduled after the current one }); ``` @@ -6247,7 +6806,7 @@ Ember.run.scheduleOnce = function(queue, target, method) { If you pass a string it will be resolved on the target at the time the method is invoked. @param {Object} [args*] Optional arguments to pass to the timeout. - @return {Object} timer + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.next = function() { var args = slice.call(arguments); @@ -6257,7 +6816,8 @@ Ember.run.next = function() { /** Cancels a scheduled item. Must be a value returned by `Ember.run.later()`, - `Ember.run.once()`, or `Ember.run.next()`. + `Ember.run.once()`, `Ember.run.next()`, `Ember.run.debounce()`, or + `Ember.run.throttle()`. ```javascript var runNext = Ember.run.next(myContext, function() { @@ -6274,11 +6834,29 @@ Ember.run.next = function() { // will not be executed }); Ember.run.cancel(runOnce); + + var throttle = Ember.run.throttle(myContext, function() { + // will not be executed + }, 1); + Ember.run.cancel(throttle); + + var debounce = Ember.run.debounce(myContext, function() { + // will not be executed + }, 1); + Ember.run.cancel(debounce); + + var debounceImmediate = Ember.run.debounce(myContext, function() { + // will be executed since we passed in true (immediate) + }, 100, true); + // the 100ms delay until this method can be called again will be cancelled + Ember.run.cancel(debounceImmediate); + ``` + ``` ``` @method cancel @param {Object} timer Timer object to cancel - @return {void} + @return {Boolean} true if cancelled or false/undefined if it wasn't found */ Ember.run.cancel = function(timer) { return backburner.cancel(timer); @@ -6310,6 +6888,34 @@ Ember.run.cancel = function(timer) { // console logs 'debounce ran.' one time. ``` + Immediate allows you to run the function immediately, but debounce + other calls for this function until the wait time has elapsed. If + `debounce` is called again before the specified time has elapsed, + the timer is reset and the entire period msut pass again before + the method can be called again. + + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'debounce'}; + + Ember.run.debounce(myContext, myFunc, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 100ms passes + + Ember.run.debounce(myContext, myFunc, 150, true); + + // 150ms passes and nothing else is logged to the console and + // the debouncee is no longer being watched + + Ember.run.debounce(myContext, myFunc, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 150ms passes and nothing else is logged tot he console and + // the debouncee is no longer being watched + + ``` + @method debounce @param {Object} [target] target of method to invoke @param {Function|String} method The method to invoke. @@ -6318,7 +6924,7 @@ Ember.run.cancel = function(timer) { @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} wait Number of milliseconds to wait. @param {Boolean} immediate Trigger the function on the leading instead of the trailing edge of the wait interval. - @return {void} + @return {Array} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.debounce = function() { return backburner.debounce.apply(backburner, arguments); @@ -6355,7 +6961,7 @@ Ember.run.debounce = function() { then it will be looked up on the passed target. @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} spacing Number of milliseconds to space out requests. - @return {void} + @return {Array} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.throttle = function() { return backburner.throttle.apply(backburner, arguments); @@ -6863,10 +7469,14 @@ var Mixin, REQUIRED, Alias, a_slice = [].slice, o_create = Ember.create, defineProperty = Ember.defineProperty, - guidFor = Ember.guidFor; + guidFor = Ember.guidFor, + metaFor = Ember.meta, + META_KEY = Ember.META_KEY; + +var expandProperties = Ember.expandProperties; function mixinsMeta(obj) { - var m = Ember.meta(obj, true), ret = m.mixins; + var m = metaFor(obj, true), ret = m.mixins; if (!ret) { ret = m.mixins = {}; } else if (!m.hasOwnProperty('mixins')) { @@ -7019,15 +7629,14 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats, m descs[key] = value; values[key] = undefined; } else { - // impl super if needed... - if (isMethod(value)) { - value = giveMethodSuper(base, key, value, values, descs); - } else if ((concats && a_indexOf.call(concats, key) >= 0) || + if ((concats && a_indexOf.call(concats, key) >= 0) || key === 'concatenatedProperties' || key === 'mergedProperties') { value = applyConcatenatedProperties(base, key, value, values); } else if ((mergings && a_indexOf.call(mergings, key) >= 0)) { value = applyMergedProperties(base, key, value, values); + } else if (isMethod(value)) { + value = giveMethodSuper(base, key, value, values, descs); } descs[key] = undefined; @@ -7045,13 +7654,14 @@ function mergeMixins(mixins, m, descs, values, base, keys) { for(var i=0, l=mixins.length; i this.changingFrom ? 'green' : 'red'; // logic } - }, 'content.value'), + }), - friendsDidChange: Ember.observer(function(obj, keyName) { + friendsDidChange: Ember.observer('friends.@each.name', function(obj, keyName) { // some logic // obj.get(keyName) returns friends array - }, 'friends.@each.name') + }) }); ``` @@ -7595,12 +8200,35 @@ Ember.immediateObserver = function() { @method beforeObserver @for Ember - @param {Function} func @param {String} propertyNames* + @param {Function} func @return func */ -Ember.beforeObserver = function(func) { - var paths = a_slice.call(arguments, 1); +Ember.beforeObserver = function() { + var func = a_slice.call(arguments, -1)[0]; + var paths; + + var addWatchedProperty = function(path) { paths.push(path); }; + + var _paths = a_slice.call(arguments, 0, -1); + + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering + + func = arguments[0]; + _paths = a_slice.call(arguments, 1); + } + + paths = []; + + for (var i=0; i<_paths.length; ++i) { + expandProperties(_paths[i], addWatchedProperty); + } + + if (typeof func !== "function") { + throw new Ember.Error("Ember.beforeObserver called without a function"); + } + func.__ember_observesBefore__ = paths; return func; }; @@ -7609,6 +8237,55 @@ Ember.beforeObserver = function(func) { +(function() { +// Provides a way to register library versions with ember. +var forEach = Ember.EnumerableUtils.forEach, + indexOf = Ember.EnumerableUtils.indexOf; + +Ember.libraries = function() { + var libraries = []; + var coreLibIndex = 0; + + var getLibrary = function(name) { + for (var i = 0; i < libraries.length; i++) { + if (libraries[i].name === name) { + return libraries[i]; + } + } + }; + + libraries.register = function(name, version) { + if (!getLibrary(name)) { + libraries.push({name: name, version: version}); + } + }; + + libraries.registerCoreLibrary = function(name, version) { + if (!getLibrary(name)) { + libraries.splice(coreLibIndex++, 0, {name: name, version: version}); + } + }; + + libraries.deRegister = function(name) { + var lib = getLibrary(name); + if (lib) libraries.splice(indexOf(libraries, lib), 1); + }; + + libraries.each = function (callback) { + forEach(libraries, function(lib) { + callback(lib.name, lib.version); + }); + }; + + return libraries; +}(); + +Ember.libraries.registerCoreLibrary('Ember', Ember.VERSION); + +})(); + + + (function() { /** Ember Metal @@ -7620,30 +8297,115 @@ Ember Metal })(); (function() { -define("rsvp/all", - ["rsvp/promise","exports"], +/** + @class RSVP + @module RSVP + */ +define("rsvp/all", + ["./promise","exports"], function(__dependency1__, __exports__) { "use strict"; - var Promise = __dependency1__.Promise; - /* global toString */ + var Promise = __dependency1__["default"]; + /** + This is a convenient alias for `RSVP.Promise.all`. + + @method all + @for RSVP + @param {Array} array Array of promises. + @param {String} label An optional label. This is useful + for tooling. + @static + */ + __exports__["default"] = function all(array, label) { + return Promise.all(array, label); + }; + }); +define("rsvp/all_settled", + ["./promise","./utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + var isArray = __dependency2__.isArray; + var isNonThenable = __dependency2__.isNonThenable; - function all(promises) { - if (Object.prototype.toString.call(promises) !== "[object Array]") { - throw new TypeError('You must pass an array to all.'); - } + /** + `RSVP.allSettled` is similar to `RSVP.all`, but instead of implementing + a fail-fast method, it waits until all the promises have returned and + shows you all the results. This is useful if you want to handle multiple + promises' failure states together as a set. + Returns a promise that is fulfilled when all the given promises have been + settled. The return promise is fulfilled with an array of the states of + the promises passed into the `promises` array argument. + + Each state object will either indicate fulfillment or rejection, and + provide the corresponding value or reason. The states will take one of + the following formats: + + ```javascript + { state: 'fulfilled', value: value } + or + { state: 'rejected', reason: reason } + ``` + + Example: + + ```javascript + var promise1 = RSVP.Promise.resolve(1); + var promise2 = RSVP.Promise.reject(new Error('2')); + var promise3 = RSVP.Promise.reject(new Error('3')); + var promises = [ promise1, promise2, promise3 ]; + + RSVP.allSettled(promises).then(function(array){ + // array == [ + // { state: 'fulfilled', value: 1 }, + // { state: 'rejected', reason: Error }, + // { state: 'rejected', reason: Error } + // ] + // Note that for the second item, reason.message will be "2", and for the + // third item, reason.message will be "3". + }, function(error) { + // Not run. (This block would only be called if allSettled had failed, + // for instance if passed an incorrect argument type.) + }); + ``` + + @method allSettled + @for RSVP + @param {Array} promises + @param {String} label - optional string that describes the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled with an array of the settled + states of the constituent promises. + @static + */ + + __exports__["default"] = function allSettled(entries, label) { return new Promise(function(resolve, reject) { - var results = [], remaining = promises.length, - promise; + if (!isArray(entries)) { + throw new TypeError('You must pass an array to allSettled.'); + } + + var remaining = entries.length; + var entry; if (remaining === 0) { resolve([]); + return; } - function resolver(index) { + var results = new Array(remaining); + + function fulfilledResolver(index) { return function(value) { - resolveAll(index, value); + resolveAll(index, fulfilled(value)); + }; + } + + function rejectedResolver(index) { + return function(reason) { + resolveAll(index, rejected(reason)); }; } @@ -7654,152 +8416,113 @@ define("rsvp/all", } } - for (var i = 0; i < promises.length; i++) { - promise = promises[i]; + for (var index = 0; index < entries.length; index++) { + entry = entries[index]; - if (promise && typeof promise.then === 'function') { - promise.then(resolver(i), reject); + if (isNonThenable(entry)) { + resolveAll(index, fulfilled(entry)); } else { - resolveAll(i, promise); + Promise.cast(entry).then(fulfilledResolver(index), rejectedResolver(index)); } } - }); - } + }, label); + }; + function fulfilled(value) { + return { state: 'fulfilled', value: value }; + } - __exports__.all = all; + function rejected(reason) { + return { state: 'rejected', reason: reason }; + } }); -define("rsvp/async", - ["exports"], - function(__exports__) { +define("rsvp/config", + ["./events","exports"], + function(__dependency1__, __exports__) { "use strict"; - var browserGlobal = (typeof window !== 'undefined') ? window : {}; - var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; - var async; - var local = (typeof global !== 'undefined') ? global : this; - - // old node - function useNextTick() { - return function(callback, arg) { - process.nextTick(function() { - callback(arg); - }); - }; - } + var EventTarget = __dependency1__["default"]; - // node >= 0.10.x - function useSetImmediate() { - return function(callback, arg) { - /* global setImmediate */ - setImmediate(function(){ - callback(arg); - }); - }; - } + var config = { + instrument: false + }; - function useMutationObserver() { - var queue = []; + EventTarget.mixin(config); - var observer = new BrowserMutationObserver(function() { - var toProcess = queue.slice(); - queue = []; + function configure(name, value) { + if (name === 'onerror') { + // handle for legacy users that expect the actual + // error to be passed to their function added via + // `RSVP.configure('onerror', someFunctionHere);` + config.on('error', value); + return; + } - toProcess.forEach(function(tuple) { - var callback = tuple[0], arg= tuple[1]; - callback(arg); - }); - }); + if (arguments.length === 2) { + config[name] = value; + } else { + return config[name]; + } + } - var element = document.createElement('div'); - observer.observe(element, { attributes: true }); + __exports__.config = config; + __exports__.configure = configure; + }); +define("rsvp/defer", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; - // Chrome Memory Leak: https://bugs.webkit.org/show_bug.cgi?id=93661 - window.addEventListener('unload', function(){ - observer.disconnect(); - observer = null; - }, false); + /** + `RSVP.defer` returns an object similar to jQuery's `$.Deferred`. + `RSVP.defer` should be used when porting over code reliant on `$.Deferred`'s + interface. New code should use the `RSVP.Promise` constructor instead. - return function(callback, arg) { - queue.push([callback, arg]); - element.setAttribute('drainQueue', 'drainQueue'); - }; - } + The object returned from `RSVP.defer` is a plain object with three properties: - function useSetTimeout() { - return function(callback, arg) { - local.setTimeout(function() { - callback(arg); - }, 1); - }; - } + * promise - an `RSVP.Promise`. + * reject - a function that causes the `promise` property on this object to + become rejected + * resolve - a function that causes the `promise` property on this object to + become fulfilled. - if (typeof setImmediate === 'function') { - async = useSetImmediate(); - } else if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') { - async = useNextTick(); - } else if (BrowserMutationObserver) { - async = useMutationObserver(); - } else { - async = useSetTimeout(); - } + Example: + ```javascript + var deferred = RSVP.defer(); - __exports__.async = async; - }); -define("rsvp/config", - ["rsvp/async","exports"], - function(__dependency1__, __exports__) { - "use strict"; - var async = __dependency1__.async; + deferred.resolve("Success!"); - var config = {}; - config.async = async; + defered.promise.then(function(value){ + // value here is "Success!" + }); + ``` + @method defer + @for RSVP + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Object} + */ - __exports__.config = config; - }); -define("rsvp/defer", - ["rsvp/promise","exports"], - function(__dependency1__, __exports__) { - "use strict"; - var Promise = __dependency1__.Promise; - - function defer() { - var deferred = { - // pre-allocate shape - resolve: undefined, - reject: undefined, - promise: undefined - }; + __exports__["default"] = function defer(label) { + var deferred = { }; deferred.promise = new Promise(function(resolve, reject) { deferred.resolve = resolve; deferred.reject = reject; - }); + }, label); return deferred; - } - - - __exports__.defer = defer; + }; }); -define("rsvp/events", +define("rsvp/events", ["exports"], function(__exports__) { "use strict"; - var Event = function(type, options) { - this.type = type; - - for (var option in options) { - if (!options.hasOwnProperty(option)) { continue; } - - this[option] = options[option]; - } - }; - var indexOf = function(callbacks, callback) { for (var i=0, l=callbacks.length; i 1; }; - var resolveAll = function(prop, value) { - results[prop] = value; - if (--remaining === 0) { - deferred.resolve(results); - } - }; + RSVP.filter(promises, filterFn).then(function(result){ + // result is [ 2, 3 ] + }); + ``` + + If any of the `promises` given to `RSVP.filter` are rejected, the first promise + that is rejected will be given as an argument to the returned promise's + rejection handler. For example: - var rejectAll = function(error) { - deferred.reject(error); + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; + + var filterFn = function(item){ + return item > 1; }; - for (var prop in promises) { - if (promises[prop] && typeof promises[prop].then === 'function') { - promises[prop].then(resolver(prop), rejectAll); - } else { - resolveAll(prop, promises[prop]); - } - } + RSVP.filter(promises, filterFn).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "2" + }); + ``` - return deferred.promise; - } + `RSVP.filter` will also wait for any promises returned from `filterFn`. + For instance, you may want to fetch a list of users then return a subset + of those users based on some asynchronous operation: + ```javascript - __exports__.hash = hash; - }); -define("rsvp/node", - ["rsvp/promise","rsvp/all","exports"], - function(__dependency1__, __dependency2__, __exports__) { - "use strict"; - var Promise = __dependency1__.Promise; - var all = __dependency2__.all; + var alice = { name: 'alice' }; + var bob = { name: 'bob' }; + var users = [ alice, bob ]; - function makeNodeCallbackFor(resolve, reject) { - return function (error, value) { - if (error) { - reject(error); - } else if (arguments.length > 2) { - resolve(Array.prototype.slice.call(arguments, 1)); - } else { - resolve(value); - } + var promises = users.map(function(user){ + return RSVP.resolve(user); + }); + + var filterFn = function(user){ + // Here, Alice has permissions to create a blog post, but Bob does not. + return getPrivilegesForUser(user).then(function(privs){ + return privs.can_create_blog_post === true; + }); }; - } + RSVP.filter(promises, filterFn).then(function(users){ + // true, because the server told us only Alice can create a blog post. + users.length === 1; + // false, because Alice is the only user present in `users` + users[0] === bob; + }); + ``` - function denodeify(nodeFunc) { - return function() { - var nodeArgs = Array.prototype.slice.call(arguments), resolve, reject; - var thisArg = this; + @method filter + @for RSVP + @param {Array} promises + @param {Function} filterFn - function to be called on each resolved value to + filter the final results. + @param {String} label optional string describing the promise. Useful for + tooling. + @return {Promise} + */ + function filter(promises, filterFn, label) { + return all(promises, label).then(function(values){ + if (!isArray(promises)) { + throw new TypeError('You must pass an array to filter.'); + } - var promise = new Promise(function(nodeResolve, nodeReject) { - resolve = nodeResolve; - reject = nodeReject; - }); + if (!isFunction(filterFn)){ + throw new TypeError("You must pass a function to filter's second argument."); + } - all(nodeArgs).then(function(nodeArgs) { - nodeArgs.push(makeNodeCallbackFor(resolve, reject)); + return map(promises, filterFn, label).then(function(filterResults){ + var i, + valuesLen = values.length, + filtered = []; - try { - nodeFunc.apply(thisArg, nodeArgs); - } catch(e) { - reject(e); - } + for (i = 0; i < valuesLen; i++){ + if(filterResults[i]) filtered.push(values[i]); + } + return filtered; }); - - return promise; - }; + }); } - - __exports__.denodeify = denodeify; + __exports__["default"] = filter; }); -define("rsvp/promise", - ["rsvp/config","rsvp/events","exports"], +define("rsvp/hash", + ["./promise","./utils","exports"], function(__dependency1__, __dependency2__, __exports__) { "use strict"; - var config = __dependency1__.config; - var EventTarget = __dependency2__.EventTarget; + var Promise = __dependency1__["default"]; + var isNonThenable = __dependency2__.isNonThenable; + var keysOf = __dependency2__.keysOf; - function objectOrFunction(x) { - return isFunction(x) || (typeof x === "object" && x !== null); - } - - function isFunction(x){ - return typeof x === "function"; - } - - var Promise = function(resolver) { - var promise = this, - resolved = false; + /** + `RSVP.hash` is similar to `RSVP.all`, but takes an object instead of an array + for its `promises` argument. - if (typeof resolver !== 'function') { - throw new TypeError('You must pass a resolver function as the sole argument to the promise constructor'); - } + Returns a promise that is fulfilled when all the given promises have been + fulfilled, or rejected if any of them become rejected. The returned promise + is fulfilled with a hash that has the same key names as the `promises` object + argument. If any of the values in the object are not promises, they will + simply be copied over to the fulfilled object. - if (!(promise instanceof Promise)) { - return new Promise(resolver); - } + Example: - var resolvePromise = function(value) { - if (resolved) { return; } - resolved = true; - resolve(promise, value); + ```javascript + var promises = { + myPromise: RSVP.resolve(1), + yourPromise: RSVP.resolve(2), + theirPromise: RSVP.resolve(3), + notAPromise: 4 }; - var rejectPromise = function(value) { - if (resolved) { return; } - resolved = true; - reject(promise, value); - }; + RSVP.hash(promises).then(function(hash){ + // hash here is an object that looks like: + // { + // myPromise: 1, + // yourPromise: 2, + // theirPromise: 3, + // notAPromise: 4 + // } + }); + ```` - this.on('promise:resolved', function(event) { - this.trigger('success', { detail: event.detail }); - }, this); + If any of the `promises` given to `RSVP.hash` are rejected, the first promise + that is rejected will be given as the reason to the rejection handler. - this.on('promise:failed', function(event) { - this.trigger('error', { detail: event.detail }); - }, this); + Example: - this.on('error', onerror); + ```javascript + var promises = { + myPromise: RSVP.resolve(1), + rejectedPromise: RSVP.reject(new Error("rejectedPromise")), + anotherRejectedPromise: RSVP.reject(new Error("anotherRejectedPromise")), + }; - try { - resolver(resolvePromise, rejectPromise); - } catch(e) { - rejectPromise(e); - } - }; + RSVP.hash(promises).then(function(hash){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "rejectedPromise" + }); + ``` - function onerror(event) { - if (config.onerror) { - config.onerror(event.detail); - } - } + An important note: `RSVP.hash` is intended for plain JavaScript objects that + are just a set of keys and values. `RSVP.hash` will NOT preserve prototype + chains. - var invokeCallback = function(type, promise, callback, event) { - var hasCallback = isFunction(callback), - value, error, succeeded, failed; + Example: - if (hasCallback) { - try { - value = callback(event.detail); - succeeded = true; - } catch(e) { - failed = true; - error = e; - } - } else { - value = event.detail; - succeeded = true; + ```javascript + function MyConstructor(){ + this.example = RSVP.resolve("Example"); } - if (handleThenable(promise, value)) { - return; - } else if (hasCallback && succeeded) { - resolve(promise, value); - } else if (failed) { - reject(promise, error); - } else if (type === 'resolve') { - resolve(promise, value); - } else if (type === 'reject') { - reject(promise, value); - } - }; + MyConstructor.prototype = { + protoProperty: RSVP.resolve("Proto Property") + }; - Promise.prototype = { - constructor: Promise, + var myObject = new MyConstructor(); + + RSVP.hash(myObject).then(function(hash){ + // protoProperty will not be present, instead you will just have an + // object that looks like: + // { + // example: "Example" + // } + // + // hash.hasOwnProperty('protoProperty'); // false + // 'undefined' === typeof hash.protoProperty + }); + ``` - isRejected: undefined, - isFulfilled: undefined, - rejectedReason: undefined, - fulfillmentValue: undefined, + @method hash + @for RSVP + @param {Object} promises + @param {String} label optional string that describes the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all properties of `promises` + have been fulfilled, or rejected if any of them become rejected. + @static + */ + __exports__["default"] = function hash(object, label) { + return new Promise(function(resolve, reject){ + var results = {}; + var keys = keysOf(object); + var remaining = keys.length; + var entry, property; - then: function(done, fail) { - this.off('error', onerror); + if (remaining === 0) { + resolve(results); + return; + } - var thenPromise = new this.constructor(function() {}); + function fulfilledTo(property) { + return function(value) { + results[property] = value; + if (--remaining === 0) { + resolve(results); + } + }; + } - if (this.isFulfilled) { - config.async(function(promise) { - invokeCallback('resolve', thenPromise, done, { detail: promise.fulfillmentValue }); - }, this); + function onRejection(reason) { + remaining = 0; + reject(reason); } - if (this.isRejected) { - config.async(function(promise) { - invokeCallback('reject', thenPromise, fail, { detail: promise.rejectedReason }); - }, this); + for (var i = 0; i < keys.length; i++) { + property = keys[i]; + entry = object[property]; + + if (isNonThenable(entry)) { + results[property] = entry; + if (--remaining === 0) { + resolve(results); + } + } else { + Promise.cast(entry).then(fulfilledTo(property), onRejection); + } } + }); + }; + }); +define("rsvp/instrument", + ["./config","./utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var config = __dependency1__.config; + var now = __dependency2__.now; - this.on('promise:resolved', function(event) { - invokeCallback('resolve', thenPromise, done, event); + __exports__["default"] = function instrument(eventName, promise, child) { + // instrumentation should not disrupt normal usage. + try { + config.trigger(eventName, { + guid: promise._guidKey + promise._id, + eventName: eventName, + detail: promise._detail, + childGuid: child && promise._guidKey + child._id, + label: promise._label, + timeStamp: now(), + stack: new Error(promise._label).stack }); + } catch(error) { + setTimeout(function(){ + throw error; + }, 0); + } + }; + }); +define("rsvp/map", + ["./promise","./all","./utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + var all = __dependency2__["default"]; + var isArray = __dependency3__.isArray; + var isFunction = __dependency3__.isFunction; - this.on('promise:failed', function(event) { - invokeCallback('reject', thenPromise, fail, event); - }); + /** + `RSVP.map` is similar to JavaScript's native `map` method, except that it + waits for all promises to become fulfilled before running the `mapFn` on + each item in given to `promises`. `RSVP.map` returns a promise that will + become fulfilled with the result of running `mapFn` on the values the promises + become fulfilled with. - return thenPromise; - }, + For example: - fail: function(fail) { - return this.then(null, fail); - } - }; + ```javascript - EventTarget.mixin(Promise.prototype); + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.resolve(2); + var promise3 = RSVP.resolve(3); + var promises = [ promise1, promise2, promise3 ]; - function resolve(promise, value) { - if (promise === value) { - fulfill(promise, value); - } else if (!handleThenable(promise, value)) { - fulfill(promise, value); - } - } + var mapFn = function(item){ + return item + 1; + }; - function handleThenable(promise, value) { - var then = null, - resolved; + RSVP.map(promises, mapFn).then(function(result){ + // result is [ 2, 3, 4 ] + }); + ``` - try { - if (promise === value) { - throw new TypeError("A promises callback cannot return that same promise."); - } + If any of the `promises` given to `RSVP.map` are rejected, the first promise + that is rejected will be given as an argument to the returned promise's + rejection handler. For example: - if (objectOrFunction(value)) { - then = value.then; + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; - if (isFunction(then)) { - then.call(value, function(val) { - if (resolved) { return true; } - resolved = true; + var mapFn = function(item){ + return item + 1; + }; - if (value !== val) { - resolve(promise, val); - } else { - fulfill(promise, val); - } - }, function(val) { - if (resolved) { return true; } - resolved = true; + RSVP.map(promises, mapFn).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "2" + }); + ``` - reject(promise, val); - }); + `RSVP.map` will also wait if a promise is returned from `mapFn`. For example, + say you want to get all comments from a set of blog posts, but you need + the blog posts first becuase they contain a url to those comments. - return true; - } - } - } catch (error) { - reject(promise, error); - return true; - } + ```javscript - return false; - } + var mapFn = function(blogPost){ + // getComments does some ajax and returns an RSVP.Promise that is fulfilled + // with some comments data + return getComments(blogPost.comments_url); + }; - function fulfill(promise, value) { - config.async(function() { - promise.trigger('promise:resolved', { detail: value }); - promise.isFulfilled = true; - promise.fulfillmentValue = value; + // getBlogPosts does some ajax and returns an RSVP.Promise that is fulfilled + // with some blog post data + RSVP.map(getBlogPosts(), mapFn).then(function(comments){ + // comments is the result of asking the server for the comments + // of all blog posts returned from getBlogPosts() }); - } + ``` - function reject(promise, value) { - config.async(function() { - promise.trigger('promise:failed', { detail: value }); - promise.isRejected = true; - promise.rejectedReason = value; - }); - } + @method map + @for RSVP + @param {Array} promises + @param {Function} mapFn function to be called on each fulfilled promise. + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled with the result of calling + `mapFn` on each fulfilled promise or value when they become fulfilled. + The promise will be rejected if any of the given `promises` become rejected. + @static + */ + __exports__["default"] = function map(promises, mapFn, label) { + return all(promises, label).then(function(results){ + if (!isArray(promises)) { + throw new TypeError('You must pass an array to map.'); + } + if (!isFunction(mapFn)){ + throw new TypeError("You must pass a function to map's second argument."); + } - __exports__.Promise = Promise; + + var resultLen = results.length, + mappedResults = [], + i; + + for (i = 0; i < resultLen; i++){ + mappedResults.push(mapFn(results[i])); + } + + return all(mappedResults, label); + }); + }; }); -define("rsvp/reject", - ["rsvp/promise","exports"], +define("rsvp/node", + ["./promise","exports"], function(__dependency1__, __exports__) { "use strict"; - var Promise = __dependency1__.Promise; + var Promise = __dependency1__["default"]; - function reject(reason) { - return new Promise(function (resolve, reject) { - reject(reason); - }); + var slice = Array.prototype.slice; + + function makeNodeCallbackFor(resolve, reject) { + return function (error, value) { + if (error) { + reject(error); + } else if (arguments.length > 2) { + resolve(slice.call(arguments, 1)); + } else { + resolve(value); + } + }; } + /** + `RSVP.denodeify` takes a "node-style" function and returns a function that + will return an `RSVP.Promise`. You can use `denodeify` in Node.js or the + browser when you'd prefer to use promises over using callbacks. For example, + `denodeify` transforms the following: - __exports__.reject = reject; - }); -define("rsvp/resolve", - ["rsvp/promise","exports"], - function(__dependency1__, __exports__) { - "use strict"; - var Promise = __dependency1__.Promise; + ```javascript + var fs = require('fs'); - function resolve(thenable) { - return new Promise(function(resolve, reject) { - resolve(thenable); + fs.readFile('myfile.txt', function(err, data){ + if (err) return handleError(err); + handleData(data); }); - } + ``` + into: - __exports__.resolve = resolve; - }); -define("rsvp/rethrow", - ["exports"], - function(__exports__) { - "use strict"; - var local = (typeof global === "undefined") ? this : global; + ```javascript + var fs = require('fs'); - function rethrow(reason) { - local.setTimeout(function() { - throw reason; + var readFile = RSVP.denodeify(fs.readFile); + + readFile('myfile.txt').then(handleData, handleError); + ``` + + Using `denodeify` makes it easier to compose asynchronous operations instead + of using callbacks. For example, instead of: + + ```javascript + var fs = require('fs'); + var log = require('some-async-logger'); + + fs.readFile('myfile.txt', function(err, data){ + if (err) return handleError(err); + fs.writeFile('myfile2.txt', data, function(err){ + if (err) throw err; + log('success', function(err) { + if (err) throw err; + }); + }); }); - throw reason; - } + ``` + You can chain the operations together using `then` from the returned promise: - __exports__.rethrow = rethrow; + ```javascript + var fs = require('fs'); + var denodeify = RSVP.denodeify; + var readFile = denodeify(fs.readFile); + var writeFile = denodeify(fs.writeFile); + var log = denodeify(require('some-async-logger')); + + readFile('myfile.txt').then(function(data){ + return writeFile('myfile2.txt', data); + }).then(function(){ + return log('SUCCESS'); + }).then(function(){ + // success handler + }, function(reason){ + // rejection handler + }); + ``` + + @method denodeify + @for RSVP + @param {Function} nodeFunc a "node-style" function that takes a callback as + its last argument. The callback expects an error to be passed as its first + argument (if an error occurred, otherwise null), and the value from the + operation as its second argument ("function(err, value){ }"). + @param {Any} binding optional argument for binding the "this" value when + calling the `nodeFunc` function. + @return {Function} a function that wraps `nodeFunc` to return an + `RSVP.Promise` + @static + */ + __exports__["default"] = function denodeify(nodeFunc, binding) { + return function() { + var nodeArgs = slice.call(arguments), resolve, reject; + var thisArg = this || binding; + + return new Promise(function(resolve, reject) { + Promise.all(nodeArgs).then(function(nodeArgs) { + try { + nodeArgs.push(makeNodeCallbackFor(resolve, reject)); + nodeFunc.apply(thisArg, nodeArgs); + } catch(e) { + reject(e); + } + }); + }); + }; + }; }); -define("rsvp", - ["rsvp/events","rsvp/promise","rsvp/node","rsvp/all","rsvp/hash","rsvp/rethrow","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __exports__) { +define("rsvp/promise", + ["./config","./events","./instrument","./utils","./promise/cast","./promise/all","./promise/race","./promise/resolve","./promise/reject","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { "use strict"; - var EventTarget = __dependency1__.EventTarget; - var Promise = __dependency2__.Promise; - var denodeify = __dependency3__.denodeify; - var all = __dependency4__.all; - var hash = __dependency5__.hash; - var rethrow = __dependency6__.rethrow; - var defer = __dependency7__.defer; - var config = __dependency8__.config; - var resolve = __dependency9__.resolve; - var reject = __dependency10__.reject; + var config = __dependency1__.config; + var EventTarget = __dependency2__["default"]; + var instrument = __dependency3__["default"]; + var objectOrFunction = __dependency4__.objectOrFunction; + var isFunction = __dependency4__.isFunction; + var now = __dependency4__.now; + var cast = __dependency5__["default"]; + var all = __dependency6__["default"]; + var race = __dependency7__["default"]; + var Resolve = __dependency8__["default"]; + var Reject = __dependency9__["default"]; - function configure(name, value) { - config[name] = value; - } + var guidKey = 'rsvp_' + now() + '-'; + var counter = 0; + function noop() {} - __exports__.Promise = Promise; - __exports__.EventTarget = EventTarget; - __exports__.all = all; - __exports__.hash = hash; - __exports__.rethrow = rethrow; - __exports__.defer = defer; - __exports__.denodeify = denodeify; - __exports__.configure = configure; - __exports__.resolve = resolve; - __exports__.reject = reject; - }); -})(); + __exports__["default"] = Promise; -(function() { -/** - Flag to enable/disable model factory injections (disabled by default) - If model factory injections are enabled, models should not be - accessed globally (only through `container.lookupFactory('model:modelName'))`); -*/ -Ember.MODEL_FACTORY_INJECTIONS = false || !!Ember.ENV.MODEL_FACTORY_INJECTIONS; -define("container", - [], - function() { + /** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise’s eventual value or the reason + why the promise cannot be fulfilled. - /** - A safe and simple inheriting object. + Terminology + ----------- - @class InheritingDict - */ - function InheritingDict(parent) { - this.parent = parent; - this.dict = {}; - } + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. - InheritingDict.prototype = { + A promise can be in one of three states: pending, fulfilled, or rejected. - /** - @property parent - @type InheritingDict - @default null - */ + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. Similarly, a + rejection reason is never a thenable. - parent: null, + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. - /** - Object used to store the current nodes data. - @property dict - @type Object - @default Object - */ - dict: null, + Basic Usage: + ------------ - /** - Retrieve the value given a key, if the value is present at the current - level use it, otherwise walk up the parent hierarchy and try again. If - no matching key is found, return undefined. + ```js + var promise = new Promise(function(resolve, reject) { + // on success + resolve(value); - @method get - @return {any} - */ - get: function(key) { - var dict = this.dict; + // on failure + reject(reason); + }); - if (dict.hasOwnProperty(key)) { - return dict[key]; - } + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` - if (this.parent) { - return this.parent.get(key); - } - }, + Advanced Usage: + --------------- - /** - Set the given value for the given key, at the current level. + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. - @method set - @param {String} key - @param {Any} value - */ - set: function(key, value) { - this.dict[key] = value; - }, + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + var xhr = new XMLHttpRequest(); - /** - Delete the given key + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); - @method remove - @param {String} key - */ - remove: function(key) { - delete this.dict[key]; - }, + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error("getJSON: `" + url + "` failed with status: [" + this.status + "]"); + } + } + }; + }); + } - /** - Check for the existence of given a key, if the key is present at the current - level return true, otherwise walk up the parent hierarchy and try again. If - no matching key is found, return false. + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` - @method has - @param {String} key - @returns {Boolean} - */ - has: function(key) { - var dict = this.dict; + Unlike callbacks, promises are great composable primitives. - if (dict.hasOwnProperty(key)) { - return true; - } + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON - if (this.parent) { - return this.parent.has(key); - } + return values; + }); + ``` - return false; - }, + @class RSVP.Promise + @param {function} + @param {String} label optional string for labeling the promise. + Useful for tooling. + @constructor + */ + function Promise(resolver, label) { + if (!isFunction(resolver)) { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); + } - /** - Iterate and invoke a callback for each local key-value pair. + if (!(this instanceof Promise)) { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); + } - @method eachLocal - @param {Function} callback - @param {Object} binding - */ - eachLocal: function(callback, binding) { - var dict = this.dict; + this._id = counter++; + this._label = label; + this._subscribers = []; - for (var prop in dict) { - if (dict.hasOwnProperty(prop)) { - callback.call(binding, prop, dict[prop]); - } - } + if (config.instrument) { + instrument('created', this); } - }; - /** - A lightweight container that helps to assemble and decouple components. + if (noop !== resolver) { + invokeResolver(resolver, this); + } + } - @class Container - */ - function Container(parent) { - this.parent = parent; - this.children = []; + function invokeResolver(resolver, promise) { + function resolvePromise(value) { + resolve(promise, value); + } - this.resolver = parent && parent.resolver || function() {}; + function rejectPromise(reason) { + reject(promise, reason); + } - this.registry = new InheritingDict(parent && parent.registry); - this.cache = new InheritingDict(parent && parent.cache); - this.factoryCache = new InheritingDict(parent && parent.cache); - this.typeInjections = new InheritingDict(parent && parent.typeInjections); - this.injections = {}; + try { + resolver(resolvePromise, rejectPromise); + } catch(e) { + rejectPromise(e); + } + } - this.factoryTypeInjections = new InheritingDict(parent && parent.factoryTypeInjections); - this.factoryInjections = {}; + Promise.cast = cast; + Promise.all = all; + Promise.race = race; + Promise.resolve = Resolve; + Promise.reject = Reject; - this._options = new InheritingDict(parent && parent._options); - this._typeOptions = new InheritingDict(parent && parent._typeOptions); + var PENDING = void 0; + var SEALED = 0; + var FULFILLED = 1; + var REJECTED = 2; + + function subscribe(parent, child, onFulfillment, onRejection) { + var subscribers = parent._subscribers; + var length = subscribers.length; + + subscribers[length] = child; + subscribers[length + FULFILLED] = onFulfillment; + subscribers[length + REJECTED] = onRejection; } - Container.prototype = { + function publish(promise, settled) { + var child, callback, subscribers = promise._subscribers, detail = promise._detail; - /** - @property parent - @type Container - @default null - */ - parent: null, + if (config.instrument) { + instrument(settled === FULFILLED ? 'fulfilled' : 'rejected', promise); + } - /** - @property children - @type Array - @default [] - */ - children: null, + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; - /** - @property resolver - @type function - */ - resolver: null, + invokeCallback(settled, child, callback, detail); + } - /** - @property registry - @type InheritingDict - */ - registry: null, + promise._subscribers = null; + } - /** - @property cache - @type InheritingDict - */ - cache: null, + Promise.prototype = { + constructor: Promise, - /** - @property typeInjections - @type InheritingDict - */ - typeInjections: null, + _id: undefined, + _guidKey: guidKey, + _label: undefined, - /** - @property injections - @type Object - @default {} - */ - injections: null, + _state: undefined, + _detail: undefined, + _subscribers: undefined, - /** - @private + _onerror: function (reason) { + config.trigger('error', reason); + }, - @property _options - @type InheritingDict - @default null - */ - _options: null, + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` - /** - @private + Chaining + -------- + + The return value of `then` is itself a promise. This second, "downstream" + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return "default name"; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `"default name"` + }); - @property _typeOptions - @type InheritingDict - */ - _typeOptions: null, + findUser().then(function (user) { + throw new Error("Found user, but still unhappy"); + }, function (reason) { + throw new Error("`findUser` rejected and we're unhappy"); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be "Found user, but still unhappy". + // If `findUser` rejected, `reason` will be "`findUser` rejected and we're unhappy". + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + + ```js + findUser().then(function (user) { + throw new PedagogicalException("Upstream error"); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` - /** - Returns a new child of the current container. These children are configured - to correctly inherit from the current container. + Assimilation + ------------ - @method child - @returns {Container} - */ - child: function() { - var container = new Container(this); - this.children.push(container); - return container; - }, + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. - /** - Sets a key-value pair on the current container. If a parent container, - has the same key, once set on a child, the parent and child will diverge - as expected. + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` - @method set - @param {Object} object - @param {String} key - @param {any} value - */ - set: function(object, key, value) { - object[key] = value; - }, + If the assimliated promise rejects, then the downstream promise will also reject. - /** - Registers a factory for later injection. + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` - Example: + Simple Example + -------------- - ```javascript - var container = new Container(); + Synchronous Example - container.register('model:user', Person, {singleton: false }); - container.register('fruit:favorite', Orange); - container.register('communication:main', Email, {singleton: false}); - ``` + ```javascript + var result; - @method register - @param {String} type - @param {String} name - @param {Function} factory - @param {Object} options - */ - register: function(type, name, factory, options) { - var fullName; + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` - if (type.indexOf(':') !== -1) { - options = factory; - factory = name; - fullName = type; + Errback Example + + ```js + findResult(function(result, err){ + if (err) { + // failure } else { - Ember.deprecate('register("'+type +'", "'+ name+'") is now deprecated in-favour of register("'+type+':'+name+'");', false); - fullName = type + ":" + name; + // success } + }); + ``` - var normalizedName = this.normalize(fullName); + Promise Example; - this.registry.set(normalizedName, factory); - this._options.set(normalizedName, options || {}); - }, + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` - /** - Unregister a fullName + Advanced Example + -------------- - ```javascript - var container = new Container(); - container.register('model:user', User); + Synchronous Example - container.lookup('model:user') instanceof User //=> true + ```javascript + var author, books; - container.unregister('model:user') - container.lookup('model:user') === undefined //=> true - ``` + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` - @method unregister - @param {String} fullName - */ - unregister: function(fullName) { - var normalizedName = this.normalize(fullName); + Errback Example - this.registry.remove(normalizedName); - this.cache.remove(normalizedName); - this.factoryCache.remove(normalizedName); - this._options.remove(normalizedName); - }, + ```js - /** - Given a fullName return the corresponding factory. + function foundBooks(books) { - By default `resolve` will retrieve the factory from - its container's registry. + } - ```javascript - var container = new Container(); - container.register('api:twitter', Twitter); + function failure(reason) { - container.resolve('api:twitter') // => Twitter - ``` + } - Optionally the container can be provided with a custom resolver. - If provided, `resolve` will first provide the custom resolver - the oppertunity to resolve the fullName, otherwise it will fallback - to the registry. + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` - ```javascript - var container = new Container(); - container.resolver = function(fullName) { - // lookup via the module system of choice - }; + Promise Example; - // the twitter factory is added to the module system - container.resolve('api:twitter') // => Twitter - ``` - - @method resolve - @param {String} fullName - @returns {Function} fullName's factory - */ - resolve: function(fullName) { - return this.resolver(fullName) || this.registry.get(fullName); - }, + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` - /** - A hook that can be used to describe how the resolver will - attempt to find the factory. + @method then + @param {Function} onFulfilled + @param {Function} onRejected + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + then: function(onFulfillment, onRejection, label) { + var promise = this; + this._onerror = null; - For example, the default Ember `.describe` returns the full - class name (including namespace) where Ember's resolver expects - to find the `fullName`. + var thenPromise = new this.constructor(noop, label); - @method describe - */ - describe: function(fullName) { - return fullName; - }, + if (this._state) { + var callbacks = arguments; + config.async(function invokePromiseCallback() { + invokeCallback(promise._state, thenPromise, callbacks[promise._state - 1], promise._detail); + }); + } else { + subscribe(this, thenPromise, onFulfillment, onRejection); + } - /** - A hook to enable custom fullName normalization behaviour + if (config.instrument) { + instrument('chained', promise, thenPromise); + } - @method normalize - @param {String} fullName - @return {string} normalized fullName - */ - normalize: function(fullName) { - return fullName; + return thenPromise; }, - /** - @method makeToString - - @param {any} factory - @param {string} fullNae - @return {function} toString function - */ - makeToString: function(factory, fullName) { - return factory.toString(); - }, + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. - /** - Given a fullName return a corresponding instance. + ```js + function findAuthor(){ + throw new Error("couldn't find that author"); + } - The default behaviour is for lookup to return a singleton instance. - The singleton is scoped to the container, allowing multiple containers - to all have their own locally scoped singletons. + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } - ```javascript - var container = new Container(); - container.register('api:twitter', Twitter); + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` - var twitter = container.lookup('api:twitter'); + @method catch + @param {Function} onRejection + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + 'catch': function(onRejection, label) { + return this.then(null, onRejection, label); + }, - twitter instanceof Twitter; // => true + /** + `finally` will be invoked regardless of the promise's fate just as native + try/catch/finally behaves - // by default the container will return singletons - twitter2 = container.lookup('api:twitter'); - twitter instanceof Twitter; // => true + Synchronous example: - twitter === twitter2; //=> true - ``` + ```js + findAuthor() { + if (Math.random() > 0.5) { + throw new Error(); + } + return new Author(); + } - If singletons are not wanted an optional flag can be provided at lookup. + try { + return findAuthor(); // succeed or fail + } catch(error) { + return findOtherAuther(); + } finally { + // always runs + // doesn't affect the return value + } + ``` - ```javascript - var container = new Container(); - container.register('api:twitter', Twitter); + Asynchronous example: - var twitter = container.lookup('api:twitter', { singleton: false }); - var twitter2 = container.lookup('api:twitter', { singleton: false }); + ```js + findAuthor().catch(function(reason){ + return findOtherAuther(); + }).finally(function(){ + // author was either found, or not + }); + ``` - twitter === twitter2; //=> false - ``` + @method finally + @param {Function} callback + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + 'finally': function(callback, label) { + var constructor = this.constructor; - @method lookup - @param {String} fullName - @param {Object} options - @return {any} - */ - lookup: function(fullName, options) { - fullName = this.normalize(fullName); + return this.then(function(value) { + return constructor.cast(callback()).then(function(){ + return value; + }); + }, function(reason) { + return constructor.cast(callback()).then(function(){ + throw reason; + }); + }, label); + } + }; - options = options || {}; + function invokeCallback(settled, promise, callback, detail) { + var hasCallback = isFunction(callback), + value, error, succeeded, failed; - if (this.cache.has(fullName) && options.singleton !== false) { - return this.cache.get(fullName); + if (hasCallback) { + try { + value = callback(detail); + succeeded = true; + } catch(e) { + failed = true; + error = e; } + } else { + value = detail; + succeeded = true; + } - var value = instantiate(this, fullName); + if (handleThenable(promise, value)) { + return; + } else if (hasCallback && succeeded) { + resolve(promise, value); + } else if (failed) { + reject(promise, error); + } else if (settled === FULFILLED) { + resolve(promise, value); + } else if (settled === REJECTED) { + reject(promise, value); + } + } - if (!value) { return; } + function handleThenable(promise, value) { + var then = null, + resolved; - if (isSingleton(this, fullName) && options.singleton !== false) { - this.cache.set(fullName, value); + try { + if (promise === value) { + throw new TypeError("A promises callback cannot return that same promise."); } - return value; - }, + if (objectOrFunction(value)) { + then = value.then; - /** - Given a fullName return the corresponding factory. + if (isFunction(then)) { + then.call(value, function(val) { + if (resolved) { return true; } + resolved = true; - @method lookupFactory - @param {String} fullName - @return {any} - */ - lookupFactory: function(fullName) { - return factoryFor(this, fullName); - }, + if (value !== val) { + resolve(promise, val); + } else { + fulfill(promise, val); + } + }, function(val) { + if (resolved) { return true; } + resolved = true; - /** - Given a fullName check if the container is aware of its factory - or singleton instance. + reject(promise, val); + }, 'derived from: ' + (promise._label || ' unknown promise')); - @method has - @param {String} fullName - @return {Boolean} - */ - has: function(fullName) { - if (this.cache.has(fullName)) { - return true; + return true; + } } + } catch (error) { + if (resolved) { return true; } + reject(promise, error); + return true; + } - return !!factoryFor(this, fullName); - }, - - /** - Allow registering options for all factories of a type. + return false; + } - ```javascript - var container = new Container(); + function resolve(promise, value) { + if (promise === value) { + fulfill(promise, value); + } else if (!handleThenable(promise, value)) { + fulfill(promise, value); + } + } - // if all of type `connection` must not be singletons - container.optionsForType('connection', { singleton: false }); + function fulfill(promise, value) { + if (promise._state !== PENDING) { return; } + promise._state = SEALED; + promise._detail = value; - container.register('connection:twitter', TwitterConnection); - container.register('connection:facebook', FacebookConnection); + config.async(publishFulfillment, promise); + } - var twitter = container.lookup('connection:twitter'); - var twitter2 = container.lookup('connection:twitter'); + function reject(promise, reason) { + if (promise._state !== PENDING) { return; } + promise._state = SEALED; + promise._detail = reason; - twitter === twitter2; // => false + config.async(publishRejection, promise); + } - var facebook = container.lookup('connection:facebook'); - var facebook2 = container.lookup('connection:facebook'); + function publishFulfillment(promise) { + publish(promise, promise._state = FULFILLED); + } - facebook === facebook2; // => false - ``` + function publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._detail); + } - @method optionsForType - @param {String} type - @param {Object} options - */ - optionsForType: function(type, options) { - if (this.parent) { illegalChildOperation('optionsForType'); } + publish(promise, promise._state = REJECTED); + } + }); +define("rsvp/promise/all", + ["../utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var isArray = __dependency1__.isArray; + var isNonThenable = __dependency1__.isNonThenable; - this._typeOptions.set(type, options); - }, + /** + `RSVP.Promise.all` accepts an array of promises, and returns a new promise which + is fulfilled with an array of fulfillment values for the passed promises, or + rejected with the reason of the first passed promise to be rejected. It casts all + elements of the passed iterable to promises as it runs this algorithm. - /** - @method options - @param {String} type - @param {Object} options - */ - options: function(type, options) { - this.optionsForType(type, options); - }, + Example: - /* - @private + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.resolve(2); + var promise3 = RSVP.resolve(3); + var promises = [ promise1, promise2, promise3 ]; - Used only via `injection`. + RSVP.Promise.all(promises).then(function(array){ + // The array here would be [ 1, 2, 3 ]; + }); + ``` - Provides a specialized form of injection, specifically enabling - all objects of one type to be injected with a reference to another - object. + If any of the `promises` given to `RSVP.all` are rejected, the first promise + that is rejected will be given as an argument to the returned promises's + rejection handler. For example: - For example, provided each object of type `controller` needed a `router`. - one would do the following: + Example: - ```javascript - var container = new Container(); + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; + + RSVP.Promise.all(promises).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(error) { + // error.message === "2" + }); + ``` - container.register('router:main', Router); - container.register('controller:user', UserController); - container.register('controller:post', PostController); + @method all + @for Ember.RSVP.Promise + @param {Array} entries array of promises + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all `promises` have been + fulfilled, or rejected if any of them become rejected. + @static + */ + __exports__["default"] = function all(entries, label) { - container.typeInjection('controller', 'router', 'router:main'); + /*jshint validthis:true */ + var Constructor = this; - var user = container.lookup('controller:user'); - var post = container.lookup('controller:post'); + return new Constructor(function(resolve, reject) { + if (!isArray(entries)) { + throw new TypeError('You must pass an array to all.'); + } - user.router instanceof Router; //=> true - post.router instanceof Router; //=> true + var remaining = entries.length; + var results = new Array(remaining); + var entry, pending = true; - // both controllers share the same router - user.router === post.router; //=> true - ``` + if (remaining === 0) { + resolve(results); + return; + } - @method typeInjection - @param {String} type - @param {String} property - @param {String} fullName - */ - typeInjection: function(type, property, fullName) { - if (this.parent) { illegalChildOperation('typeInjection'); } + function fulfillmentAt(index) { + return function(value) { + results[index] = value; + if (--remaining === 0) { + resolve(results); + } + }; + } - addTypeInjection(this.typeInjections, type, property, fullName); - }, + function onRejection(reason) { + remaining = 0; + reject(reason); + } - /* - Defines injection rules. + for (var index = 0; index < entries.length; index++) { + entry = entries[index]; + if (isNonThenable(entry)) { + results[index] = entry; + if (--remaining === 0) { + resolve(results); + } + } else { + Constructor.cast(entry).then(fulfillmentAt(index), onRejection); + } + } + }, label); + }; + }); +define("rsvp/promise/cast", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.Promise.cast` coerces its argument to a promise, or returns the + argument if it is already a promise which shares a constructor with the caster. - These rules are used to inject dependencies onto objects when they - are instantiated. + Example: - Two forms of injections are possible: + ```javascript + var promise = RSVP.Promise.resolve(1); + var casted = RSVP.Promise.cast(promise); - * Injecting one fullName on another fullName - * Injecting one fullName on a type + console.log(promise === casted); // true + ``` - Example: + In the case of a promise whose constructor does not match, it is assimilated. + The resulting promise will fulfill or reject based on the outcome of the + promise being casted. - ```javascript - var container = new Container(); + Example: - container.register('source:main', Source); - container.register('model:user', User); - container.register('model:post', Post); + ```javascript + var thennable = $.getJSON('/api/foo'); + var casted = RSVP.Promise.cast(thennable); - // injecting one fullName on another fullName - // eg. each user model gets a post model - container.injection('model:user', 'post', 'model:post'); + console.log(thennable === casted); // false + console.log(casted instanceof RSVP.Promise) // true - // injecting one fullName on another type - container.injection('model', 'source', 'source:main'); + casted.then(function(data) { + // data is the value getJSON fulfills with + }); + ``` - var user = container.lookup('model:user'); - var post = container.lookup('model:post'); + In the case of a non-promise, a promise which will fulfill with that value is + returned. - user.source instanceof Source; //=> true - post.source instanceof Source; //=> true + Example: - user.post instanceof Post; //=> true + ```javascript + var value = 1; // could be a number, boolean, string, undefined... + var casted = RSVP.Promise.cast(value); - // and both models share the same source - user.source === post.source; //=> true - ``` + console.log(value === casted); // false + console.log(casted instanceof RSVP.Promise) // true - @method injection - @param {String} factoryName - @param {String} property - @param {String} injectionName - */ - injection: function(factoryName, property, injectionName) { - if (this.parent) { illegalChildOperation('injection'); } + casted.then(function(val) { + val === value // => true + }); + ``` - if (factoryName.indexOf(':') === -1) { - return this.typeInjection(factoryName, property, injectionName); - } + `RSVP.Promise.cast` is similar to `RSVP.Promise.resolve`, but `RSVP.Promise.cast` differs in the + following ways: + + * `RSVP.Promise.cast` serves as a memory-efficient way of getting a promise, when you + have something that could either be a promise or a value. RSVP.resolve + will have the same effect but will create a new promise wrapper if the + argument is a promise. + * `RSVP.Promise.cast` is a way of casting incoming thenables or promise subclasses to + promises of the exact class specified, so that the resulting object's `then` is + ensured to have the behavior of the constructor you are calling cast on (i.e., RSVP.Promise). + + @method cast + @param {Object} object to be casted + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise + @static + */ - addInjection(this.injections, factoryName, property, injectionName); - }, + __exports__["default"] = function cast(object, label) { + /*jshint validthis:true */ + var Constructor = this; + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } - /* - @private + return new Constructor(function(resolve) { + resolve(object); + }, label); + }; + }); +define("rsvp/promise/race", + ["../utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /* global toString */ - Used only via `factoryInjection`. + var isArray = __dependency1__.isArray; + var isFunction = __dependency1__.isFunction; + var isNonThenable = __dependency1__.isNonThenable; - Provides a specialized form of injection, specifically enabling - all factory of one type to be injected with a reference to another - object. + /** + `RSVP.Promise.race` returns a new promise which is settled in the same way as the + first passed promise to settle. - For example, provided each factory of type `model` needed a `store`. - one would do the following: + Example: - ```javascript - var container = new Container(); + ```javascript + var promise1 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 1"); + }, 200); + }); - container.registerFactory('model:user', User); - container.register('store:main', SomeStore); + var promise2 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 2"); + }, 100); + }); - container.factoryTypeInjection('model', 'store', 'store:main'); + RSVP.Promise.race([promise1, promise2]).then(function(result){ + // result === "promise 2" because it was resolved before promise1 + // was resolved. + }); + ``` - var store = container.lookup('store:main'); - var UserFactory = container.lookupFactory('model:user'); + `RSVP.Promise.race` is deterministic in that only the state of the first + settled promise matters. For example, even if other promises given to the + `promises` array argument are resolved, but the first settled promise has + become rejected before the other promises became fulfilled, the returned + promise will become rejected: - UserFactory.store instanceof SomeStore; //=> true - ``` + ```javascript + var promise1 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 1"); + }, 200); + }); - @method factoryTypeInjection - @param {String} type - @param {String} property - @param {String} fullName - */ - factoryTypeInjection: function(type, property, fullName) { - if (this.parent) { illegalChildOperation('factoryTypeInjection'); } + var promise2 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + reject(new Error("promise 2")); + }, 100); + }); - addTypeInjection(this.factoryTypeInjections, type, property, fullName); - }, + RSVP.Promise.race([promise1, promise2]).then(function(result){ + // Code here never runs + }, function(reason){ + // reason.message === "promise2" because promise 2 became rejected before + // promise 1 became fulfilled + }); + ``` - /* - Defines factory injection rules. + An example real-world use case is implementing timeouts: - Similar to regular injection rules, but are run against factories, via - `Container#lookupFactory`. + ```javascript + RSVP.Promise.race([ajax('foo.json'), timeout(5000)]) + ``` - These rules are used to inject objects onto factories when they - are looked up. + @method race + @param {Array} promises array of promises to observe + @param {String} label optional string for describing the promise returned. + Useful for tooling. + @return {Promise} a promise which settles in the same way as the first passed + promise to settle. + @static + */ + __exports__["default"] = function race(entries, label) { + /*jshint validthis:true */ + var Constructor = this, entry; - Two forms of injections are possible: + return new Constructor(function(resolve, reject) { + if (!isArray(entries)) { + throw new TypeError('You must pass an array to race.'); + } - * Injecting one fullName on another fullName - * Injecting one fullName on a type + var pending = true; - Example: + function onFulfillment(value) { if (pending) { pending = false; resolve(value); } } + function onRejection(reason) { if (pending) { pending = false; reject(reason); } } - ```javascript - var container = new Container(); + for (var i = 0; i < entries.length; i++) { + entry = entries[i]; + if (isNonThenable(entry)) { + pending = false; + resolve(entry); + return; + } else { + Constructor.cast(entry).then(onFulfillment, onRejection); + } + } + }, label); + }; + }); +define("rsvp/promise/reject", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.Promise.reject` returns a promise rejected with the passed `reason`. + It is shorthand for the following: - container.register('store:main', Store); - container.register('store:secondary', OtherStore); - container.register('model:user', User); - container.register('model:post', Post); + ```javascript + var promise = new RSVP.Promise(function(resolve, reject){ + reject(new Error('WHOOPS')); + }); - // injecting one fullName on another type - container.factoryInjection('model', 'store', 'store:main'); + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` - // injecting one fullName on another fullName - container.factoryInjection('model:post', 'secondaryStore', 'store:secondary'); + Instead of writing the above, your code now simply becomes the following: - var UserFactory = container.lookupFactory('model:user'); - var PostFactory = container.lookupFactory('model:post'); - var store = container.lookup('store:main'); + ```javascript + var promise = RSVP.Promise.reject(new Error('WHOOPS')); - UserFactory.store instanceof Store; //=> true - UserFactory.secondaryStore instanceof OtherStore; //=> false + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` - PostFactory.store instanceof Store; //=> true - PostFactory.secondaryStore instanceof OtherStore; //=> true + @method reject + @param {Any} reason value that the returned promise will be rejected with. + @param {String} label optional string for identifying the returned promise. + Useful for tooling. + @return {Promise} a promise rejected with the given `reason`. + @static + */ + __exports__["default"] = function reject(reason, label) { + /*jshint validthis:true */ + var Constructor = this; - // and both models share the same source instance - UserFactory.store === PostFactory.store; //=> true - ``` + return new Constructor(function (resolve, reject) { + reject(reason); + }, label); + }; + }); +define("rsvp/promise/resolve", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.Promise.resolve` returns a promise that will become resolved with the + passed `value`. It is shorthand for the following: - @method factoryInjection - @param {String} factoryName - @param {String} property - @param {String} injectionName - */ - factoryInjection: function(factoryName, property, injectionName) { - if (this.parent) { illegalChildOperation('injection'); } + ```javascript + var promise = new RSVP.Promise(function(resolve, reject){ + resolve(1); + }); - if (factoryName.indexOf(':') === -1) { - return this.factoryTypeInjection(factoryName, property, injectionName); - } + promise.then(function(value){ + // value === 1 + }); + ``` - addInjection(this.factoryInjections, factoryName, property, injectionName); - }, + Instead of writing the above, your code now simply becomes the following: - /** - A depth first traversal, destroying the container, its descendant containers and all - their managed objects. + ```javascript + var promise = RSVP.Promise.resolve(1); - @method destroy - */ - destroy: function() { - this.isDestroyed = true; + promise.then(function(value){ + // value === 1 + }); + ``` - for (var i=0, l=this.children.length; i w. -*/ -Ember.compare = function compare(v, w) { - if (v === w) { return 0; } + /** + @property registry + @type InheritingDict + */ + registry: null, - var type1 = Ember.typeOf(v); - var type2 = Ember.typeOf(w); + /** + @property cache + @type InheritingDict + */ + cache: null, - var Comparable = Ember.Comparable; - if (Comparable) { - if (type1==='instance' && Comparable.detect(v.constructor)) { - return v.constructor.compare(v, w); - } + /** + @property typeInjections + @type InheritingDict + */ + typeInjections: null, - if (type2 === 'instance' && Comparable.detect(w.constructor)) { - return 1-w.constructor.compare(w, v); - } - } + /** + @property injections + @type Object + @default {} + */ + injections: null, - // If we haven't yet generated a reverse-mapping of Ember.ORDER_DEFINITION, - // do so now. - var mapping = Ember.ORDER_DEFINITION_MAPPING; - if (!mapping) { - var order = Ember.ORDER_DEFINITION; - mapping = Ember.ORDER_DEFINITION_MAPPING = {}; - var idx, len; - for (idx = 0, len = order.length; idx < len; ++idx) { - mapping[order[idx]] = idx; - } + /** + @private - // We no longer need Ember.ORDER_DEFINITION. - delete Ember.ORDER_DEFINITION; - } + @property _options + @type InheritingDict + @default null + */ + _options: null, - var type1Index = mapping[type1]; - var type2Index = mapping[type2]; + /** + @private - if (type1Index < type2Index) { return -1; } - if (type1Index > type2Index) { return 1; } + @property _typeOptions + @type InheritingDict + */ + _typeOptions: null, - // types are equal - so we have to check values now - switch (type1) { - case 'boolean': - case 'number': - if (v < w) { return -1; } - if (v > w) { return 1; } - return 0; + /** + Returns a new child of the current container. These children are configured + to correctly inherit from the current container. - case 'string': - var comp = v.localeCompare(w); - if (comp < 0) { return -1; } - if (comp > 0) { return 1; } - return 0; + @method child + @return {Container} + */ + child: function() { + var container = new Container(this); + this.children.push(container); + return container; + }, - case 'array': - var vLen = v.length; - var wLen = w.length; - var l = Math.min(vLen, wLen); - var r = 0; - var i = 0; - while (r === 0 && i < l) { - r = compare(v[i],w[i]); - i++; - } - if (r !== 0) { return r; } + /** + Sets a key-value pair on the current container. If a parent container, + has the same key, once set on a child, the parent and child will diverge + as expected. - // all elements are equal now - // shorter array should be ordered first - if (vLen < wLen) { return -1; } - if (vLen > wLen) { return 1; } - // arrays are equal now - return 0; + @method set + @param {Object} object + @param {String} key + @param {any} value + */ + set: function(object, key, value) { + object[key] = value; + }, - case 'instance': - if (Ember.Comparable && Ember.Comparable.detect(v)) { - return v.compare(v, w); - } - return 0; + /** + Registers a factory for later injection. - case 'date': - var vNum = v.getTime(); - var wNum = w.getTime(); - if (vNum < wNum) { return -1; } - if (vNum > wNum) { return 1; } - return 0; + Example: - default: - return 0; - } -}; + ```javascript + var container = new Container(); -function _copy(obj, deep, seen, copies) { - var ret, loc, key; + container.register('model:user', Person, {singleton: false }); + container.register('fruit:favorite', Orange); + container.register('communication:main', Email, {singleton: false}); + ``` - // primitive data types are immutable, just return them. - if ('object' !== typeof obj || obj===null) return obj; + @method register + @param {String} fullName + @param {Function} factory + @param {Object} options + */ + register: function(fullName, factory, options) { + validateFullName(fullName); - // avoid cyclical loops - if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; + if (factory === undefined) { + throw new TypeError('Attempting to register an unknown factory: `' + fullName + '`'); + } - Ember.assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj))); + var normalizedName = this.normalize(fullName); - // IMPORTANT: this specific test will detect a native array only. Any other - // object will need to implement Copyable. - if (Ember.typeOf(obj) === 'array') { - ret = obj.slice(); - if (deep) { - loc = ret.length; - while(--loc>=0) ret[loc] = _copy(ret[loc], deep, seen, copies); - } - } else if (Ember.Copyable && Ember.Copyable.detect(obj)) { - ret = obj.copy(deep, seen, copies); - } else { - ret = {}; - for(key in obj) { - if (!obj.hasOwnProperty(key)) continue; + if (this.cache.has(normalizedName)) { + throw new Error('Cannot re-register: `' + fullName +'`, as it has already been looked up.'); + } - // Prevents browsers that don't respect non-enumerability from - // copying internal Ember properties - if (key.substring(0,2) === '__') continue; + this.registry.set(normalizedName, factory); + this._options.set(normalizedName, options || {}); + }, - ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key]; - } - } + /** + Unregister a fullName - if (deep) { - seen.push(obj); - copies.push(ret); - } + ```javascript + var container = new Container(); + container.register('model:user', User); - return ret; -} + container.lookup('model:user') instanceof User //=> true -/** - Creates a clone of the passed object. This function can take just about - any type of object and create a clone of it, including primitive values - (which are not actually cloned because they are immutable). + container.unregister('model:user') + container.lookup('model:user') === undefined //=> true + ``` - If the passed object implements the `clone()` method, then this function - will simply call that method and return the result. + @method unregister + @param {String} fullName + */ + unregister: function(fullName) { + validateFullName(fullName); - @method copy - @for Ember - @param {Object} obj The object to clone - @param {Boolean} deep If true, a deep copy of the object is made - @return {Object} The cloned object -*/ -Ember.copy = function(obj, deep) { - // fast paths - if ('object' !== typeof obj || obj===null) return obj; // can't copy primitives - if (Ember.Copyable && Ember.Copyable.detect(obj)) return obj.copy(deep); - return _copy(obj, deep, deep ? [] : null, deep ? [] : null); -}; + var normalizedName = this.normalize(fullName); -/** - Convenience method to inspect an object. This method will attempt to - convert the object into a useful string description. + this.registry.remove(normalizedName); + this.cache.remove(normalizedName); + this.factoryCache.remove(normalizedName); + this.resolveCache.remove(normalizedName); + this._options.remove(normalizedName); + }, - It is a pretty simple implementation. If you want something more robust, - use something like JSDump: https://github.com/NV/jsDump + /** + Given a fullName return the corresponding factory. - @method inspect - @for Ember - @param {Object} obj The object you want to inspect. - @return {String} A description of the object -*/ -Ember.inspect = function(obj) { - var type = Ember.typeOf(obj); - if (type === 'array') { - return '[' + obj + ']'; - } - if (type !== 'object') { - return obj + ''; - } + By default `resolve` will retrieve the factory from + its container's registry. - var v, ret = []; - for(var key in obj) { - if (obj.hasOwnProperty(key)) { - v = obj[key]; - if (v === 'toString') { continue; } // ignore useless items - if (Ember.typeOf(v) === 'function') { v = "function() { ... }"; } - ret.push(key + ": " + v); - } - } - return "{" + ret.join(", ") + "}"; -}; + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); -/** - Compares two objects, returning true if they are logically equal. This is - a deeper comparison than a simple triple equal. For sets it will compare the - internal objects. For any other object that implements `isEqual()` it will - respect that method. + container.resolve('api:twitter') // => Twitter + ``` - ```javascript - Ember.isEqual('hello', 'hello'); // true - Ember.isEqual(1, 2); // false - Ember.isEqual([4,2], [4,2]); // false - ``` + Optionally the container can be provided with a custom resolver. + If provided, `resolve` will first provide the custom resolver + the oppertunity to resolve the fullName, otherwise it will fallback + to the registry. - @method isEqual - @for Ember - @param {Object} a first object to compare - @param {Object} b second object to compare - @return {Boolean} -*/ -Ember.isEqual = function(a, b) { - if (a && 'function'===typeof a.isEqual) return a.isEqual(b); - return a === b; -}; + ```javascript + var container = new Container(); + container.resolver = function(fullName) { + // lookup via the module system of choice + }; -// Used by Ember.compare -Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ - 'undefined', - 'null', - 'boolean', - 'number', - 'string', - 'array', - 'object', - 'instance', - 'function', - 'class', - 'date' -]; + // the twitter factory is added to the module system + container.resolve('api:twitter') // => Twitter + ``` -/** - Returns all of the keys defined on an object or hash. This is useful - when inspecting objects for debugging. On browsers that support it, this - uses the native `Object.keys` implementation. + @method resolve + @param {String} fullName + @return {Function} fullName's factory + */ + resolve: function(fullName) { + validateFullName(fullName); - @method keys - @for Ember - @param {Object} obj - @return {Array} Array containing keys of obj -*/ -Ember.keys = Object.keys; + var normalizedName = this.normalize(fullName); + var cached = this.resolveCache.get(normalizedName); -if (!Ember.keys || Ember.create.isSimulated) { - Ember.keys = function(obj) { - var ret = []; - for(var key in obj) { - // Prevents browsers that don't respect non-enumerability from - // copying internal Ember properties - if (key.substring(0,2) === '__') continue; - if (key === '_super') continue; + if (cached) { return cached; } - if (obj.hasOwnProperty(key)) { ret.push(key); } - } - return ret; - }; -} + var resolved = this.resolver(normalizedName) || this.registry.get(normalizedName); -// .......................................................... -// ERROR -// + this.resolveCache.set(normalizedName, resolved); -var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; + return resolved; + }, -/** - A subclass of the JavaScript Error object for use in Ember. + /** + A hook that can be used to describe how the resolver will + attempt to find the factory. - @class Error - @namespace Ember - @extends Error - @constructor -*/ -Ember.Error = function() { - var tmp = Error.apply(this, arguments); + For example, the default Ember `.describe` returns the full + class name (including namespace) where Ember's resolver expects + to find the `fullName`. - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } -}; + @method describe + @param {String} fullName + @return {string} described fullName + */ + describe: function(fullName) { + return fullName; + }, -Ember.Error.prototype = Ember.create(Error.prototype); + /** + A hook to enable custom fullName normalization behaviour -})(); + @method normalize + @param {String} fullName + @return {string} normalized fullName + */ + normalize: function(fullName) { + return fullName; + }, + /** + @method makeToString + @param {any} factory + @param {string} fullName + @return {function} toString function + */ + makeToString: function(factory, fullName) { + return factory.toString(); + }, -(function() { -/** -@module ember -@submodule ember-runtime -*/ + /** + Given a fullName return a corresponding instance. -// .......................................................... -// HELPERS -// + The default behaviour is for lookup to return a singleton instance. + The singleton is scoped to the container, allowing multiple containers + to all have their own locally scoped singletons. -var get = Ember.get, set = Ember.set; -var a_slice = Array.prototype.slice; -var a_indexOf = Ember.EnumerableUtils.indexOf; + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); -var contexts = []; + var twitter = container.lookup('api:twitter'); -function popCtx() { - return contexts.length===0 ? {} : contexts.pop(); -} + twitter instanceof Twitter; // => true -function pushCtx(ctx) { - contexts.push(ctx); - return null; -} + // by default the container will return singletons + var twitter2 = container.lookup('api:twitter'); + twitter instanceof Twitter; // => true -function iter(key, value) { - var valueProvided = arguments.length === 2; + twitter === twitter2; //=> true + ``` - function i(item) { - var cur = get(item, key); - return valueProvided ? value===cur : !!cur; - } - return i ; -} + If singletons are not wanted an optional flag can be provided at lookup. -/** - This mixin defines the common interface implemented by enumerable objects - in Ember. Most of these methods follow the standard Array iteration - API defined up to JavaScript 1.8 (excluding language-specific features that - cannot be emulated in older versions of JavaScript). + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); - This mixin is applied automatically to the Array class on page load, so you - can use any of these methods on simple arrays. If Array already implements - one of these methods, the mixin will not override them. + var twitter = container.lookup('api:twitter', { singleton: false }); + var twitter2 = container.lookup('api:twitter', { singleton: false }); - ## Writing Your Own Enumerable + twitter === twitter2; //=> false + ``` - To make your own custom class enumerable, you need two items: + @method lookup + @param {String} fullName + @param {Object} options + @return {any} + */ + lookup: function(fullName, options) { + validateFullName(fullName); + return lookup(this, this.normalize(fullName), options); + }, - 1. You must have a length property. This property should change whenever - the number of items in your enumerable object changes. If you using this - with an `Ember.Object` subclass, you should be sure to change the length - property using `set().` + /** + Given a fullName return the corresponding factory. - 2. If you must implement `nextObject().` See documentation. + @method lookupFactory + @param {String} fullName + @return {any} + */ + lookupFactory: function(fullName) { + validateFullName(fullName); + return factoryFor(this, this.normalize(fullName)); + }, - Once you have these two methods implement, apply the `Ember.Enumerable` mixin - to your class and you will be able to enumerate the contents of your object - like any other collection. + /** + Given a fullName check if the container is aware of its factory + or singleton instance. - ## Using Ember Enumeration with Other Libraries + @method has + @param {String} fullName + @return {Boolean} + */ + has: function(fullName) { + validateFullName(fullName); + return has(this, this.normalize(fullName)); + }, - Many other libraries provide some kind of iterator or enumeration like - facility. This is often where the most common API conflicts occur. - Ember's API is designed to be as friendly as possible with other - libraries by implementing only methods that mostly correspond to the - JavaScript 1.8 API. + /** + Allow registering options for all factories of a type. - @class Enumerable - @namespace Ember - @since Ember 0.9 -*/ -Ember.Enumerable = Ember.Mixin.create({ + ```javascript + var container = new Container(); - /** - Implement this method to make your class enumerable. + // if all of type `connection` must not be singletons + container.optionsForType('connection', { singleton: false }); - This method will be call repeatedly during enumeration. The index value - will always begin with 0 and increment monotonically. You don't have to - rely on the index value to determine what object to return, but you should - always check the value and start from the beginning when you see the - requested index is 0. + container.register('connection:twitter', TwitterConnection); + container.register('connection:facebook', FacebookConnection); - The `previousObject` is the object that was returned from the last call - to `nextObject` for the current iteration. This is a useful way to - manage iteration if you are tracing a linked list, for example. + var twitter = container.lookup('connection:twitter'); + var twitter2 = container.lookup('connection:twitter'); - Finally the context parameter will always contain a hash you can use as - a "scratchpad" to maintain any other state you need in order to iterate - properly. The context object is reused and is not reset between - iterations so make sure you setup the context with a fresh state whenever - the index parameter is 0. + twitter === twitter2; // => false - Generally iterators will continue to call `nextObject` until the index - reaches the your current length-1. If you run out of data before this - time for some reason, you should simply return undefined. + var facebook = container.lookup('connection:facebook'); + var facebook2 = container.lookup('connection:facebook'); - The default implementation of this method simply looks up the index. - This works great on any Array-like objects. + facebook === facebook2; // => false + ``` - @method nextObject - @param {Number} index the current index of the iteration - @param {Object} previousObject the value returned by the last call to - `nextObject`. - @param {Object} context a context object you can use to maintain state. - @return {Object} the next object in the iteration or undefined - */ - nextObject: Ember.required(Function), + @method optionsForType + @param {String} type + @param {Object} options + */ + optionsForType: function(type, options) { + if (this.parent) { illegalChildOperation('optionsForType'); } - /** - Helper method returns the first object from a collection. This is usually - used by bindings and other parts of the framework to extract a single - object if the enumerable contains only one item. + this._typeOptions.set(type, options); + }, - If you override this method, you should implement it so that it will - always return the same value each time it is called. If your enumerable - contains only one object, this method should always return that object. - If your enumerable is empty, this method should return `undefined`. + /** + @method options + @param {String} type + @param {Object} options + */ + options: function(type, options) { + this.optionsForType(type, options); + }, - ```javascript - var arr = ["a", "b", "c"]; - arr.get('firstObject'); // "a" + /** + Used only via `injection`. - var arr = []; - arr.get('firstObject'); // undefined - ``` + Provides a specialized form of injection, specifically enabling + all objects of one type to be injected with a reference to another + object. - @property firstObject - @return {Object} the object or undefined - */ - firstObject: Ember.computed(function() { - if (get(this, 'length')===0) return undefined ; + For example, provided each object of type `controller` needed a `router`. + one would do the following: - // handle generic enumerables - var context = popCtx(), ret; - ret = this.nextObject(0, null, context); - pushCtx(context); - return ret ; - }).property('[]'), + ```javascript + var container = new Container(); - /** - Helper method returns the last object from a collection. If your enumerable - contains only one object, this method should always return that object. - If your enumerable is empty, this method should return `undefined`. + container.register('router:main', Router); + container.register('controller:user', UserController); + container.register('controller:post', PostController); - ```javascript - var arr = ["a", "b", "c"]; - arr.get('lastObject'); // "c" + container.typeInjection('controller', 'router', 'router:main'); - var arr = []; - arr.get('lastObject'); // undefined - ``` + var user = container.lookup('controller:user'); + var post = container.lookup('controller:post'); - @property lastObject - @return {Object} the last object or undefined - */ - lastObject: Ember.computed(function() { - var len = get(this, 'length'); - if (len===0) return undefined ; - var context = popCtx(), idx=0, cur, last = null; - do { - last = cur; - cur = this.nextObject(idx++, last, context); - } while (cur !== undefined); - pushCtx(context); - return last; - }).property('[]'), + user.router instanceof Router; //=> true + post.router instanceof Router; //=> true - /** - Returns `true` if the passed object can be found in the receiver. The - default version will iterate through the enumerable until the object - is found. You may want to override this with a more efficient version. + // both controllers share the same router + user.router === post.router; //=> true + ``` - ```javascript - var arr = ["a", "b", "c"]; - arr.contains("a"); // true - arr.contains("z"); // false - ``` + @private + @method typeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + typeInjection: function(type, property, fullName) { + validateFullName(fullName); + if (this.parent) { illegalChildOperation('typeInjection'); } - @method contains - @param {Object} obj The object to search for. - @return {Boolean} `true` if object is found in enumerable. - */ - contains: function(obj) { - return this.find(function(item) { return item===obj; }) !== undefined; - }, + addTypeInjection(this.typeInjections, type, property, fullName); + }, - /** - Iterates through the enumerable, calling the passed function on each - item. This method corresponds to the `forEach()` method defined in - JavaScript 1.6. + /** + Defines injection rules. - The callback method you provide should have the following signature (all - parameters are optional): + These rules are used to inject dependencies onto objects when they + are instantiated. - ```javascript - function(item, index, enumerable); - ``` + Two forms of injections are possible: - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. + * Injecting one fullName on another fullName + * Injecting one fullName on a type - Note that in addition to a callback, you can also pass an optional target - object that will be set as `this` on the context. This is a good way - to give your iterator function access to the current object. + Example: - @method forEach - @param {Function} callback The callback to execute - @param {Object} [target] The target object to use - @return {Object} receiver - */ - forEach: function(callback, target) { - if (typeof callback !== "function") throw new TypeError() ; - var len = get(this, 'length'), last = null, context = popCtx(); + ```javascript + var container = new Container(); - if (target === undefined) target = null; + container.register('source:main', Source); + container.register('model:user', User); + container.register('model:post', Post); - for(var idx=0;idx true + post.source instanceof Source; //=> true - @method setEach - @param {String} key The key to set - @param {Object} value The object to set - @return {Object} receiver - */ - setEach: function(key, value) { - return this.forEach(function(item) { - set(item, key, value); - }); - }, + user.post instanceof Post; //=> true - /** - Maps all of the items in the enumeration to another value, returning - a new array. This method corresponds to `map()` defined in JavaScript 1.6. + // and both models share the same source + user.source === post.source; //=> true + ``` - The callback method you provide should have the following signature (all - parameters are optional): + @method injection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + injection: function(fullName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } - ```javascript - function(item, index, enumerable); - ``` + validateFullName(injectionName); + var normalizedInjectionName = this.normalize(injectionName); - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. + if (fullName.indexOf(':') === -1) { + return this.typeInjection(fullName, property, normalizedInjectionName); + } - It should return the mapped value. + validateFullName(fullName); + var normalizedName = this.normalize(fullName); - Note that in addition to a callback, you can also pass an optional target - object that will be set as `this` on the context. This is a good way - to give your iterator function access to the current object. + addInjection(this.injections, normalizedName, property, normalizedInjectionName); + }, - @method map - @param {Function} callback The callback to execute - @param {Object} [target] The target object to use - @return {Array} The mapped array. - */ - map: function(callback, target) { - var ret = Ember.A(); - this.forEach(function(x, idx, i) { - ret[idx] = callback.call(target, x, idx,i); - }); - return ret ; - }, - /** - Similar to map, this specialized function returns the value of the named - property on all items in the enumeration. + /** + Used only via `factoryInjection`. - @method mapBy - @param {String} key name of the property - @return {Array} The mapped array. - */ - mapBy: function(key) { - return this.map(function(next) { - return get(next, key); - }); - }, + Provides a specialized form of injection, specifically enabling + all factory of one type to be injected with a reference to another + object. - /** - Similar to map, this specialized function returns the value of the named - property on all items in the enumeration. + For example, provided each factory of type `model` needed a `store`. + one would do the following: - @method mapProperty - @param {String} key name of the property - @return {Array} The mapped array. - @deprecated Use `mapBy` instead - */ + ```javascript + var container = new Container(); - mapProperty: Ember.aliasMethod('mapBy'), + container.register('store:main', SomeStore); - /** - Returns an array with all of the items in the enumeration that the passed - function returns true for. This method corresponds to `filter()` defined in - JavaScript 1.6. + container.factoryTypeInjection('model', 'store', 'store:main'); - The callback method you provide should have the following signature (all - parameters are optional): + var store = container.lookup('store:main'); + var UserFactory = container.lookupFactory('model:user'); - ```javascript - function(item, index, enumerable); - ``` + UserFactory.store instanceof SomeStore; //=> true + ``` - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. + @private + @method factoryTypeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + factoryTypeInjection: function(type, property, fullName) { + if (this.parent) { illegalChildOperation('factoryTypeInjection'); } - It should return the `true` to include the item in the results, `false` - otherwise. + addTypeInjection(this.factoryTypeInjections, type, property, this.normalize(fullName)); + }, - Note that in addition to a callback, you can also pass an optional target - object that will be set as `this` on the context. This is a good way - to give your iterator function access to the current object. + /** + Defines factory injection rules. - @method filter - @param {Function} callback The callback to execute - @param {Object} [target] The target object to use - @return {Array} A filtered array. - */ - filter: function(callback, target) { - var ret = Ember.A(); - this.forEach(function(x, idx, i) { - if (callback.call(target, x, idx, i)) ret.push(x); - }); - return ret ; - }, + Similar to regular injection rules, but are run against factories, via + `Container#lookupFactory`. - /** - Returns an array with all of the items in the enumeration where the passed - function returns false for. This method is the inverse of filter(). + These rules are used to inject objects onto factories when they + are looked up. - The callback method you provide should have the following signature (all - parameters are optional): + Two forms of injections are possible: - function(item, index, enumerable); + * Injecting one fullName on another fullName + * Injecting one fullName on a type - - *item* is the current item in the iteration. - - *index* is the current index in the iteration - - *enumerable* is the enumerable object itself. + Example: - It should return the a falsey value to include the item in the results. + ```javascript + var container = new Container(); - Note that in addition to a callback, you can also pass an optional target - object that will be set as "this" on the context. This is a good way - to give your iterator function access to the current object. + container.register('store:main', Store); + container.register('store:secondary', OtherStore); + container.register('model:user', User); + container.register('model:post', Post); - @method reject - @param {Function} callback The callback to execute - @param {Object} [target] The target object to use - @return {Array} A rejected array. - */ - reject: function(callback, target) { - return this.filter(function() { - return !(callback.apply(target, arguments)); - }); - }, + // injecting one fullName on another type + container.factoryInjection('model', 'store', 'store:main'); - /** - Returns an array with just the items with the matched property. You - can pass an optional second argument with the target value. Otherwise - this will match any property that evaluates to `true`. + // injecting one fullName on another fullName + container.factoryInjection('model:post', 'secondaryStore', 'store:secondary'); - @method filterBy - @param {String} key the property to test - @param {String} [value] optional value to test against. - @return {Array} filtered array - */ - filterBy: function(key, value) { - return this.filter(iter.apply(this, arguments)); - }, + var UserFactory = container.lookupFactory('model:user'); + var PostFactory = container.lookupFactory('model:post'); + var store = container.lookup('store:main'); - /** - Returns an array with just the items with the matched property. You - can pass an optional second argument with the target value. Otherwise - this will match any property that evaluates to `true`. + UserFactory.store instanceof Store; //=> true + UserFactory.secondaryStore instanceof OtherStore; //=> false - @method filterProperty - @param {String} key the property to test - @param {String} [value] optional value to test against. - @return {Array} filtered array - @deprecated Use `filterBy` instead - */ - filterProperty: Ember.aliasMethod('filterBy'), + PostFactory.store instanceof Store; //=> true + PostFactory.secondaryStore instanceof OtherStore; //=> true - /** - Returns an array with the items that do not have truthy values for - key. You can pass an optional second argument with the target value. Otherwise - this will match any property that evaluates to false. + // and both models share the same source instance + UserFactory.store === PostFactory.store; //=> true + ``` - @method rejectBy - @param {String} key the property to test - @param {String} [value] optional value to test against. - @return {Array} rejected array - */ - rejectBy: function(key, value) { - var exactValue = function(item) { return get(item, key) === value; }, - hasValue = function(item) { return !!get(item, key); }, - use = (arguments.length === 2 ? exactValue : hasValue); + @method factoryInjection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + factoryInjection: function(fullName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } - return this.reject(use); - }, + var normalizedName = this.normalize(fullName); + var normalizedInjectionName = this.normalize(injectionName); - /** - Returns an array with the items that do not have truthy values for - key. You can pass an optional second argument with the target value. Otherwise - this will match any property that evaluates to false. + validateFullName(injectionName); - @method rejectProperty - @param {String} key the property to test - @param {String} [value] optional value to test against. - @return {Array} rejected array - @deprecated Use `rejectBy` instead - */ - rejectProperty: Ember.aliasMethod('rejectBy'), + if (fullName.indexOf(':') === -1) { + return this.factoryTypeInjection(normalizedName, property, normalizedInjectionName); + } - /** - Returns the first item in the array for which the callback returns true. - This method works similar to the `filter()` method defined in JavaScript 1.6 - except that it will stop working on the array once a match is found. + validateFullName(fullName); - The callback method you provide should have the following signature (all - parameters are optional): + addInjection(this.factoryInjections, normalizedName, property, normalizedInjectionName); + }, - ```javascript - function(item, index, enumerable); - ``` + /** + A depth first traversal, destroying the container, its descendant containers and all + their managed objects. - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. + @method destroy + */ + destroy: function() { + for (var i=0, l=this.children.length; i1) args = a_slice.call(arguments, 1); +})(); - this.forEach(function(x, idx) { - var method = x && x[methodName]; - if ('function' === typeof method) { - ret[idx] = args ? method.apply(x, args) : method.call(x); - } - }, this); +(function() { +/*globals ENV */ +/** +@module ember +@submodule ember-runtime +*/ - return ret; - }, +var indexOf = Ember.EnumerableUtils.indexOf; - /** - Simply converts the enumerable into a genuine array. The order is not - guaranteed. Corresponds to the method implemented by Prototype. +/** + This will compare two javascript values of possibly different types. + It will tell you which one is greater than the other by returning: - @method toArray - @return {Array} the enumerable as an array. - */ - toArray: function() { - var ret = Ember.A(); - this.forEach(function(o, idx) { ret[idx] = o; }); - return ret ; - }, + - -1 if the first is smaller than the second, + - 0 if both are equal, + - 1 if the first is greater than the second. - /** - Returns a copy of the array with all null and undefined elements removed. + The order is calculated based on `Ember.ORDER_DEFINITION`, if types are different. + In case they have the same type an appropriate comparison for this type is made. - ```javascript - var arr = ["a", null, "c", undefined]; - arr.compact(); // ["a", "c"] - ``` + ```javascript + Ember.compare('hello', 'hello'); // 0 + Ember.compare('abc', 'dfg'); // -1 + Ember.compare(2, 1); // 1 + ``` - @method compact - @return {Array} the array without null and undefined elements. - */ - compact: function() { - return this.filter(function(value) { return value != null; }); - }, + @method compare + @for Ember + @param {Object} v First value to compare + @param {Object} w Second value to compare + @return {Number} -1 if v < w, 0 if v = w and 1 if v > w. +*/ +Ember.compare = function compare(v, w) { + if (v === w) { return 0; } - /** - Returns a new enumerable that excludes the passed value. The default - implementation returns an array regardless of the receiver type unless - the receiver does not contain the value. + var type1 = Ember.typeOf(v); + var type2 = Ember.typeOf(w); - ```javascript - var arr = ["a", "b", "a", "c"]; - arr.without("a"); // ["b", "c"] - ``` + var Comparable = Ember.Comparable; + if (Comparable) { + if (type1==='instance' && Comparable.detect(v.constructor)) { + return v.constructor.compare(v, w); + } - @method without - @param {Object} value - @return {Ember.Enumerable} - */ - without: function(value) { - if (!this.contains(value)) return this; // nothing to do - var ret = Ember.A(); - this.forEach(function(k) { - if (k !== value) ret[ret.length] = k; - }) ; - return ret ; - }, + if (type2 === 'instance' && Comparable.detect(w.constructor)) { + return 1-w.constructor.compare(w, v); + } + } - /** - Returns a new enumerable that contains only unique values. The default - implementation returns an array regardless of the receiver type. + // If we haven't yet generated a reverse-mapping of Ember.ORDER_DEFINITION, + // do so now. + var mapping = Ember.ORDER_DEFINITION_MAPPING; + if (!mapping) { + var order = Ember.ORDER_DEFINITION; + mapping = Ember.ORDER_DEFINITION_MAPPING = {}; + var idx, len; + for (idx = 0, len = order.length; idx < len; ++idx) { + mapping[order[idx]] = idx; + } - ```javascript - var arr = ["a", "a", "b", "b"]; - arr.uniq(); // ["a", "b"] - ``` + // We no longer need Ember.ORDER_DEFINITION. + delete Ember.ORDER_DEFINITION; + } - @method uniq - @return {Ember.Enumerable} - */ - uniq: function() { - var ret = Ember.A(); - this.forEach(function(k) { - if (a_indexOf(ret, k)<0) ret.push(k); - }); - return ret; - }, + var type1Index = mapping[type1]; + var type2Index = mapping[type2]; - /** - This property will trigger anytime the enumerable's content changes. - You can observe this property to be notified of changes to the enumerables - content. + if (type1Index < type2Index) { return -1; } + if (type1Index > type2Index) { return 1; } - For plain enumerables, this property is read only. `Ember.Array` overrides - this method. + // types are equal - so we have to check values now + switch (type1) { + case 'boolean': + case 'number': + if (v < w) { return -1; } + if (v > w) { return 1; } + return 0; - @property [] - @type Ember.Array - @return this - */ - '[]': Ember.computed(function(key, value) { - return this; - }), + case 'string': + var comp = v.localeCompare(w); + if (comp < 0) { return -1; } + if (comp > 0) { return 1; } + return 0; - // .......................................................... - // ENUMERABLE OBSERVERS - // + case 'array': + var vLen = v.length; + var wLen = w.length; + var l = Math.min(vLen, wLen); + var r = 0; + var i = 0; + while (r === 0 && i < l) { + r = compare(v[i],w[i]); + i++; + } + if (r !== 0) { return r; } - /** - Registers an enumerable observer. Must implement `Ember.EnumerableObserver` - mixin. + // all elements are equal now + // shorter array should be ordered first + if (vLen < wLen) { return -1; } + if (vLen > wLen) { return 1; } + // arrays are equal now + return 0; - @method addEnumerableObserver - @param {Object} target - @param {Hash} [opts] - @return this - */ - addEnumerableObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'enumerableWillChange', - didChange = (opts && opts.didChange) || 'enumerableDidChange'; + case 'instance': + if (Ember.Comparable && Ember.Comparable.detect(v)) { + return v.compare(v, w); + } + return 0; - var hasObservers = get(this, 'hasEnumerableObservers'); - if (!hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); - Ember.addListener(this, '@enumerable:before', target, willChange); - Ember.addListener(this, '@enumerable:change', target, didChange); - if (!hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); - return this; - }, + case 'date': + var vNum = v.getTime(); + var wNum = w.getTime(); + if (vNum < wNum) { return -1; } + if (vNum > wNum) { return 1; } + return 0; - /** - Removes a registered enumerable observer. + default: + return 0; + } +}; - @method removeEnumerableObserver - @param {Object} target - @param {Hash} [opts] - @return this - */ - removeEnumerableObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'enumerableWillChange', - didChange = (opts && opts.didChange) || 'enumerableDidChange'; +function _copy(obj, deep, seen, copies) { + var ret, loc, key; - var hasObservers = get(this, 'hasEnumerableObservers'); - if (hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); - Ember.removeListener(this, '@enumerable:before', target, willChange); - Ember.removeListener(this, '@enumerable:change', target, didChange); - if (hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); - return this; - }, + // primitive data types are immutable, just return them. + if ('object' !== typeof obj || obj===null) return obj; - /** - Becomes true whenever the array currently has observers watching changes - on the array. + // avoid cyclical loops + if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; - @property hasEnumerableObservers - @type Boolean - */ - hasEnumerableObservers: Ember.computed(function() { - return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before'); - }), + Ember.assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj))); + // IMPORTANT: this specific test will detect a native array only. Any other + // object will need to implement Copyable. + if (Ember.typeOf(obj) === 'array') { + ret = obj.slice(); + if (deep) { + loc = ret.length; + while(--loc>=0) ret[loc] = _copy(ret[loc], deep, seen, copies); + } + } else if (Ember.Copyable && Ember.Copyable.detect(obj)) { + ret = obj.copy(deep, seen, copies); + } else { + ret = {}; + for(key in obj) { + if (!obj.hasOwnProperty(key)) continue; - /** - Invoke this method just before the contents of your enumerable will - change. You can either omit the parameters completely or pass the objects - to be removed or added if available or just a count. + // Prevents browsers that don't respect non-enumerability from + // copying internal Ember properties + if (key.substring(0,2) === '__') continue; - @method enumerableContentWillChange - @param {Ember.Enumerable|Number} removing An enumerable of the objects to - be removed or the number of items to be removed. - @param {Ember.Enumerable|Number} adding An enumerable of the objects to be - added or the number of items to be added. - @chainable - */ - enumerableContentWillChange: function(removing, adding) { + ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key]; + } + } - var removeCnt, addCnt, hasDelta; + if (deep) { + seen.push(obj); + copies.push(ret); + } - if ('number' === typeof removing) removeCnt = removing; - else if (removing) removeCnt = get(removing, 'length'); - else removeCnt = removing = -1; + return ret; +} - if ('number' === typeof adding) addCnt = adding; - else if (adding) addCnt = get(adding,'length'); - else addCnt = adding = -1; +/** + Creates a clone of the passed object. This function can take just about + any type of object and create a clone of it, including primitive values + (which are not actually cloned because they are immutable). - hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + If the passed object implements the `clone()` method, then this function + will simply call that method and return the result. - if (removing === -1) removing = null; - if (adding === -1) adding = null; + @method copy + @for Ember + @param {Object} obj The object to clone + @param {Boolean} deep If true, a deep copy of the object is made + @return {Object} The cloned object +*/ +Ember.copy = function(obj, deep) { + // fast paths + if ('object' !== typeof obj || obj===null) return obj; // can't copy primitives + if (Ember.Copyable && Ember.Copyable.detect(obj)) return obj.copy(deep); + return _copy(obj, deep, deep ? [] : null, deep ? [] : null); +}; - Ember.propertyWillChange(this, '[]'); - if (hasDelta) Ember.propertyWillChange(this, 'length'); - Ember.sendEvent(this, '@enumerable:before', [this, removing, adding]); +/** + Compares two objects, returning true if they are logically equal. This is + a deeper comparison than a simple triple equal. For sets it will compare the + internal objects. For any other object that implements `isEqual()` it will + respect that method. - return this; - }, + ```javascript + Ember.isEqual('hello', 'hello'); // true + Ember.isEqual(1, 2); // false + Ember.isEqual([4,2], [4,2]); // false + ``` - /** - Invoke this method when the contents of your enumerable has changed. - This will notify any observers watching for content changes. If your are - implementing an ordered enumerable (such as an array), also pass the - start and end values where the content changed so that it can be used to - notify range observers. + @method isEqual + @for Ember + @param {Object} a first object to compare + @param {Object} b second object to compare + @return {Boolean} +*/ +Ember.isEqual = function(a, b) { + if (a && 'function'===typeof a.isEqual) return a.isEqual(b); + return a === b; +}; - @method enumerableContentDidChange - @param {Number} [start] optional start offset for the content change. - For unordered enumerables, you should always pass -1. - @param {Ember.Enumerable|Number} removing An enumerable of the objects to - be removed or the number of items to be removed. - @param {Ember.Enumerable|Number} adding An enumerable of the objects to - be added or the number of items to be added. - @chainable - */ - enumerableContentDidChange: function(removing, adding) { - var removeCnt, addCnt, hasDelta; +// Used by Ember.compare +Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ + 'undefined', + 'null', + 'boolean', + 'number', + 'string', + 'array', + 'object', + 'instance', + 'function', + 'class', + 'date' +]; - if ('number' === typeof removing) removeCnt = removing; - else if (removing) removeCnt = get(removing, 'length'); - else removeCnt = removing = -1; +/** + Returns all of the keys defined on an object or hash. This is useful + when inspecting objects for debugging. On browsers that support it, this + uses the native `Object.keys` implementation. - if ('number' === typeof adding) addCnt = adding; - else if (adding) addCnt = get(adding, 'length'); - else addCnt = adding = -1; + @method keys + @for Ember + @param {Object} obj + @return {Array} Array containing keys of obj +*/ +Ember.keys = Object.keys; - hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; +if (!Ember.keys || Ember.create.isSimulated) { + var prototypeProperties = [ + 'constructor', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'valueOf', + 'toLocaleString', + 'toString' + ], + pushPropertyName = function(obj, array, key) { + // Prevents browsers that don't respect non-enumerability from + // copying internal Ember properties + if (key.substring(0,2) === '__') return; + if (key === '_super') return; + if (indexOf(array, key) >= 0) return; + if (!obj.hasOwnProperty(key)) return; + + array.push(key); + }; - if (removing === -1) removing = null; - if (adding === -1) adding = null; + Ember.keys = function(obj) { + var ret = [], key; + for (key in obj) { + pushPropertyName(obj, ret, key); + } - Ember.sendEvent(this, '@enumerable:change', [this, removing, adding]); - if (hasDelta) Ember.propertyDidChange(this, 'length'); - Ember.propertyDidChange(this, '[]'); + // IE8 doesn't enumerate property that named the same as prototype properties. + for (var i = 0, l = prototypeProperties.length; i < l; i++) { + key = prototypeProperties[i]; - return this ; - } + pushPropertyName(obj, ret, key); + } -}) ; + return ret; + }; +} })(); @@ -10448,2002 +11663,2009 @@ Ember.Enumerable = Ember.Mixin.create({ @submodule ember-runtime */ -// .......................................................... -// HELPERS -// - -var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; +var STRING_DASHERIZE_REGEXP = (/[ _]/g); +var STRING_DASHERIZE_CACHE = {}; +var STRING_DECAMELIZE_REGEXP = (/([a-z\d])([A-Z])/g); +var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); +var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); +var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); +var STRING_PARAMETERIZE_REGEXP_1 = (/[_|\/|\s]+/g); +var STRING_PARAMETERIZE_REGEXP_2 = (/[^a-z0-9\-]+/gi); +var STRING_PARAMETERIZE_REGEXP_3 = (/[\-]+/g); +var STRING_PARAMETERIZE_REGEXP_4 = (/^-+|-+$/g); -// .......................................................... -// ARRAY -// /** - This module implements Observer-friendly Array-like behavior. This mixin is - picked up by the Array class as well as other controllers, etc. that want to - appear to be arrays. - - Unlike `Ember.Enumerable,` this mixin defines methods specifically for - collections that provide index-ordered access to their contents. When you - are designing code that needs to accept any kind of Array-like object, you - should use these methods instead of Array primitives because these will - properly notify observers of changes to the array. - - Although these methods are efficient, they do add a layer of indirection to - your application so it is a good idea to use them only when you need the - flexibility of using both true JavaScript arrays and "virtual" arrays such - as controllers and collections. - - You can use the methods defined in this module to access and modify array - contents in a KVO-friendly way. You can also be notified whenever the - membership of an array changes by changing the syntax of the property to - `.observes('*myProperty.[]')`. + Defines the hash of localized strings for the current language. Used by + the `Ember.String.loc()` helper. To localize, add string values to this + hash. - To support `Ember.Array` in your own class, you must override two - primitives to use it: `replace()` and `objectAt()`. + @property STRINGS + @for Ember + @type Hash +*/ +Ember.STRINGS = {}; - Note that the Ember.Array mixin also incorporates the `Ember.Enumerable` - mixin. All `Ember.Array`-like objects are also enumerable. +/** + Defines string helper methods including string formatting and localization. + Unless `Ember.EXTEND_PROTOTYPES.String` is `false` these methods will also be + added to the `String.prototype` as well. - @class Array + @class String @namespace Ember - @uses Ember.Enumerable - @since Ember 0.9.0 + @static */ -Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ { +Ember.String = { /** - Your array must support the `length` property. Your replace methods should - set this property whenever it changes. + Apply formatting options to the string. This will look for occurrences + of "%@" in your string and substitute them with the arguments you pass into + this method. If you want to control the specific order of replacement, + you can add a number after the key as well to indicate which argument + you want to insert. - @property {Number} length + Ordered insertions are most useful when building loc strings where values + you need to insert may appear in different orders. + + ```javascript + "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" + "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" + ``` + + @method fmt + @param {String} str The string to format + @param {Array} formats An array of parameters to interpolate into string. + @return {String} formatted string */ - length: Ember.required(), + fmt: function(str, formats) { + // first, replace any ORDERED replacements. + var idx = 0; // the current index for non-numerical replacements + return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { + argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; + s = formats[argIndex]; + return (s === null) ? '(null)' : (s === undefined) ? '' : Ember.inspect(s); + }) ; + }, /** - Returns the object at the given `index`. If the given `index` is negative - or is greater or equal than the array length, returns `undefined`. + Formats the passed string, but first looks up the string in the localized + strings hash. This is a convenient way to localize text. See + `Ember.String.fmt()` for more information on formatting. - This is one of the primitives you must implement to support `Ember.Array`. - If your object supports retrieving the value of an array item using `get()` - (i.e. `myArray.get(0)`), then you do not need to implement this method - yourself. + Note that it is traditional but not required to prefix localized string + keys with an underscore or other character so you can easily identify + localized strings. ```javascript - var arr = ['a', 'b', 'c', 'd']; - arr.objectAt(0); // "a" - arr.objectAt(3); // "d" - arr.objectAt(-1); // undefined - arr.objectAt(4); // undefined - arr.objectAt(5); // undefined + Ember.STRINGS = { + '_Hello World': 'Bonjour le monde', + '_Hello %@ %@': 'Bonjour %@ %@' + }; + + Ember.String.loc("_Hello World"); // 'Bonjour le monde'; + Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith"; ``` - @method objectAt - @param {Number} idx The index of the item to return. - @return {*} item at index or undefined + @method loc + @param {String} str The string to format + @param {Array} formats Optional array of parameters to interpolate into string. + @return {String} formatted string */ - objectAt: function(idx) { - if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; - return get(this, idx); + loc: function(str, formats) { + str = Ember.STRINGS[str] || str; + return Ember.String.fmt(str, formats) ; }, /** - This returns the objects at the specified indexes, using `objectAt`. + Splits a string into separate units separated by spaces, eliminating any + empty strings in the process. This is a convenience method for split that + is mostly useful when applied to the `String.prototype`. ```javascript - var arr = ['a', 'b', 'c', 'd']; - arr.objectsAt([0, 1, 2]); // ["a", "b", "c"] - arr.objectsAt([2, 3, 4]); // ["c", "d", undefined] + Ember.String.w("alpha beta gamma").forEach(function(key) { + console.log(key); + }); + + // > alpha + // > beta + // > gamma ``` - @method objectsAt - @param {Array} indexes An array of indexes of items to return. - @return {Array} - */ - objectsAt: function(indexes) { - var self = this; - return map(indexes, function(idx) { return self.objectAt(idx); }); - }, + @method w + @param {String} str The string to split + @return {String} split string + */ + w: function(str) { return str.split(/\s+/); }, - // overrides Ember.Enumerable version - nextObject: function(idx) { - return this.objectAt(idx); + /** + Converts a camelized string into all lower case separated by underscores. + + ```javascript + 'innerHTML'.decamelize(); // 'inner_html' + 'action_name'.decamelize(); // 'action_name' + 'css-class-name'.decamelize(); // 'css-class-name' + 'my favorite items'.decamelize(); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ + decamelize: function(str) { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); }, /** - This is the handler for the special array content property. If you get - this property, it will return this. If you set this property it a new - array, it will replace the current content. + Replaces underscores, spaces, or camelCase with dashes. - This property overrides the default property defined in `Ember.Enumerable`. + ```javascript + 'innerHTML'.dasherize(); // 'inner-html' + 'action_name'.dasherize(); // 'action-name' + 'css-class-name'.dasherize(); // 'css-class-name' + 'my favorite items'.dasherize(); // 'my-favorite-items' + ``` - @property [] - @return this + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. */ - '[]': Ember.computed(function(key, value) { - if (value !== undefined) this.replace(0, get(this, 'length'), value) ; - return this ; - }), - - firstObject: Ember.computed(function() { - return this.objectAt(0); - }), + dasherize: function(str) { + var cache = STRING_DASHERIZE_CACHE, + hit = cache.hasOwnProperty(str), + ret; - lastObject: Ember.computed(function() { - return this.objectAt(get(this, 'length')-1); - }), + if (hit) { + return cache[str]; + } else { + ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); + cache[str] = ret; + } - // optimized version from Enumerable - contains: function(obj) { - return this.indexOf(obj) >= 0; + return ret; }, - // Add any extra methods to Ember.Array that are native to the built-in Array. /** - Returns a new array that is a slice of the receiver. This implementation - uses the observable array methods to retrieve the objects for the new - slice. + Returns the lowerCamelCase form of a string. ```javascript - var arr = ['red', 'green', 'blue']; - arr.slice(0); // ['red', 'green', 'blue'] - arr.slice(0, 2); // ['red', 'green'] - arr.slice(1, 100); // ['green', 'blue'] + 'innerHTML'.camelize(); // 'innerHTML' + 'action_name'.camelize(); // 'actionName' + 'css-class-name'.camelize(); // 'cssClassName' + 'my favorite items'.camelize(); // 'myFavoriteItems' + 'My Favorite Items'.camelize(); // 'myFavoriteItems' ``` - @method slice - @param {Integer} beginIndex (Optional) index to begin slicing from. - @param {Integer} endIndex (Optional) index to end the slice at. - @return {Array} New array with specified slice + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. */ - slice: function(beginIndex, endIndex) { - var ret = Ember.A(); - var length = get(this, 'length') ; - if (isNone(beginIndex)) beginIndex = 0 ; - if (isNone(endIndex) || (endIndex > length)) endIndex = length ; + camelize: function(str) { + return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { + return chr ? chr.toUpperCase() : ''; + }).replace(/^([A-Z])/, function(match, separator, chr) { + return match.toLowerCase(); + }); + }, - if (beginIndex < 0) beginIndex = length + beginIndex; - if (endIndex < 0) endIndex = length + endIndex; + /** + Returns the UpperCamelCase form of a string. - while(beginIndex < endIndex) { - ret[ret.length] = this.objectAt(beginIndex++) ; + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ + classify: function(str) { + var parts = str.split("."), + out = []; + + for (var i=0, l=parts.length; i= len) startAt = len-1; - if (startAt < 0) startAt += len; - for(idx=startAt;idx>=0;idx--) { - if (this.objectAt(idx) === object) return idx ; - } - return -1; - }, - // .......................................................... - // ARRAY OBSERVERS - // +})(); - /** - Adds an array observer to the receiving array. The array observer object - normally must implement two methods: - * `arrayWillChange(observedObj, start, removeCount, addCount)` - This method will be - called just before the array is modified. - * `arrayDidChange(observedObj, start, removeCount, addCount)` - This method will be - called just after the array is modified. - Both callbacks will be passed the observed object, starting index of the - change as well a a count of the items to be removed and added. You can use - these callbacks to optionally inspect the array during the change, clear - caches, or do any other bookkeeping necessary. +(function() { +/** +@module ember +@submodule ember-runtime +*/ - In addition to passing a target, you can also include an options hash - which you can use to override the method names that will be invoked on the - target. - @method addArrayObserver - @param {Object} target The observer object. - @param {Hash} opts Optional hash of configuration options including - `willChange` and `didChange` option. - @return {Ember.Array} receiver - */ - addArrayObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'arrayWillChange', - didChange = (opts && opts.didChange) || 'arrayDidChange'; - var hasObservers = get(this, 'hasArrayObservers'); - if (!hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); - Ember.addListener(this, '@array:before', target, willChange); - Ember.addListener(this, '@array:change', target, didChange); - if (!hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); - return this; - }, +var fmt = Ember.String.fmt, + w = Ember.String.w, + loc = Ember.String.loc, + camelize = Ember.String.camelize, + decamelize = Ember.String.decamelize, + dasherize = Ember.String.dasherize, + underscore = Ember.String.underscore, + capitalize = Ember.String.capitalize, + classify = Ember.String.classify; + + +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - Removes an array observer from the object if the observer is current - registered. Calling this method multiple times with the same object will - have no effect. + See [Ember.String.fmt](/api/classes/Ember.String.html#method_fmt). - @method removeArrayObserver - @param {Object} target The object observing the array. - @param {Hash} opts Optional hash of configuration options including - `willChange` and `didChange` option. - @return {Ember.Array} receiver + @method fmt + @for String */ - removeArrayObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'arrayWillChange', - didChange = (opts && opts.didChange) || 'arrayDidChange'; - - var hasObservers = get(this, 'hasArrayObservers'); - if (hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); - Ember.removeListener(this, '@array:before', target, willChange); - Ember.removeListener(this, '@array:change', target, didChange); - if (hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); - return this; - }, + String.prototype.fmt = function() { + return fmt(this, arguments); + }; /** - Becomes true whenever the array currently has observers watching changes - on the array. + See [Ember.String.w](/api/classes/Ember.String.html#method_w). - @property Boolean + @method w + @for String */ - hasArrayObservers: Ember.computed(function() { - return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before'); - }), + String.prototype.w = function() { + return w(this); + }; /** - If you are implementing an object that supports `Ember.Array`, call this - method just before the array content changes to notify any observers and - invalidate any related properties. Pass the starting index of the change - as well as a delta of the amounts to change. + See [Ember.String.loc](/api/classes/Ember.String.html#method_loc). - @method arrayContentWillChange - @param {Number} startIdx The starting index in the array that will change. - @param {Number} removeAmt The number of items that will be removed. If you - pass `null` assumes 0 - @param {Number} addAmt The number of items that will be added. If you - pass `null` assumes 0. - @return {Ember.Array} receiver + @method loc + @for String */ - arrayContentWillChange: function(startIdx, removeAmt, addAmt) { + String.prototype.loc = function() { + return loc(this, arguments); + }; - // if no args are passed assume everything changes - if (startIdx===undefined) { - startIdx = 0; - removeAmt = addAmt = -1; - } else { - if (removeAmt === undefined) removeAmt=-1; - if (addAmt === undefined) addAmt=-1; - } + /** + See [Ember.String.camelize](/api/classes/Ember.String.html#method_camelize). - // Make sure the @each proxy is set up if anyone is observing @each - if (Ember.isWatching(this, '@each')) { get(this, '@each'); } + @method camelize + @for String + */ + String.prototype.camelize = function() { + return camelize(this); + }; - Ember.sendEvent(this, '@array:before', [this, startIdx, removeAmt, addAmt]); + /** + See [Ember.String.decamelize](/api/classes/Ember.String.html#method_decamelize). - var removing, lim; - if (startIdx>=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) { - removing = []; - lim = startIdx+removeAmt; - for(var idx=startIdx;idx=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) { - adding = []; - lim = startIdx+addAmt; - for(var idx=startIdx;idx Ember.TrackedArray instances. We use - // this to lazily recompute indexes for item property observers. - this.trackedArraysByGuid = {}; + ## Using `get()` and `set()` - // This is used to coalesce item changes from property observers. - this.changedItems = {}; -} + Because of Ember's support for bindings and observers, you will always + access properties using the get method, and set properties using the + set method. This allows the observing objects to be notified and + computed properties to be handled properly. -function ItemPropertyObserverContext (dependentArray, index, trackedArray) { - Ember.assert("Internal error: trackedArray is null or undefined", trackedArray); + More documentation about `get` and `set` are below. - this.dependentArray = dependentArray; - this.index = index; - this.item = dependentArray.objectAt(index); - this.trackedArray = trackedArray; - this.beforeObserver = null; - this.observer = null; -} + ## Observing Property Changes -DependentArraysObserver.prototype = { - setValue: function (newValue) { - this.instanceMeta.setValue(newValue); - }, - getValue: function () { - return this.instanceMeta.getValue(); - }, + You typically observe property changes simply by adding the `observes` + call to the end of your method declarations in classes that you write. + For example: - setupObservers: function (dependentArray, dependentKey) { - Ember.assert("dependent array must be an `Ember.Array`", Ember.Array.detect(dependentArray)); + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + ``` - this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; + Although this is the most common way to add an observer, this capability + is actually built into the `Ember.Object` class on top of two methods + defined in this mixin: `addObserver` and `removeObserver`. You can use + these two methods to add and remove observers yourself if you need to + do so at runtime. - dependentArray.addArrayObserver(this, { - willChange: 'dependentArrayWillChange', - didChange: 'dependentArrayDidChange' - }); + To add an observer for a property, call: - if (this.cp._itemPropertyKeys[dependentKey]) { - this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); - } - }, + ```javascript + object.addObserver('propertyKey', targetObject, targetAction) + ``` - teardownObservers: function (dependentArray, dependentKey) { - var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; + This will call the `targetAction` method on the `targetObject` whenever + the value of the `propertyKey` changes. - delete this.dependentKeysByGuid[guidFor(dependentArray)]; + Note that if `propertyKey` is a computed property, the observer will be + called when any of the property dependencies are changed, even if the + resulting value of the computed property is unchanged. This is necessary + because computed properties are not computed until `get` is called. - this.teardownPropertyObservers(dependentKey, itemPropertyKeys); + @class Observable + @namespace Ember +*/ +Ember.Observable = Ember.Mixin.create({ - dependentArray.removeArrayObserver(this, { - willChange: 'dependentArrayWillChange', - didChange: 'dependentArrayDidChange' - }); - }, + /** + Retrieves the value of a property from the object. - setupPropertyObservers: function (dependentKey, itemPropertyKeys) { - var dependentArray = get(this.instanceMeta.context, dependentKey), - length = get(dependentArray, 'length'), - observerContexts = new Array(length); + This method is usually similar to using `object[keyName]` or `object.keyName`, + however it supports both computed properties and the unknownProperty + handler. - this.resetTransformations(dependentKey, observerContexts); + Because `get` unifies the syntax for accessing all these kinds + of properties, it can make many refactorings easier, such as replacing a + simple property with a computed property, or vice versa. - forEach(dependentArray, function (item, index) { - var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); - observerContexts[index] = observerContext; + ### Computed Properties - forEach(itemPropertyKeys, function (propertyKey) { - addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); - addObserver(item, propertyKey, this, observerContext.observer); - }, this); - }, this); - }, + Computed properties are methods defined with the `property` modifier + declared at the end, such as: - teardownPropertyObservers: function (dependentKey, itemPropertyKeys) { - var dependentArrayObserver = this, - trackedArray = this.trackedArraysByGuid[dependentKey], - beforeObserver, - observer, - item; + ```javascript + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + }.property('firstName', 'lastName') + ``` - if (!trackedArray) { return; } + When you call `get` on a computed property, the function will be + called and the return value will be returned instead of the function + itself. - trackedArray.apply(function (observerContexts, offset, operation) { - if (operation === Ember.TrackedArray.DELETE) { return; } + ### Unknown Properties - forEach(observerContexts, function (observerContext) { - beforeObserver = observerContext.beforeObserver; - observer = observerContext.observer; - item = observerContext.item; + Likewise, if you try to call `get` on a property whose value is + `undefined`, the `unknownProperty()` method will be called on the object. + If this method returns any value other than `undefined`, it will be returned + instead. This allows you to implement "virtual" properties that are + not defined upfront. - forEach(itemPropertyKeys, function (propertyKey) { - removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); - removeObserver(item, propertyKey, dependentArrayObserver, observer); - }); - }); - }); + @method get + @param {String} keyName The property to retrieve + @return {Object} The property value or undefined. + */ + get: function(keyName) { + return get(this, keyName); }, - createPropertyObserverContext: function (dependentArray, index, trackedArray) { - var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); - - this.createPropertyObserver(observerContext); - - return observerContext; - }, + /** + To get multiple properties at once, call `getProperties` + with a list of strings or an array: - createPropertyObserver: function (observerContext) { - var dependentArrayObserver = this; + ```javascript + record.getProperties('firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` - observerContext.beforeObserver = function (obj, keyName) { - dependentArrayObserver.updateIndexes(observerContext.trackedArray, observerContext.dependentArray); - return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext.index); - }; - observerContext.observer = function (obj, keyName) { - return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext.index); - }; - }, + is equivalent to: - resetTransformations: function (dependentKey, observerContexts) { - this.trackedArraysByGuid[dependentKey] = new Ember.TrackedArray(observerContexts); - }, + ```javascript + record.getProperties(['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` - addTransformation: function (dependentKey, index, newItems) { - var trackedArray = this.trackedArraysByGuid[dependentKey]; - if (trackedArray) { - trackedArray.addItems(index, newItems); - } + @method getProperties + @param {String...|Array} list of keys to get + @return {Hash} + */ + getProperties: function() { + return getProperties.apply(null, [this].concat(slice.call(arguments))); }, - removeTransformation: function (dependentKey, index, removedCount) { - var trackedArray = this.trackedArraysByGuid[dependentKey]; + /** + Sets the provided key or path to the value. - if (trackedArray) { - return trackedArray.removeItems(index, removedCount); - } + This method is generally very similar to calling `object[key] = value` or + `object.key = value`, except that it provides support for computed + properties, the `setUnknownProperty()` method and property observers. - return []; - }, + ### Computed Properties - updateIndexes: function (trackedArray, array) { - var length = get(array, 'length'); - // OPTIMIZE: we could stop updating once we hit the object whose observer - // fired; ie partially apply the transformations - trackedArray.apply(function (observerContexts, offset, operation) { - // we don't even have observer contexts for removed items, even if we did, - // they no longer have any index in the array - if (operation === Ember.TrackedArray.DELETE) { return; } - if (operation === Ember.TrackedArray.RETAIN && observerContexts.length === length && offset === 0) { - // If we update many items we don't want to walk the array each time: we - // only need to update the indexes at most once per run loop. - return; - } + If you try to set a value on a key that has a computed property handler + defined (see the `get()` method for an example), then `set()` will call + that method, passing both the value and key instead of simply changing + the value itself. This is useful for those times when you need to + implement a property that is composed of one or more member + properties. - forEach(observerContexts, function (context, index) { - context.index = index + offset; - }); - }); - }, + ### Unknown Properties - dependentArrayWillChange: function (dependentArray, index, removedCount, addedCount) { - var removedItem = this.callbacks.removedItem, - changeMeta, - guid = guidFor(dependentArray), - dependentKey = this.dependentKeysByGuid[guid], - itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || [], - item, - itemIndex, - sliceIndex, - observerContexts; + If you try to set a value on a key that is undefined in the target + object, then the `setUnknownProperty()` handler will be called instead. This + gives you an opportunity to implement complex "virtual" properties that + are not predefined on the object. If `setUnknownProperty()` returns + undefined, then `set()` will simply set the value on the object. - observerContexts = this.removeTransformation(dependentKey, index, removedCount); + ### Property Observers + In addition to changing the property, `set()` will also register a property + change with the object. Unless you have placed this call inside of a + `beginPropertyChanges()` and `endPropertyChanges(),` any "local" observers + (i.e. observer methods declared on the same object), will be called + immediately. Any "remote" observers (i.e. observer methods declared on + another object) will be placed in a queue and called at a later time in a + coalesced manner. - function removeObservers(propertyKey) { - removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); - removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); - } + ### Chaining - for (sliceIndex = removedCount - 1; sliceIndex >= 0; --sliceIndex) { - itemIndex = index + sliceIndex; - item = dependentArray.objectAt(itemIndex); + In addition to property changes, `set()` returns the value of the object + itself so you can do chaining like this: - forEach(itemPropertyKeys, removeObservers, this); + ```javascript + record.set('firstName', 'Charles').set('lastName', 'Jolley'); + ``` - changeMeta = createChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp); - this.setValue( removedItem.call( - this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); - } + @method set + @param {String} keyName The property to set + @param {Object} value The value to set or `null`. + @return {Ember.Observable} + */ + set: function(keyName, value) { + set(this, keyName, value); + return this; }, - dependentArrayDidChange: function (dependentArray, index, removedCount, addedCount) { - var addedItem = this.callbacks.addedItem, - guid = guidFor(dependentArray), - dependentKey = this.dependentKeysByGuid[guid], - observerContexts = new Array(addedCount), - itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey], - changeMeta, - observerContext; - forEach(dependentArray.slice(index, index + addedCount), function (item, sliceIndex) { - if (itemPropertyKeys) { - observerContext = - observerContexts[sliceIndex] = - this.createPropertyObserverContext(dependentArray, index + sliceIndex, this.trackedArraysByGuid[dependentKey]); - forEach(itemPropertyKeys, function (propertyKey) { - addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); - addObserver(item, propertyKey, this, observerContext.observer); - }, this); - } + /** + Sets a list of properties at once. These properties are set inside + a single `beginPropertyChanges` and `endPropertyChanges` batch, so + observers will be buffered. - changeMeta = createChangeMeta(dependentArray, item, index + sliceIndex, this.instanceMeta.propertyName, this.cp); - this.setValue( addedItem.call( - this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); - }, this); + ```javascript + record.setProperties({ firstName: 'Charles', lastName: 'Jolley' }); + ``` - this.addTransformation(dependentKey, index, observerContexts); + @method setProperties + @param {Hash} hash the hash of keys and values to set + @return {Ember.Observable} + */ + setProperties: function(hash) { + return Ember.setProperties(this, hash); }, - itemPropertyWillChange: function (obj, keyName, array, index) { - var guid = guidFor(obj); + /** + Begins a grouping of property changes. - if (!this.changedItems[guid]) { - this.changedItems[guid] = { - array: array, - index: index, - obj: obj, - previousValues: {} - }; - } + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call this + method at the beginning of the changes to begin deferring change + notifications. When you are done making changes, call + `endPropertyChanges()` to deliver the deferred change notifications and end + deferring. - this.changedItems[guid].previousValues[keyName] = get(obj, keyName); + @method beginPropertyChanges + @return {Ember.Observable} + */ + beginPropertyChanges: function() { + Ember.beginPropertyChanges(); + return this; }, - itemPropertyDidChange: function(obj, keyName, array, index) { - Ember.run.once(this, 'flushChanges'); - }, + /** + Ends a grouping of property changes. - flushChanges: function() { - var changedItems = this.changedItems, key, c, changeMeta; - for (key in changedItems) { - c = changedItems[key]; - changeMeta = createChangeMeta(c.array, c.obj, c.index, this.instanceMeta.propertyName, this.cp, c.previousValues); - this.setValue( - this.callbacks.removedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); - this.setValue( - this.callbacks.addedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); - } - this.changedItems = {}; - } -}; + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call + `beginPropertyChanges()` at the beginning of the changes to defer change + notifications. When you are done making changes, call this method to + deliver the deferred change notifications and end deferring. -function createChangeMeta(dependentArray, item, index, propertyName, property, previousValues) { - var meta = { - arrayChanged: dependentArray, - index: index, - item: item, - propertyName: propertyName, - property: property - }; + @method endPropertyChanges + @return {Ember.Observable} + */ + endPropertyChanges: function() { + Ember.endPropertyChanges(); + return this; + }, - if (previousValues) { - // previous values only available for item property changes - meta.previousValues = previousValues; - } + /** + Notify the observer system that a property is about to change. - return meta; -} + Sometimes you need to change a value directly or indirectly without + actually calling `get()` or `set()` on it. In this case, you can use this + method and `propertyDidChange()` instead. Calling these two methods + together will notify all observers that the property has potentially + changed value. -function addItems (dependentArray, callbacks, cp, propertyName, meta) { - forEach(dependentArray, function (item, index) { - meta.setValue( callbacks.addedItem.call( - this, meta.getValue(), item, createChangeMeta(dependentArray, item, index, propertyName, cp), meta.sugarMeta)); - }, this); -} + Note that you must always call `propertyWillChange` and `propertyDidChange` + as a pair. If you do not, it may get the property change groups out of + order and cause notifications to be delivered more often than you would + like. -function reset(cp, propertyName) { - var callbacks = cp._callbacks(), - meta; + @method propertyWillChange + @param {String} keyName The property key that is about to change. + @return {Ember.Observable} + */ + propertyWillChange: function(keyName) { + Ember.propertyWillChange(this, keyName); + return this; + }, - if (cp._hasInstanceMeta(this, propertyName)) { - meta = cp._instanceMeta(this, propertyName); - meta.setValue(cp.resetValue(meta.getValue())); - } else { - meta = cp._instanceMeta(this, propertyName); - } + /** + Notify the observer system that a property has just changed. - if (cp.options.initialize) { - cp.options.initialize.call(this, meta.getValue(), { property: cp, propertyName: propertyName }, meta.sugarMeta); - } -} + Sometimes you need to change a value directly or indirectly without + actually calling `get()` or `set()` on it. In this case, you can use this + method and `propertyWillChange()` instead. Calling these two methods + together will notify all observers that the property has potentially + changed value. -function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { - this.context = context; - this.propertyName = propertyName; - this.cache = metaFor(context).cache; + Note that you must always call `propertyWillChange` and `propertyDidChange` + as a pair. If you do not, it may get the property change groups out of + order and cause notifications to be delivered more often than you would + like. - this.dependentArrays = {}; - this.sugarMeta = {}; + @method propertyDidChange + @param {String} keyName The property key that has just changed. + @return {Ember.Observable} + */ + propertyDidChange: function(keyName) { + Ember.propertyDidChange(this, keyName); + return this; + }, - this.initialValue = initialValue; -} + /** + Convenience method to call `propertyWillChange` and `propertyDidChange` in + succession. -ReduceComputedPropertyInstanceMeta.prototype = { - getValue: function () { - if (this.propertyName in this.cache) { - return this.cache[this.propertyName]; - } else { - return this.initialValue; - } + @method notifyPropertyChange + @param {String} keyName The property key to be notified about. + @return {Ember.Observable} + */ + notifyPropertyChange: function(keyName) { + this.propertyWillChange(keyName); + this.propertyDidChange(keyName); + return this; }, - setValue: function(newValue) { - // This lets sugars force a recomputation, handy for very simple - // implementations of eg max. - if (newValue !== undefined) { - this.cache[this.propertyName] = newValue; - } else { - delete this.cache[this.propertyName]; - } - } -}; + addBeforeObserver: function(key, target, method) { + Ember.addBeforeObserver(this, key, target, method); + }, -/** - A computed property whose dependent keys are arrays and which is updated with - "one at a time" semantics. + /** + Adds an observer on a property. - @class ReduceComputedProperty - @namespace Ember - @extends Ember.ComputedProperty - @constructor -*/ -function ReduceComputedProperty(options) { - var cp = this; + This is the core method used to register an observer for a property. - this.options = options; - this._instanceMetas = {}; + Once you call this method, any time the key's value is set, your observer + will be notified. Note that the observers are triggered any time the + value is set, regardless of whether it has actually changed. Your + observer should be prepared to handle that. - this._dependentKeys = null; - // A map of dependentKey -> [itemProperty, ...] that tracks what properties of - // items in the array we must track to update this property. - this._itemPropertyKeys = {}; - this._previousItemPropertyKeys = {}; + You can also pass an optional context parameter to this method. The + context will be passed to your observer method whenever it is triggered. + Note that if you add the same target/method pair on a key multiple times + with different context parameters, your observer will only be called once + with the last context you passed. - this.readOnly(); - this.cacheable(); + ### Observer Methods - this.recomputeOnce = function(propertyName) { - // What we really want to do is coalesce by . - // We need a form of `scheduleOnce` that accepts an arbitrary token to - // coalesce by, in addition to the target and method. - Ember.run.once(this, recompute, propertyName); - }; - var recompute = function(propertyName) { - var dependentKeys = cp._dependentKeys, - meta = cp._instanceMeta(this, propertyName), - callbacks = cp._callbacks(); + Observer methods you pass should generally have the following signature if + you do not pass a `context` parameter: - reset.call(this, cp, propertyName); + ```javascript + fooDidChange: function(sender, key, value, rev) { }; + ``` - forEach(cp._dependentKeys, function (dependentKey) { - var dependentArray = get(this, dependentKey), - previousDependentArray = meta.dependentArrays[dependentKey]; - - if (dependentArray === previousDependentArray) { - // The array may be the same, but our item property keys may have - // changed, so we set them up again. We can't easily tell if they've - // changed: the array may be the same object, but with different - // contents. - if (cp._previousItemPropertyKeys[dependentKey]) { - delete cp._previousItemPropertyKeys[dependentKey]; - meta.dependentArraysObserver.setupPropertyObservers(dependentKey, cp._itemPropertyKeys[dependentKey]); - } - } else { - meta.dependentArrays[dependentKey] = dependentArray; + The sender is the object that changed. The key is the property that + changes. The value property is currently reserved and unused. The rev + is the last property revision of the object when it changed, which you can + use to detect if the key value has really changed or not. - if (previousDependentArray) { - meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); - } + If you pass a `context` parameter, the context will be passed before the + revision like so: - if (dependentArray) { - meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); - } - } - }, this); + ```javascript + fooDidChange: function(sender, key, value, context, rev) { }; + ``` - forEach(cp._dependentKeys, function(dependentKey) { - var dependentArray = get(this, dependentKey); - if (dependentArray) { - addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); - } - }, this); - }; + Usually you will not need the value, context or revision parameters at + the end. In this case, it is common to write observer methods that take + only a sender and key value as parameters or, if you aren't interested in + any of these values, to write an observer that has no parameters at all. - this.func = function (propertyName) { - Ember.assert("Computed reduce values require at least one dependent key", cp._dependentKeys); + @method addObserver + @param {String} key The key to observer + @param {Object} target The target object to invoke + @param {String|Function} method The method to invoke. + @return {Ember.Object} self + */ + addObserver: function(key, target, method) { + Ember.addObserver(this, key, target, method); + }, - recompute.call(this, propertyName); + /** + Remove an observer you have previously registered on this object. Pass + the same key, target, and method you passed to `addObserver()` and your + target will no longer receive notifications. - return cp._instanceMeta(this, propertyName).getValue(); - }; -} + @method removeObserver + @param {String} key The key to observer + @param {Object} target The target object to invoke + @param {String|Function} method The method to invoke. + @return {Ember.Observable} receiver + */ + removeObserver: function(key, target, method) { + Ember.removeObserver(this, key, target, method); + }, -Ember.ReduceComputedProperty = ReduceComputedProperty; -ReduceComputedProperty.prototype = o_create(ComputedProperty.prototype); + /** + Returns `true` if the object currently has observers registered for a + particular key. You can use this method to potentially defer performing + an expensive action until someone begins observing a particular property + on the object. -function defaultCallback(computedValue) { - return computedValue; -} + @method hasObserverFor + @param {String} key Key to check + @return {Boolean} + */ + hasObserverFor: function(key) { + return Ember.hasListeners(this, key+':change'); + }, -ReduceComputedProperty.prototype._callbacks = function () { - if (!this.callbacks) { - var options = this.options; - this.callbacks = { - removedItem: options.removedItem || defaultCallback, - addedItem: options.addedItem || defaultCallback - }; - } - return this.callbacks; -}; + /** + Retrieves the value of a property, or a default value in the case that the + property returns `undefined`. -ReduceComputedProperty.prototype._hasInstanceMeta = function (context, propertyName) { - var guid = guidFor(context), - key = guid + ':' + propertyName; + ```javascript + person.getWithDefault('lastName', 'Doe'); + ``` - return !!this._instanceMetas[key]; -}; + @method getWithDefault + @param {String} keyName The name of the property to retrieve + @param {Object} defaultValue The value to return if the property value is undefined + @return {Object} The property value or the defaultValue. + */ + getWithDefault: function(keyName, defaultValue) { + return Ember.getWithDefault(this, keyName, defaultValue); + }, -ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { - var guid = guidFor(context), - key = guid + ':' + propertyName, - meta = this._instanceMetas[key]; + /** + Set the value of a property to the current value plus some amount. - if (!meta) { - meta = this._instanceMetas[key] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); - meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); - } + ```javascript + person.incrementProperty('age'); + team.incrementProperty('score', 2); + ``` - return meta; -}; + @method incrementProperty + @param {String} keyName The name of the property to increment + @param {Number} increment The amount to increment by. Defaults to 1 + @return {Number} The new property value + */ + incrementProperty: function(keyName, increment) { + if (Ember.isNone(increment)) { increment = 1; } + Ember.assert("Must pass a numeric value to incrementProperty", (!isNaN(parseFloat(increment)) && isFinite(increment))); + set(this, keyName, (get(this, keyName) || 0) + increment); + return get(this, keyName); + }, -ReduceComputedProperty.prototype.initialValue = function () { - switch (typeof this.options.initialValue) { - case 'undefined': - throw new Error("reduce computed properties require an initial value: did you forget to pass one to Ember.reduceComputed?"); - case 'function': - return this.options.initialValue(); - default: - return this.options.initialValue; - } -}; + /** + Set the value of a property to the current value minus some amount. -ReduceComputedProperty.prototype.resetValue = function (value) { - return this.initialValue(); -}; + ```javascript + player.decrementProperty('lives'); + orc.decrementProperty('health', 5); + ``` -ReduceComputedProperty.prototype.itemPropertyKey = function (dependentArrayKey, itemPropertyKey) { - this._itemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey] || []; - this._itemPropertyKeys[dependentArrayKey].push(itemPropertyKey); -}; + @method decrementProperty + @param {String} keyName The name of the property to decrement + @param {Number} decrement The amount to decrement by. Defaults to 1 + @return {Number} The new property value + */ + decrementProperty: function(keyName, decrement) { + if (Ember.isNone(decrement)) { decrement = 1; } + Ember.assert("Must pass a numeric value to decrementProperty", (!isNaN(parseFloat(decrement)) && isFinite(decrement))); + set(this, keyName, (get(this, keyName) || 0) - decrement); + return get(this, keyName); + }, -ReduceComputedProperty.prototype.clearItemPropertyKeys = function (dependentArrayKey) { - if (this._itemPropertyKeys[dependentArrayKey]) { - this._previousItemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey]; - this._itemPropertyKeys[dependentArrayKey] = []; - } -}; + /** + Set the value of a boolean property to the opposite of it's + current value. -ReduceComputedProperty.prototype.property = function () { - var cp = this, - args = a_slice.call(arguments), - propertyArgs = [], - match, - dependentArrayKey, - itemPropertyKey; + ```javascript + starship.toggleProperty('warpDriveEngaged'); + ``` - forEach(a_slice.call(arguments), function (dependentKey) { - if (doubleEachPropertyPattern.test(dependentKey)) { - throw new Error("Nested @each properties not supported: " + dependentKey); - } else if (match = eachPropertyPattern.exec(dependentKey)) { - dependentArrayKey = match[1]; - itemPropertyKey = match[2]; - cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); - propertyArgs.push(dependentArrayKey); - } else { - propertyArgs.push(dependentKey); - } - }); + @method toggleProperty + @param {String} keyName The name of the property to toggle + @return {Object} The new property value + */ + toggleProperty: function(keyName) { + set(this, keyName, !get(this, keyName)); + return get(this, keyName); + }, - return ComputedProperty.prototype.property.apply(this, propertyArgs); -}; + /** + Returns the cached value of a computed property, if it exists. + This allows you to inspect the value of a computed property + without accidentally invoking it if it is intended to be + generated lazily. -/** - Creates a computed property which operates on dependent arrays and - is updated with "one at a time" semantics. When items are added or - removed from the dependent array(s) a reduce computed only operates - on the change instead of re-evaluating the entire array. + @method cacheFor + @param {String} keyName + @return {Object} The cached value of the computed property, if any + */ + cacheFor: function(keyName) { + return Ember.cacheFor(this, keyName); + }, - If there are more than one arguments the first arguments are - considered to be dependent property keys. The last argument is - required to be an options object. The options object can have the - following four properties. + // intended for debugging purposes + observersForKey: function(keyName) { + return Ember.observersFor(this, keyName); + } +}); - `initialValue` - A value or function that will be used as the initial - value for the computed. If this property is a function the result of calling - the function will be used as the initial value. This property is required. +})(); - `initialize` - An optional initialize function. Typically this will be used - to set up state on the instanceMeta object. - `removedItem` - A function that is called each time an element is removed - from the array. - `addedItem` - A function that is called each time an element is added to - the array. +(function() { +/** + @module ember + @submodule ember-runtime +*/ - The `initialize` function has the following signature: +// NOTE: this object should never be included directly. Instead use `Ember.Object`. +// We only define this separately so that `Ember.Set` can depend on it. - ```javascript - function (initialValue, changeMeta, instanceMeta) - ``` - `initialValue` - The value of the `initialValue` property from the - options object. +var set = Ember.set, get = Ember.get, + o_create = Ember.create, + o_defineProperty = Ember.platform.defineProperty, + GUID_KEY = Ember.GUID_KEY, + guidFor = Ember.guidFor, + generateGuid = Ember.generateGuid, + meta = Ember.meta, + META_KEY = Ember.META_KEY, + rewatch = Ember.rewatch, + finishChains = Ember.finishChains, + sendEvent = Ember.sendEvent, + destroy = Ember.destroy, + schedule = Ember.run.schedule, + Mixin = Ember.Mixin, + applyMixin = Mixin._apply, + finishPartial = Mixin.finishPartial, + reopen = Mixin.prototype.reopen, + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + indexOf = Ember.EnumerableUtils.indexOf; - `changeMeta` - An object which contains meta information about the - computed. It contains the following properties: +var undefinedDescriptor = { + configurable: true, + writable: true, + enumerable: false, + value: undefined +}; - - `property` the computed property - - `propertyName` the name of the property on the object +function makeCtor() { - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. + // Note: avoid accessing any properties on the object since it makes the + // method a lot faster. This is glue code so we want it to be as fast as + // possible. + var wasApplied = false, initMixins, initProperties; - The `removedItem` and `addedItem` functions both have the following signature: + var Class = function() { + if (!wasApplied) { + Class.proto(); // prepare prototype... + } + o_defineProperty(this, GUID_KEY, undefinedDescriptor); + o_defineProperty(this, '_super', undefinedDescriptor); + var m = meta(this), proto = m.proto; + m.proto = this; + if (initMixins) { + // capture locally so we can clear the closed over variable + var mixins = initMixins; + initMixins = null; + this.reopen.apply(this, mixins); + } + if (initProperties) { + // capture locally so we can clear the closed over variable + var props = initProperties; + initProperties = null; - ```javascript - function (accumulatedValue, item, changeMeta, instanceMeta) - ``` + var concatenatedProperties = this.concatenatedProperties; - `accumulatedValue` - The value returned from the last time - `removedItem` or `addedItem` was called or `initialValue`. + for (var i = 0, l = props.length; i < l; i++) { + var properties = props[i]; - `item` - the element added or removed from the array + Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); - `changeMeta` - An object which contains meta information about the - change. It contains the following properties: + if (typeof properties !== 'object' && properties !== undefined) { + throw new Ember.Error("Ember.Object.create only accepts objects."); + } - - `property` the computed property - - `propertyName` the name of the property on the object - - `index` the index of the added or removed item - - `item` the added or removed item: this is exactly the same as - the second arg - - `arrayChanged` the array that triggered the change. Can be - useful when depending on multiple arrays. + if (!properties) { continue; } - For property changes triggered on an item property change (when - depKey is something like `someArray.@each.someProperty`), - `changeMeta` will also contain the following property: + var keyNames = Ember.keys(properties); - - `previousValues` an object whose keys are the properties that changed on - the item, and whose values are the item's previous values. + for (var j = 0, ll = keyNames.length; j < ll; j++) { + var keyName = keyNames[j]; + if (!properties.hasOwnProperty(keyName)) { continue; } - `previousValues` is important Ember coalesces item property changes via - Ember.run.once. This means that by the time removedItem gets called, item has - the new values, but you may need the previous value (eg for sorting & - filtering). + var value = properties[keyName], + IS_BINDING = Ember.IS_BINDING; - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. + if (IS_BINDING.test(keyName)) { + var bindings = m.bindings; + if (!bindings) { + bindings = m.bindings = {}; + } else if (!m.hasOwnProperty('bindings')) { + bindings = m.bindings = o_create(m.bindings); + } + bindings[keyName] = value; + } - The `removedItem` and `addedItem` functions should return the accumulated - value. It is acceptable to not return anything (ie return undefined) - to invalidate the computation. This is generally not a good idea for - arrayComputed but it's used in eg max and min. + var desc = m.descs[keyName]; - Example + Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty)); + Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1)); + Ember.assert("`actions` must be provided at extend time, not at create " + + "time, when Ember.ActionHandler is used (i.e. views, " + + "controllers & routes).", !((keyName === 'actions') && Ember.ActionHandler.detect(this))); - ```javascript - Ember.computed.max = function (dependentKey) { - return Ember.reduceComputed.call(null, dependentKey, { - initialValue: -Infinity, + if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) { + var baseValue = this[keyName]; - addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - return Math.max(accumulatedValue, item); - }, + if (baseValue) { + if ('function' === typeof baseValue.concat) { + value = baseValue.concat(value); + } else { + value = Ember.makeArray(baseValue).concat(value); + } + } else { + value = Ember.makeArray(value); + } + } - removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - if (item < accumulatedValue) { - return accumulatedValue; + if (desc) { + desc.set(this, keyName, value); + } else { + if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) { + this.setUnknownProperty(keyName, value); + } else if (MANDATORY_SETTER) { + Ember.defineProperty(this, keyName, null, value); // setup mandatory setter + } else { + this[keyName] = value; + } + } } } - }); + } + finishPartial(this, m); + this.init.apply(this, arguments); + m.proto = proto; + finishChains(this); + sendEvent(this, "init"); }; - ``` - @method reduceComputed - @for Ember - @param {String} [dependentKeys*] - @param {Object} options - @returns {Ember.ComputedProperty} -*/ -Ember.reduceComputed = function (options) { - var args; + Class.toString = Mixin.prototype.toString; + Class.willReopen = function() { + if (wasApplied) { + Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin); + } - if (arguments.length > 1) { - args = a_slice.call(arguments, 0, -1); - options = a_slice.call(arguments, -1)[0]; - } + wasApplied = false; + }; + Class._initMixins = function(args) { initMixins = args; }; + Class._initProperties = function(args) { initProperties = args; }; - if (typeof options !== "object") { - throw new Error("Reduce Computed Property declared without an options hash"); - } + Class.proto = function() { + var superclass = Class.superclass; + if (superclass) { superclass.proto(); } - if (!options.initialValue) { - throw new Error("Reduce Computed Property declared without an initial value"); - } + if (!wasApplied) { + wasApplied = true; + Class.PrototypeMixin.applyPartial(Class.prototype); + rewatch(Class.prototype); + } - var cp = new ReduceComputedProperty(options); + return this.prototype; + }; - if (args) { - cp.property.apply(cp, args); - } + return Class; - return cp; -}; +} -})(); +/** + @class CoreObject + @namespace Ember +*/ +var CoreObject = makeCtor(); +CoreObject.toString = function() { return "Ember.CoreObject"; }; +CoreObject.PrototypeMixin = Mixin.create({ + reopen: function() { + applyMixin(this, arguments, true); + return this; + }, + /** + An overridable method called when objects are instantiated. By default, + does nothing unless it is overridden during class definition. -(function() { -var ReduceComputedProperty = Ember.ReduceComputedProperty, - a_slice = [].slice, - o_create = Ember.create, - forEach = Ember.EnumerableUtils.forEach; + Example: -function ArrayComputedProperty() { - var cp = this; + ```javascript + App.Person = Ember.Object.extend({ + init: function() { + alert('Name is ' + this.get('name')); + } + }); - ReduceComputedProperty.apply(this, arguments); + var steve = App.Person.create({ + name: "Steve" + }); - this.func = (function(reduceFunc) { - return function (propertyName) { - if (!cp._hasInstanceMeta(this, propertyName)) { - // When we recompute an array computed property, we need already - // retrieved arrays to be updated; we can't simply empty the cache and - // hope the array is re-retrieved. - forEach(cp._dependentKeys, function(dependentKey) { - Ember.addObserver(this, dependentKey, function() { - cp.recomputeOnce.call(this, propertyName); - }); - }, this); - } + // alerts 'Name is Steve'. + ``` - return reduceFunc.apply(this, arguments); - }; - })(this.func); + NOTE: If you do override `init` for a framework class like `Ember.View` or + `Ember.ArrayController`, be sure to call `this._super()` in your + `init` declaration! If you don't, Ember may not have an opportunity to + do important setup work, and you'll see strange behavior in your + application. - return this; -} -Ember.ArrayComputedProperty = ArrayComputedProperty; -ArrayComputedProperty.prototype = o_create(ReduceComputedProperty.prototype); -ArrayComputedProperty.prototype.initialValue = function () { - return Ember.A(); -}; -ArrayComputedProperty.prototype.resetValue = function (array) { - array.clear(); - return array; -}; + @method init + */ + init: function() {}, -/** - Creates a computed property which operates on dependent arrays and - is updated with "one at a time" semantics. When items are added or - removed from the dependent array(s) an array computed only operates - on the change instead of re-evaluating the entire array. This should - return an array, if you'd like to use "one at a time" semantics and - compute some value other then an array look at - `Ember.reduceComputed`. + /** + Defines the properties that will be concatenated from the superclass + (instead of overridden). - If there are more than one arguments the first arguments are - considered to be dependent property keys. The last argument is - required to be an options object. The options object can have the - following three properties. + By default, when you extend an Ember class a property defined in + the subclass overrides a property with the same name that is defined + in the superclass. However, there are some cases where it is preferable + to build up a property's value by combining the superclass' property + value with the subclass' value. An example of this in use within Ember + is the `classNames` property of `Ember.View`. - `initialize` - An optional initialize function. Typically this will be used - to set up state on the instanceMeta object. + Here is some sample code showing the difference between a concatenated + property and a normal one: - `removedItem` - A function that is called each time an element is - removed from the array. + ```javascript + App.BarView = Ember.View.extend({ + someNonConcatenatedProperty: ['bar'], + classNames: ['bar'] + }); - `addedItem` - A function that is called each time an element is - added to the array. + App.FooBarView = App.BarView.extend({ + someNonConcatenatedProperty: ['foo'], + classNames: ['foo'], + }); + var fooBarView = App.FooBarView.create(); + fooBarView.get('someNonConcatenatedProperty'); // ['foo'] + fooBarView.get('classNames'); // ['ember-view', 'bar', 'foo'] + ``` - The `initialize` function has the following signature: + This behavior extends to object creation as well. Continuing the + above example: - ```javascript - function (array, changeMeta, instanceMeta) - ``` + ```javascript + var view = App.FooBarView.create({ + someNonConcatenatedProperty: ['baz'], + classNames: ['baz'] + }) + view.get('someNonConcatenatedProperty'); // ['baz'] + view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] + ``` + Adding a single property that is not an array will just add it in the array: - `array` - The initial value of the arrayComputed, an empty array. + ```javascript + var view = App.FooBarView.create({ + classNames: 'baz' + }) + view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] + ``` - `changeMeta` - An object which contains meta information about the - computed. It contains the following properties: + Using the `concatenatedProperties` property, we can tell to Ember that mix + the content of the properties. - - `property` the computed property - - `propertyName` the name of the property on the object + In `Ember.View` the `classNameBindings` and `attributeBindings` properties + are also concatenated, in addition to `classNames`. - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. + This feature is available for you to use throughout the Ember object model, + although typical app developers are likely to use it infrequently. Since + it changes expectations about behavior of properties, you should properly + document its usage in each individual concatenated property (to not + mislead your users to think they can override the property in a subclass). + @property concatenatedProperties + @type Array + @default null + */ + concatenatedProperties: null, - The `removedItem` and `addedItem` functions both have the following signature: + /** + Destroyed object property flag. - ```javascript - function (accumulatedValue, item, changeMeta, instanceMeta) - ``` + if this property is `true` the observers and bindings were already + removed by the effect of calling the `destroy()` method. - `accumulatedValue` - The value returned from the last time - `removedItem` or `addedItem` was called or an empty array. + @property isDestroyed + @default false + */ + isDestroyed: false, - `item` - the element added or removed from the array + /** + Destruction scheduled flag. The `destroy()` method has been called. - `changeMeta` - An object which contains meta information about the - change. It contains the following properties: + The object stays intact until the end of the run loop at which point + the `isDestroyed` flag is set. - - `property` the computed property - - `propertyName` the name of the property on the object - - `index` the index of the added or removed item - - `item` the added or removed item: this is exactly the same as - the second arg - - `arrayChanged` the array that triggered the change. Can be - useful when depending on multiple arrays. + @property isDestroying + @default false + */ + isDestroying: false, - For property changes triggered on an item property change (when - depKey is something like `someArray.@each.someProperty`), - `changeMeta` will also contain the following property: + /** + Destroys an object by setting the `isDestroyed` flag and removing its + metadata, which effectively destroys observers and bindings. - - `previousValues` an object whose keys are the properties that changed on - the item, and whose values are the item's previous values. + If you try to set a property on a destroyed object, an exception will be + raised. - `previousValues` is important Ember coalesces item property changes via - Ember.run.once. This means that by the time removedItem gets called, item has - the new values, but you may need the previous value (eg for sorting & - filtering). + Note that destruction is scheduled for the end of the run loop and does not + happen immediately. It will set an isDestroying flag immediately. - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. + @method destroy + @return {Ember.Object} receiver + */ + destroy: function() { + if (this.isDestroying) { return; } + this.isDestroying = true; - The `removedItem` and `addedItem` functions should return the accumulated - value. It is acceptable to not return anything (ie return undefined) - to invalidate the computation. This is generally not a good idea for - arrayComputed but it's used in eg max and min. + schedule('actions', this, this.willDestroy); + schedule('destroy', this, this._scheduledDestroy); + return this; + }, - Example + /** + Override to implement teardown. - ```javascript - Ember.computed.map = function(dependentKey, callback) { - var options = { - addedItem: function(array, item, changeMeta, instanceMeta) { - var mapped = callback(item); - array.insertAt(changeMeta.index, mapped); - return array; - }, - removedItem: function(array, item, changeMeta, instanceMeta) { - array.removeAt(changeMeta.index, 1); - return array; - } - }; + @method willDestroy + */ + willDestroy: Ember.K, - return Ember.arrayComputed(dependentKey, options); - }; - ``` + /** + Invoked by the run loop to actually destroy the object. This is + scheduled for execution by the `destroy` method. - @method arrayComputed - @for Ember - @param {String} [dependentKeys*] - @param {Object} options - @returns {Ember.ComputedProperty} -*/ -Ember.arrayComputed = function (options) { - var args; + @private + @method _scheduledDestroy + */ + _scheduledDestroy: function() { + if (this.isDestroyed) { return; } + destroy(this); + this.isDestroyed = true; + }, - if (arguments.length > 1) { - args = a_slice.call(arguments, 0, -1); - options = a_slice.call(arguments, -1)[0]; - } + bind: function(to, from) { + if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } + from.to(to).connect(this); + return from; + }, - if (typeof options !== "object") { - throw new Error("Array Computed Property declared without an options hash"); - } + /** + Returns a string representation which attempts to provide more information + than Javascript's `toString` typically does, in a generic way for all Ember + objects. - var cp = new ArrayComputedProperty(options); + ```javascript + App.Person = Em.Object.extend() + person = App.Person.create() + person.toString() //=> "" + ``` - if (args) { - cp.property.apply(cp, args); - } + If the object's class is not defined on an Ember namespace, it will + indicate it is a subclass of the registered superclass: - return cp; -}; + ```javascript + Student = App.Person.extend() + student = Student.create() + student.toString() //=> "<(subclass of App.Person):ember1025>" + ``` -})(); + If the method `toStringExtension` is defined, its return value will be + included in the output. + ```javascript + App.Teacher = App.Person.extend({ + toStringExtension: function() { + return this.get('fullName'); + } + }); + teacher = App.Teacher.create() + teacher.toString(); //=> "" + ``` + @method toString + @return {String} string representation + */ + toString: function toString() { + var hasToStringExtension = typeof this.toStringExtension === 'function', + extension = hasToStringExtension ? ":" + this.toStringExtension() : ''; + var ret = '<'+this.constructor.toString()+':'+guidFor(this)+extension+'>'; + this.toString = makeToString(ret); + return ret; + } +}); -(function() { -/** -@module ember -@submodule ember-runtime -*/ +CoreObject.PrototypeMixin.ownerConstructor = CoreObject; -var get = Ember.get, - set = Ember.set, - guidFor = Ember.guidFor, - merge = Ember.merge, - a_slice = [].slice, - forEach = Ember.EnumerableUtils.forEach, - map = Ember.EnumerableUtils.map; +function makeToString(ret) { + return function() { return ret; }; +} -/** - A computed property that calculates the maximum value in the - dependent array. This will return `-Infinity` when the dependent - array is empty. +if (Ember.config.overridePrototypeMixin) { + Ember.config.overridePrototypeMixin(CoreObject.PrototypeMixin); +} - Example +CoreObject.__super__ = null; - ```javascript - App.Person = Ember.Object.extend({ - childAges: Ember.computed.mapBy('children', 'age'), - maxChildAge: Ember.computed.max('childAges') - }); +var ClassMixin = Mixin.create({ - var lordByron = App.Person.create({children: []}); - lordByron.get('maxChildAge'); // -Infinity - lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); - lordByron.get('maxChildAge'); // 7 - lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); - lordByron.get('maxChildAge'); // 8 - ``` + ClassMixin: Ember.required(), - @method computed.max - @for Ember - @param {String} dependentKey - @return {Ember.ComputedProperty} computes the largest value in the dependentKey's array -*/ -Ember.computed.max = function (dependentKey) { - return Ember.reduceComputed.call(null, dependentKey, { - initialValue: -Infinity, + PrototypeMixin: Ember.required(), - addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - return Math.max(accumulatedValue, item); - }, + isClass: true, - removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - if (item < accumulatedValue) { - return accumulatedValue; - } - } - }); -}; + isMethod: false, -/** - A computed property that calculates the minimum value in the - dependent array. This will return `Infinity` when the dependent - array is empty. + /** + Creates a new subclass. - Example + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + alert(thing); + } + }); + ``` - ```javascript - App.Person = Ember.Object.extend({ - childAges: Ember.computed.mapBy('children', 'age'), - minChildAge: Ember.computed.min('childAges') - }); + This defines a new subclass of Ember.Object: `App.Person`. It contains one method: `say()`. - var lordByron = App.Person.create({children: []}); - lordByron.get('minChildAge'); // Infinity - lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); - lordByron.get('minChildAge'); // 7 - lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); - lordByron.get('minChildAge'); // 5 - ``` + You can also create a subclass from any existing class by calling its `extend()` method. For example, you might want to create a subclass of Ember's built-in `Ember.View` class: - @method computed.min - @for Ember - @param {String} dependentKey - @return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array -*/ -Ember.computed.min = function (dependentKey) { - return Ember.reduceComputed.call(null, dependentKey, { - initialValue: Infinity, + ```javascript + App.PersonView = Ember.View.extend({ + tagName: 'li', + classNameBindings: ['isAdministrator'] + }); + ``` - addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - return Math.min(accumulatedValue, item); - }, + When defining a subclass, you can override methods but still access the implementation of your parent class by calling the special `_super()` method: - removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - if (item > accumulatedValue) { - return accumulatedValue; + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + var name = this.get('name'); + alert(name + ' says: ' + thing); } - } - }); -}; + }); -/** - Returns an array mapped via the callback + App.Soldier = App.Person.extend({ + say: function(thing) { + this._super(thing + ", sir!"); + }, + march: function(numberOfHours) { + alert(this.get('name') + ' marches for ' + numberOfHours + ' hours.') + } + }); - The callback method you provide should have the following signature: + var yehuda = App.Soldier.create({ + name: "Yehuda Katz" + }); - ```javascript - function(item); - ``` + yehuda.say("Yes"); // alerts "Yehuda Katz says: Yes, sir!" + ``` - - `item` is the current item in the iteration. + The `create()` on line #17 creates an *instance* of the `App.Soldier` class. The `extend()` on line #8 creates a *subclass* of `App.Person`. Any instance of the `App.Person` class will *not* have the `march()` method. - Example + You can also pass `Ember.Mixin` classes to add additional properties to the subclass. - ```javascript - App.Hampster = Ember.Object.extend({ - excitingChores: Ember.computed.map('chores', function(chore) { - return chore.toUpperCase() + '!'; - }) - }); + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + alert(this.get('name') + ' says: ' + thing); + } + }); - var hampster = App.Hampster.create({chores: ['cook', 'clean', 'write more unit tests']}); - hampster.get('excitingChores'); // ['COOK!', 'CLEAN!', 'WRITE MORE UNIT TESTS!'] - ``` + App.SingingMixin = Ember.Mixin.create({ + sing: function(thing){ + alert(this.get('name') + ' sings: la la la ' + thing); + } + }); - @method computed.map - @for Ember - @param {String} dependentKey - @param {Function} callback - @return {Ember.ComputedProperty} an array mapped via the callback -*/ -Ember.computed.map = function(dependentKey, callback) { - var options = { - addedItem: function(array, item, changeMeta, instanceMeta) { - var mapped = callback(item); - array.insertAt(changeMeta.index, mapped); - return array; - }, - removedItem: function(array, item, changeMeta, instanceMeta) { - array.removeAt(changeMeta.index, 1); - return array; - } - }; + App.BroadwayStar = App.Person.extend(App.SingingMixin, { + dance: function() { + alert(this.get('name') + ' dances: tap tap tap tap '); + } + }); + ``` - return Ember.arrayComputed(dependentKey, options); -}; + The `App.BroadwayStar` class contains three methods: `say()`, `sing()`, and `dance()`. -/** - Returns an array mapped to the specified key. + @method extend + @static - Example + @param {Ember.Mixin} [mixins]* One or more Ember.Mixin classes + @param {Object} [arguments]* Object containing values to use within the new class + */ + extend: function() { + var Class = makeCtor(), proto; + Class.ClassMixin = Mixin.create(this.ClassMixin); + Class.PrototypeMixin = Mixin.create(this.PrototypeMixin); - ```javascript - App.Person = Ember.Object.extend({ - childAges: Ember.computed.mapBy('children', 'age'), - minChildAge: Ember.computed.min('childAges') - }); + Class.ClassMixin.ownerConstructor = Class; + Class.PrototypeMixin.ownerConstructor = Class; - var lordByron = App.Person.create({children: []}); - lordByron.get('childAge'); // [] - lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); - lordByron.get('childAge'); // [7] - lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); - lordByron.get('childAge'); // [7, 5, 8] - ``` + reopen.apply(Class.PrototypeMixin, arguments); - @method computed.mapBy - @for Ember - @param {String} dependentKey - @param {String} propertyKey - @return {Ember.ComputedProperty} an array mapped to the specified key -*/ -Ember.computed.mapBy = function(dependentKey, propertyKey) { - var callback = function(item) { return get(item, propertyKey); }; - return Ember.computed.map(dependentKey + '.@each.' + propertyKey, callback); -}; + Class.superclass = this; + Class.__super__ = this.prototype; -/** - @method computed.mapProperty - @for Ember - @deprecated Use `Ember.computed.mapBy` instead - @param dependentKey - @param propertyKey -*/ -Ember.computed.mapProperty = Ember.computed.mapBy; + proto = Class.prototype = o_create(this.prototype); + proto.constructor = Class; + generateGuid(proto); + meta(proto).proto = proto; // this will disable observers on prototype -/** - Filters the array by the callback. + Class.ClassMixin.apply(Class); + return Class; + }, - The callback method you provide should have the following signature: + /** + Equivalent to doing `extend(arguments).create()`. + If possible use the normal `create` method instead. - ```javascript - function(item); - ``` + @method createWithMixins + @static + @param [arguments]* + */ + createWithMixins: function() { + var C = this; + if (arguments.length>0) { this._initMixins(arguments); } + return new C(); + }, - - `item` is the current item in the iteration. + /** + Creates an instance of a class. Accepts either no arguments, or an object + containing values to initialize the newly instantiated object with. - Example + ```javascript + App.Person = Ember.Object.extend({ + helloWorld: function() { + alert("Hi, my name is " + this.get('name')); + } + }); - ```javascript - App.Hampster = Ember.Object.extend({ - remainingChores: Ember.computed.filter('chores', function(chore) { - return !chore.done; - }) - }); + var tom = App.Person.create({ + name: 'Tom Dale' + }); - var hampster = App.Hampster.create({chores: [ - {name: 'cook', done: true}, - {name: 'clean', done: true}, - {name: 'write more unit tests', done: false} - ]}); - hampster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] - ``` + tom.helloWorld(); // alerts "Hi, my name is Tom Dale". + ``` - @method computed.filter - @for Ember - @param {String} dependentKey - @param {Function} callback - @return {Ember.ComputedProperty} the filtered array -*/ -Ember.computed.filter = function(dependentKey, callback) { - var options = { - initialize: function (array, changeMeta, instanceMeta) { - instanceMeta.filteredArrayIndexes = new Ember.SubArray(); - }, + `create` will call the `init` function if defined during + `Ember.AnyObject.extend` - addedItem: function(array, item, changeMeta, instanceMeta) { - var match = !!callback(item), - filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); + If no arguments are passed to `create`, it will not set values to the new + instance during initialization: - if (match) { - array.insertAt(filterIndex, item); + ```javascript + var noName = App.Person.create(); + noName.helloWorld(); // alerts undefined + ``` + + NOTE: For performance reasons, you cannot declare methods or computed + properties during `create`. You should instead declare methods and computed + properties when using `extend` or use the `createWithMixins` shorthand. + + @method create + @static + @param [arguments]* + */ + create: function() { + var C = this; + if (arguments.length>0) { this._initProperties(arguments); } + return new C(); + }, + + /** + Augments a constructor's prototype with additional + properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + o = MyObject.create(); + o.get('name'); // 'an object' + + MyObject.reopen({ + say: function(msg){ + console.log(msg); } + }) - return array; - }, + o2 = MyObject.create(); + o2.say("hello"); // logs "hello" - removedItem: function(array, item, changeMeta, instanceMeta) { - var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); + o.say("goodbye"); // logs "goodbye" + ``` - if (filterIndex > -1) { - array.removeAt(filterIndex); + To add functions and properties to the constructor itself, + see `reopenClass` + + @method reopen + */ + reopen: function() { + this.willReopen(); + reopen.apply(this.PrototypeMixin, arguments); + return this; + }, + + /** + Augments a constructor's own properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + MyObject.reopenClass({ + canBuild: false + }); + + MyObject.canBuild; // false + o = MyObject.create(); + ``` + + In other words, this creates static properties and functions for the class. These are only available on the class + and not on any instance of that class. + + ```javascript + App.Person = Ember.Object.extend({ + name : "", + sayHello : function(){ + alert("Hello. My name is " + this.get('name')); } + }); - return array; - } - }; + App.Person.reopenClass({ + species : "Homo sapiens", + createPerson: function(newPersonsName){ + return App.Person.create({ + name:newPersonsName + }); + } + }); - return Ember.arrayComputed(dependentKey, options); -}; + var tom = App.Person.create({ + name : "Tom Dale" + }); + var yehuda = App.Person.createPerson("Yehuda Katz"); -/** - Filters the array by the property and value + tom.sayHello(); // "Hello. My name is Tom Dale" + yehuda.sayHello(); // "Hello. My name is Yehuda Katz" + alert(App.Person.species); // "Homo sapiens" + ``` - Example + Note that `species` and `createPerson` are *not* valid on the `tom` and `yehuda` + variables. They are only valid on `App.Person`. - ```javascript - App.Hampster = Ember.Object.extend({ - remainingChores: Ember.computed.filterBy('chores', 'done', false) - }); + To add functions and properties to instances of + a constructor by extending the constructor's prototype + see `reopen` - var hampster = App.Hampster.create({chores: [ - {name: 'cook', done: true}, - {name: 'clean', done: true}, - {name: 'write more unit tests', done: false} - ]}); - hampster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] - ``` + @method reopenClass + */ + reopenClass: function() { + reopen.apply(this.ClassMixin, arguments); + applyMixin(this, arguments, false); + return this; + }, - @method computed.filterBy - @for Ember - @param {String} dependentKey - @param {String} propertyKey - @param {String} value - @return {Ember.ComputedProperty} the filtered array -*/ -Ember.computed.filterBy = function(dependentKey, propertyKey, value) { - var callback; + detect: function(obj) { + if ('function' !== typeof obj) { return false; } + while(obj) { + if (obj===this) { return true; } + obj = obj.superclass; + } + return false; + }, - if (arguments.length === 2) { - callback = function(item) { - return get(item, propertyKey); - }; - } else { - callback = function(item) { - return get(item, propertyKey) === value; - }; - } + detectInstance: function(obj) { + return obj instanceof this; + }, - return Ember.computed.filter(dependentKey + '.@each.' + propertyKey, callback); -}; + /** + In some cases, you may want to annotate computed properties with additional + metadata about how they function or what values they operate on. For + example, computed property functions may close over variables that are then + no longer available for introspection. -/** - @method computed.filterProperty - @for Ember - @param dependentKey - @param propertyKey - @param value - @deprecated Use `Ember.computed.filterBy` instead -*/ -Ember.computed.filterProperty = Ember.computed.filterBy; + You can pass a hash of these values to a computed property like this: -/** - A computed property which returns a new array with all the unique - elements from one or more dependent arrays. + ```javascript + person: function() { + var personId = this.get('personId'); + return App.Person.create({ id: personId }); + }.property().meta({ type: App.Person }) + ``` - Example + Once you've done this, you can retrieve the values saved to the computed + property from your class like this: - ```javascript - App.Hampster = Ember.Object.extend({ - uniqueFruits: Ember.computed.uniq('fruits') - }); + ```javascript + MyClass.metaForProperty('person'); + ``` - var hampster = App.Hampster.create({fruits: [ - 'banana', - 'grape', - 'kale', - 'banana' - ]}); - hampster.get('uniqueFruits'); // ['banana', 'grape', 'kale'] - ``` + This will return the original hash that was passed to `meta()`. - @method computed.uniq - @for Ember - @param {String} propertyKey* - @return {Ember.ComputedProperty} computes a new array with all the - unique elements from the dependent array -*/ -Ember.computed.uniq = function() { - var args = a_slice.call(arguments); - args.push({ - initialize: function(array, changeMeta, instanceMeta) { - instanceMeta.itemCounts = {}; - }, + @method metaForProperty + @param key {String} property name + */ + metaForProperty: function(key) { + var meta = this.proto()[META_KEY], + desc = meta && meta.descs[key]; - addedItem: function(array, item, changeMeta, instanceMeta) { - var guid = guidFor(item); + Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); + return desc._meta || {}; + }, - if (!instanceMeta.itemCounts[guid]) { - instanceMeta.itemCounts[guid] = 1; - } else { - ++instanceMeta.itemCounts[guid]; - } - array.addObject(item); - return array; - }, - removedItem: function(array, item, _, instanceMeta) { - var guid = guidFor(item), - itemCounts = instanceMeta.itemCounts; + /** + Iterate over each computed property for the class, passing its name + and any associated metadata (see `metaForProperty`) to the callback. - if (--itemCounts[guid] === 0) { - array.removeObject(item); + @method eachComputedProperty + @param {Function} callback + @param {Object} binding + */ + eachComputedProperty: function(callback, binding) { + var proto = this.proto(), + descs = meta(proto).descs, + empty = {}, + property; + + for (var name in descs) { + property = descs[name]; + + if (property instanceof Ember.ComputedProperty) { + callback.call(binding || this, name, property._meta || empty); } - return array; } - }); - return Ember.arrayComputed.apply(null, args); -}; + } -/** - Alias for [Ember.computed.uniq](/api/#method_computed_uniq). +}); - @method computed.union - @for Ember - @param {String} propertyKey* - @return {Ember.ComputedProperty} computes a new array with all the - unique elements from the dependent array -*/ -Ember.computed.union = Ember.computed.uniq; +ClassMixin.ownerConstructor = CoreObject; -/** - A computed property which returns a new array with all the duplicated - elements from two or more dependeny arrays. +if (Ember.config.overrideClassMixin) { + Ember.config.overrideClassMixin(ClassMixin); +} - Example +CoreObject.ClassMixin = ClassMixin; +ClassMixin.apply(CoreObject); - ```javascript - var obj = Ember.Object.createWithMixins({ - adaFriends: ['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'], - charlesFriends: ['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock'], - friendsInCommon: Ember.computed.intersect('adaFriends', 'charlesFriends') - }); +Ember.CoreObject = CoreObject; - obj.get('friendsInCommon'); // ['William King', 'Mary Somerville'] - ``` +})(); - @method computed.intersect - @for Ember - @param {String} propertyKey* - @return {Ember.ComputedProperty} computes a new array with all the - duplicated elements from the dependent arrays + + +(function() { +/** +@module ember +@submodule ember-runtime */ -Ember.computed.intersect = function () { - var getDependentKeyGuids = function (changeMeta) { - return map(changeMeta.property._dependentKeys, function (dependentKey) { - return guidFor(dependentKey); - }); - }; - var args = a_slice.call(arguments); - args.push({ - initialize: function (array, changeMeta, instanceMeta) { - instanceMeta.itemCounts = {}; - }, +/** + `Ember.Object` is the main base class for all Ember objects. It is a subclass + of `Ember.CoreObject` with the `Ember.Observable` mixin applied. For details, + see the documentation for each of these. - addedItem: function(array, item, changeMeta, instanceMeta) { - var itemGuid = guidFor(item), - dependentGuids = getDependentKeyGuids(changeMeta), - dependentGuid = guidFor(changeMeta.arrayChanged), - numberOfDependentArrays = changeMeta.property._dependentKeys.length, - itemCounts = instanceMeta.itemCounts; + @class Object + @namespace Ember + @extends Ember.CoreObject + @uses Ember.Observable +*/ +Ember.Object = Ember.CoreObject.extend(Ember.Observable); +Ember.Object.toString = function() { return "Ember.Object"; }; - if (!itemCounts[itemGuid]) { itemCounts[itemGuid] = {}; } - if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } +})(); - if (++itemCounts[itemGuid][dependentGuid] === 1 && - numberOfDependentArrays === Ember.keys(itemCounts[itemGuid]).length) { - array.addObject(item); - } - return array; - }, - removedItem: function(array, item, changeMeta, instanceMeta) { - var itemGuid = guidFor(item), - dependentGuids = getDependentKeyGuids(changeMeta), - dependentGuid = guidFor(changeMeta.arrayChanged), - numberOfDependentArrays = changeMeta.property._dependentKeys.length, - numberOfArraysItemAppearsIn, - itemCounts = instanceMeta.itemCounts; - if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } - if (--itemCounts[itemGuid][dependentGuid] === 0) { - delete itemCounts[itemGuid][dependentGuid]; - numberOfArraysItemAppearsIn = Ember.keys(itemCounts[itemGuid]).length; +(function() { +/** +@module ember +@submodule ember-runtime +*/ - if (numberOfArraysItemAppearsIn === 0) { - delete itemCounts[itemGuid]; - } - array.removeObject(item); - } - return array; - } - }); - return Ember.arrayComputed.apply(null, args); -}; +var get = Ember.get, indexOf = Ember.ArrayPolyfills.indexOf; /** - A computed property which returns a new array with all the - properties from the first dependent array that are not in the second - dependent array. + A Namespace is an object usually used to contain other objects or methods + such as an application or framework. Create a namespace anytime you want + to define one of these new containers. - Example + # Example Usage ```javascript - App.Hampster = Ember.Object.extend({ - likes: ['banana', 'grape', 'kale'], - wants: Ember.computed.setDiff('likes', 'fruits') + MyFramework = Ember.Namespace.create({ + VERSION: '1.0.0' }); - - var hampster = App.Hampster.create({fruits: [ - 'grape', - 'kale', - ]}); - hampster.get('wants'); // ['banana'] ``` - @method computed.setDiff - @for Ember - @param {String} setAProperty - @param {String} setBProperty - @return {Ember.ComputedProperty} computes a new array with all the - items from the first dependent array that are not in the second - dependent array + @class Namespace + @namespace Ember + @extends Ember.Object */ -Ember.computed.setDiff = function (setAProperty, setBProperty) { - if (arguments.length !== 2) { - throw new Error("setDiff requires exactly two dependent arrays."); - } - return Ember.arrayComputed.call(null, setAProperty, setBProperty, { - addedItem: function (array, item, changeMeta, instanceMeta) { - var setA = get(this, setAProperty), - setB = get(this, setBProperty); +var Namespace = Ember.Namespace = Ember.Object.extend({ + isNamespace: true, - if (changeMeta.arrayChanged === setA) { - if (!setB.contains(item)) { - array.addObject(item); - } - } else { - array.removeObject(item); - } - return array; - }, + init: function() { + Ember.Namespace.NAMESPACES.push(this); + Ember.Namespace.PROCESSED = false; + }, - removedItem: function (array, item, changeMeta, instanceMeta) { - var setA = get(this, setAProperty), - setB = get(this, setBProperty); + toString: function() { + var name = get(this, 'name'); + if (name) { return name; } - if (changeMeta.arrayChanged === setB) { - if (setA.contains(item)) { - array.addObject(item); - } - } else { - array.removeObject(item); - } - return array; - } - }); -}; + findNamespaces(); + return this[Ember.GUID_KEY+'_name']; + }, -function binarySearch(array, item, low, high) { - var mid, midItem, res, guidMid, guidItem; + nameClasses: function() { + processNamespace([this.toString()], this, {}); + }, - if (arguments.length < 4) { high = get(array, 'length'); } - if (arguments.length < 3) { low = 0; } + destroy: function() { + var namespaces = Ember.Namespace.NAMESPACES; + Ember.lookup[this.toString()] = undefined; + namespaces.splice(indexOf.call(namespaces, this), 1); + this._super(); + } +}); - if (low === high) { - return low; +Namespace.reopenClass({ + NAMESPACES: [Ember], + NAMESPACES_BY_ID: {}, + PROCESSED: false, + processAll: processAllNamespaces, + byName: function(name) { + if (!Ember.BOOTED) { + processAllNamespaces(); + } + + return NAMESPACES_BY_ID[name]; } +}); - mid = low + Math.floor((high - low) / 2); - midItem = array.objectAt(mid); +var NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID; - guidMid = _guidFor(midItem); - guidItem = _guidFor(item); +var hasOwnProp = ({}).hasOwnProperty, + guidFor = Ember.guidFor; - if (guidMid === guidItem) { - return mid; - } +function processNamespace(paths, root, seen) { + var idx = paths.length; - res = this.order(midItem, item); - if (res === 0) { - res = guidMid < guidItem ? -1 : 1; - } + NAMESPACES_BY_ID[paths.join('.')] = root; + // Loop over all of the keys in the namespace, looking for classes + for(var key in root) { + if (!hasOwnProp.call(root, key)) { continue; } + var obj = root[key]; - if (res < 0) { - return this.binarySearch(array, item, mid+1, high); - } else if (res > 0) { - return this.binarySearch(array, item, low, mid); - } + // If we are processing the `Ember` namespace, for example, the + // `paths` will start with `["Ember"]`. Every iteration through + // the loop will update the **second** element of this list with + // the key, so processing `Ember.View` will make the Array + // `['Ember', 'View']`. + paths[idx] = key; - return mid; + // If we have found an unprocessed class + if (obj && obj.toString === classToString) { + // Replace the class' `toString` with the dot-separated path + // and set its `NAME_KEY` + obj.toString = makeToString(paths.join('.')); + obj[NAME_KEY] = paths.join('.'); - function _guidFor(item) { - if (Ember.ObjectProxy.detectInstance(item)) { - return guidFor(get(item, 'content')); + // Support nested namespaces + } else if (obj && obj.isNamespace) { + // Skip aliased namespaces + if (seen[guidFor(obj)]) { continue; } + seen[guidFor(obj)] = true; + + // Process the child namespace + processNamespace(paths, obj, seen); } - return guidFor(item); } -} -/** - A computed property which returns a new array with all the - properties from the first dependent array sorted based on a property - or sort function. + paths.length = idx; // cut out last item +} - The callback method you provide should have the following signature: +function findNamespaces() { + var Namespace = Ember.Namespace, lookup = Ember.lookup, obj, isNamespace; - ```javascript - function(itemA, itemB); - ``` + if (Namespace.PROCESSED) { return; } - - `itemA` the first item to compare. - - `itemB` the second item to compare. + for (var prop in lookup) { + // These don't raise exceptions but can cause warnings + if (prop === "parent" || prop === "top" || prop === "frameElement" || prop === "webkitStorageInfo") { continue; } - This function should return `-1` when `itemA` should come before - `itemB`. It should return `1` when `itemA` should come after - `itemB`. If the `itemA` and `itemB` are equal this function should return `0`. + // get(window.globalStorage, 'isNamespace') would try to read the storage for domain isNamespace and cause exception in Firefox. + // globalStorage is a storage obsoleted by the WhatWG storage specification. See https://developer.mozilla.org/en/DOM/Storage#globalStorage + if (prop === "globalStorage" && lookup.StorageList && lookup.globalStorage instanceof lookup.StorageList) { continue; } + // Unfortunately, some versions of IE don't support window.hasOwnProperty + if (lookup.hasOwnProperty && !lookup.hasOwnProperty(prop)) { continue; } - Example + // At times we are not allowed to access certain properties for security reasons. + // There are also times where even if we can access them, we are not allowed to access their properties. + try { + obj = Ember.lookup[prop]; + isNamespace = obj && obj.isNamespace; + } catch (e) { + continue; + } - ```javascript - var ToDoList = Ember.Object.extend({ - todosSorting: ['name'], - sortedTodos: Ember.computed.sort('todos', 'todosSorting'), - priorityTodos: Ember.computed.sort('todos', function(a, b){ - if (a.priority > b.priority) { - return 1; - } else if (a.priority < b.priority) { - return -1; - } - return 0; - }), - }); - var todoList = ToDoList.create({todos: [ - {name: 'Unit Test', priority: 2}, - {name: 'Documentation', priority: 3}, - {name: 'Release', priority: 1} - ]}); + if (isNamespace) { + Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); + obj[NAME_KEY] = prop; + } + } +} - todoList.get('sortedTodos'); // [{name:'Documentation', priority:3}, {name:'Release', priority:1}, {name:'Unit Test', priority:2}] - todoList.get('priroityTodos'); // [{name:'Release', priority:1}, {name:'Unit Test', priority:2}, {name:'Documentation', priority:3}] - ``` +var NAME_KEY = Ember.NAME_KEY = Ember.GUID_KEY + '_name'; - @method computed.sort - @for Ember - @param {String} dependentKey - @param {String or Function} sortDefinition a dependent key to an - array of sort properties or a function to use when sorting - @return {Ember.ComputedProperty} computes a new sorted array based - on the sort property array or callback function -*/ -Ember.computed.sort = function (itemsKey, sortDefinition) { - Ember.assert("Ember.computed.sort requires two arguments: an array key to sort and either a sort properties key or sort function", arguments.length === 2); +function superClassString(mixin) { + var superclass = mixin.superclass; + if (superclass) { + if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; } + else { return superClassString(superclass); } + } else { + return; + } +} - var initFn, sortPropertiesKey; +function classToString() { + if (!Ember.BOOTED && !this[NAME_KEY]) { + processAllNamespaces(); + } - if (typeof sortDefinition === 'function') { - initFn = function (array, changeMeta, instanceMeta) { - instanceMeta.order = sortDefinition; - instanceMeta.binarySearch = binarySearch; - }; + var ret; + + if (this[NAME_KEY]) { + ret = this[NAME_KEY]; + } else if (this._toString) { + ret = this._toString; } else { - sortPropertiesKey = sortDefinition; - initFn = function (array, changeMeta, instanceMeta) { - function setupSortProperties() { - var sortPropertyDefinitions = get(this, sortPropertiesKey), - sortProperty, - sortProperties = instanceMeta.sortProperties = [], - sortPropertyAscending = instanceMeta.sortPropertyAscending = {}, - idx, - asc; + var str = superClassString(this); + if (str) { + ret = "(subclass of " + str + ")"; + } else { + ret = "(unknown mixin)"; + } + this.toString = makeToString(ret); + } - Ember.assert("Cannot sort: '" + sortPropertiesKey + "' is not an array.", Ember.isArray(sortPropertyDefinitions)); + return ret; +} - changeMeta.property.clearItemPropertyKeys(itemsKey); +function processAllNamespaces() { + var unprocessedNamespaces = !Namespace.PROCESSED, + unprocessedMixins = Ember.anyUnprocessedMixins; - forEach(sortPropertyDefinitions, function (sortPropertyDefinition) { - if ((idx = sortPropertyDefinition.indexOf(':')) !== -1) { - sortProperty = sortPropertyDefinition.substring(0, idx); - asc = sortPropertyDefinition.substring(idx+1).toLowerCase() !== 'desc'; - } else { - sortProperty = sortPropertyDefinition; - asc = true; - } + if (unprocessedNamespaces) { + findNamespaces(); + Namespace.PROCESSED = true; + } - sortProperties.push(sortProperty); - sortPropertyAscending[sortProperty] = asc; - changeMeta.property.itemPropertyKey(itemsKey, sortProperty); - }); + if (unprocessedNamespaces || unprocessedMixins) { + var namespaces = Namespace.NAMESPACES, namespace; + for (var i=0, l=namespaces.length; i alpha - // > beta - // > gamma + var arr = []; + arr.get('firstObject'); // undefined ``` - @method w - @param {String} str The string to split - @return {String} split string + @property firstObject + @return {Object} the object or undefined */ - w: function(str) { return str.split(/\s+/); }, + firstObject: Ember.computed(function() { + if (get(this, 'length')===0) return undefined ; + + // handle generic enumerables + var context = popCtx(), ret; + ret = this.nextObject(0, null, context); + pushCtx(context); + return ret ; + }).property('[]'), /** - Converts a camelized string into all lower case separated by underscores. + Helper method returns the last object from a collection. If your enumerable + contains only one object, this method should always return that object. + If your enumerable is empty, this method should return `undefined`. ```javascript - 'innerHTML'.decamelize(); // 'inner_html' - 'action_name'.decamelize(); // 'action_name' - 'css-class-name'.decamelize(); // 'css-class-name' - 'my favorite items'.decamelize(); // 'my favorite items' + var arr = ["a", "b", "c"]; + arr.get('lastObject'); // "c" + + var arr = []; + arr.get('lastObject'); // undefined ``` - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. + @property lastObject + @return {Object} the last object or undefined */ - decamelize: function(str) { - return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); - }, + lastObject: Ember.computed(function() { + var len = get(this, 'length'); + if (len===0) return undefined ; + var context = popCtx(), idx=0, cur, last = null; + do { + last = cur; + cur = this.nextObject(idx++, last, context); + } while (cur !== undefined); + pushCtx(context); + return last; + }).property('[]'), /** - Replaces underscores, spaces, or camelCase with dashes. + Returns `true` if the passed object can be found in the receiver. The + default version will iterate through the enumerable until the object + is found. You may want to override this with a more efficient version. ```javascript - 'innerHTML'.dasherize(); // 'inner-html' - 'action_name'.dasherize(); // 'action-name' - 'css-class-name'.dasherize(); // 'css-class-name' - 'my favorite items'.dasherize(); // 'my-favorite-items' + var arr = ["a", "b", "c"]; + arr.contains("a"); // true + arr.contains("z"); // false ``` - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. + @method contains + @param {Object} obj The object to search for. + @return {Boolean} `true` if object is found in enumerable. */ - dasherize: function(str) { - var cache = STRING_DASHERIZE_CACHE, - hit = cache.hasOwnProperty(str), - ret; - - if (hit) { - return cache[str]; - } else { - ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); - cache[str] = ret; - } - - return ret; + contains: function(obj) { + return this.find(function(item) { return item===obj; }) !== undefined; }, /** - Returns the lowerCamelCase form of a string. + Iterates through the enumerable, calling the passed function on each + item. This method corresponds to the `forEach()` method defined in + JavaScript 1.6. + + The callback method you provide should have the following signature (all + parameters are optional): ```javascript - 'innerHTML'.camelize(); // 'innerHTML' - 'action_name'.camelize(); // 'actionName' - 'css-class-name'.camelize(); // 'cssClassName' - 'my favorite items'.camelize(); // 'myFavoriteItems' - 'My Favorite Items'.camelize(); // 'myFavoriteItems' + function(item, index, enumerable); ``` - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. + - `item` is the current item in the iteration. + - `index` is the current index in the iteration. + - `enumerable` is the enumerable object itself. + + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. This is a good way + to give your iterator function access to the current object. + + @method forEach + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @return {Object} receiver */ - camelize: function(str) { - return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { - return chr ? chr.toUpperCase() : ''; - }).replace(/^([A-Z])/, function(match, separator, chr) { - return match.toLowerCase(); - }); + forEach: function(callback, target) { + if (typeof callback !== "function") throw new TypeError() ; + var len = get(this, 'length'), last = null, context = popCtx(); + + if (target === undefined) target = null; + + for(var idx=0;idx1) args = a_slice.call(arguments, 1); - ```javascript - Ember.Mixin.create({ - doSomethingWithElement: function() { - // Executes whenever the "didInsertElement" event fires - }.on('didInsertElement') - }); - ``` + this.forEach(function(x, idx) { + var method = x && x[methodName]; + if ('function' === typeof method) { + ret[idx] = args ? method.apply(x, args) : x[methodName](); + } + }, this); - See `Ember.on`. + return ret; + }, - @method on - @for Function + /** + Simply converts the enumerable into a genuine array. The order is not + guaranteed. Corresponds to the method implemented by Prototype. + + @method toArray + @return {Array} the enumerable as an array. */ - Function.prototype.on = function() { - var events = a_slice.call(arguments); - this.__ember_listens__ = events; - return this; - }; -} + toArray: function() { + var ret = Ember.A(); + this.forEach(function(o, idx) { ret[idx] = o; }); + return ret ; + }, + /** + Returns a copy of the array with all null and undefined elements removed. -})(); + ```javascript + var arr = ["a", null, "c", undefined]; + arr.compact(); // ["a", "c"] + ``` + @method compact + @return {Array} the array without null and undefined elements. + */ + compact: function() { + return this.filter(function(value) { return value != null; }); + }, + /** + Returns a new enumerable that excludes the passed value. The default + implementation returns an array regardless of the receiver type unless + the receiver does not contain the value. -(function() { + ```javascript + var arr = ["a", "b", "a", "c"]; + arr.without("a"); // ["b", "c"] + ``` -})(); + @method without + @param {Object} value + @return {Ember.Enumerable} + */ + without: function(value) { + if (!this.contains(value)) return this; // nothing to do + var ret = Ember.A(); + this.forEach(function(k) { + if (k !== value) ret[ret.length] = k; + }) ; + return ret ; + }, + /** + Returns a new enumerable that contains only unique values. The default + implementation returns an array regardless of the receiver type. + ```javascript + var arr = ["a", "a", "b", "b"]; + arr.uniq(); // ["a", "b"] + ``` -(function() { -/** -@module ember -@submodule ember-runtime -*/ + @method uniq + @return {Ember.Enumerable} + */ + uniq: function() { + var ret = Ember.A(); + this.forEach(function(k) { + if (a_indexOf(ret, k)<0) ret.push(k); + }); + return ret; + }, + /** + This property will trigger anytime the enumerable's content changes. + You can observe this property to be notified of changes to the enumerables + content. -/** - Implements some standard methods for comparing objects. Add this mixin to - any class you create that can compare its instances. + For plain enumerables, this property is read only. `Ember.Array` overrides + this method. - You should implement the `compare()` method. + @property [] + @type Ember.Array + @return this + */ + '[]': Ember.computed(function(key, value) { + return this; + }), - @class Comparable - @namespace Ember - @since Ember 0.9 -*/ -Ember.Comparable = Ember.Mixin.create( /** @scope Ember.Comparable.prototype */{ + // .......................................................... + // ENUMERABLE OBSERVERS + // /** - Override to return the result of the comparison of the two parameters. The - compare method should return: + Registers an enumerable observer. Must implement `Ember.EnumerableObserver` + mixin. - - `-1` if `a < b` - - `0` if `a == b` - - `1` if `a > b` + @method addEnumerableObserver + @param {Object} target + @param {Hash} [opts] + @return this + */ + addEnumerableObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'enumerableWillChange', + didChange = (opts && opts.didChange) || 'enumerableDidChange'; - Default implementation raises an exception. + var hasObservers = get(this, 'hasEnumerableObservers'); + if (!hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); + Ember.addListener(this, '@enumerable:before', target, willChange); + Ember.addListener(this, '@enumerable:change', target, didChange); + if (!hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); + return this; + }, - @method compare - @param a {Object} the first object to compare - @param b {Object} the second object to compare - @return {Integer} the result of the comparison + /** + Removes a registered enumerable observer. + + @method removeEnumerableObserver + @param {Object} target + @param {Hash} [opts] + @return this */ - compare: Ember.required(Function) + removeEnumerableObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'enumerableWillChange', + didChange = (opts && opts.didChange) || 'enumerableDidChange'; -}); + var hasObservers = get(this, 'hasEnumerableObservers'); + if (hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); + Ember.removeListener(this, '@enumerable:before', target, willChange); + Ember.removeListener(this, '@enumerable:change', target, didChange); + if (hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); + return this; + }, + /** + Becomes true whenever the array currently has observers watching changes + on the array. -})(); + @property hasEnumerableObservers + @type Boolean + */ + hasEnumerableObservers: Ember.computed(function() { + return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before'); + }), + /** + Invoke this method just before the contents of your enumerable will + change. You can either omit the parameters completely or pass the objects + to be removed or added if available or just a count. -(function() { -/** -@module ember -@submodule ember-runtime -*/ + @method enumerableContentWillChange + @param {Ember.Enumerable|Number} removing An enumerable of the objects to + be removed or the number of items to be removed. + @param {Ember.Enumerable|Number} adding An enumerable of the objects to be + added or the number of items to be added. + @chainable + */ + enumerableContentWillChange: function(removing, adding) { + var removeCnt, addCnt, hasDelta; + if ('number' === typeof removing) removeCnt = removing; + else if (removing) removeCnt = get(removing, 'length'); + else removeCnt = removing = -1; -var get = Ember.get, set = Ember.set; + if ('number' === typeof adding) addCnt = adding; + else if (adding) addCnt = get(adding,'length'); + else addCnt = adding = -1; -/** - Implements some standard methods for copying an object. Add this mixin to - any object you create that can create a copy of itself. This mixin is - added automatically to the built-in array. + hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; - You should generally implement the `copy()` method to return a copy of the - receiver. + if (removing === -1) removing = null; + if (adding === -1) adding = null; - Note that `frozenCopy()` will only work if you also implement - `Ember.Freezable`. + Ember.propertyWillChange(this, '[]'); + if (hasDelta) Ember.propertyWillChange(this, 'length'); + Ember.sendEvent(this, '@enumerable:before', [this, removing, adding]); - @class Copyable - @namespace Ember - @since Ember 0.9 -*/ -Ember.Copyable = Ember.Mixin.create(/** @scope Ember.Copyable.prototype */ { + return this; + }, /** - Override to return a copy of the receiver. Default implementation raises - an exception. + Invoke this method when the contents of your enumerable has changed. + This will notify any observers watching for content changes. If your are + implementing an ordered enumerable (such as an array), also pass the + start and end values where the content changed so that it can be used to + notify range observers. - @method copy - @param {Boolean} deep if `true`, a deep copy of the object should be made - @return {Object} copy of receiver + @method enumerableContentDidChange + @param {Ember.Enumerable|Number} removing An enumerable of the objects to + be removed or the number of items to be removed. + @param {Ember.Enumerable|Number} adding An enumerable of the objects to + be added or the number of items to be added. + @chainable */ - copy: Ember.required(Function), + enumerableContentDidChange: function(removing, adding) { + var removeCnt, addCnt, hasDelta; - /** - If the object implements `Ember.Freezable`, then this will return a new - copy if the object is not frozen and the receiver if the object is frozen. + if ('number' === typeof removing) removeCnt = removing; + else if (removing) removeCnt = get(removing, 'length'); + else removeCnt = removing = -1; - Raises an exception if you try to call this method on a object that does - not support freezing. + if ('number' === typeof adding) addCnt = adding; + else if (adding) addCnt = get(adding, 'length'); + else addCnt = adding = -1; - You should use this method whenever you want a copy of a freezable object - since a freezable object can simply return itself without actually - consuming more memory. + hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; - @method frozenCopy - @return {Object} copy of receiver or receiver - */ - frozenCopy: function() { - if (Ember.Freezable && Ember.Freezable.detect(this)) { - return get(this, 'isFrozen') ? this : this.copy().freeze(); - } else { - throw new Error(Ember.String.fmt("%@ does not support freezing", [this])); - } + if (removing === -1) removing = null; + if (adding === -1) adding = null; + + Ember.sendEvent(this, '@enumerable:change', [this, removing, adding]); + if (hasDelta) Ember.propertyDidChange(this, 'length'); + Ember.propertyDidChange(this, '[]'); + + return this ; + }, + + /** + Converts the enumerable into an array and sorts by the keys + specified in the argument. + + You may provide multiple arguments to sort by multiple properties. + + @method sortBy + @param {String} property name(s) to sort on + @return {Array} The sorted array. + */ + sortBy: function() { + var sortKeys = arguments; + return this.toArray().sort(function(a, b){ + for(var i = 0; i < sortKeys.length; i++) { + var key = sortKeys[i], + propA = get(a, key), + propB = get(b, key); + // return 1 or -1 else continue to the next sortKey + var compareValue = Ember.compare(propA, propB); + if (compareValue) { return compareValue; } + } + return 0; + }); } }); @@ -13139,1163 +14699,2434 @@ Ember.Copyable = Ember.Mixin.create(/** @scope Ember.Copyable.prototype */ { @submodule ember-runtime */ +// .......................................................... +// HELPERS +// -var get = Ember.get, set = Ember.set; - -/** - The `Ember.Freezable` mixin implements some basic methods for marking an - object as frozen. Once an object is frozen it should be read only. No changes - may be made the internal state of the object. - - ## Enforcement - - To fully support freezing in your subclass, you must include this mixin and - override any method that might alter any property on the object to instead - raise an exception. You can check the state of an object by checking the - `isFrozen` property. - - Although future versions of JavaScript may support language-level freezing - object objects, that is not the case today. Even if an object is freezable, - it is still technically possible to modify the object, even though it could - break other parts of your application that do not expect a frozen object to - change. It is, therefore, very important that you always respect the - `isFrozen` property on all freezable objects. - - ## Example Usage - - The example below shows a simple object that implement the `Ember.Freezable` - protocol. - - ```javascript - Contact = Ember.Object.extend(Ember.Freezable, { - firstName: null, - lastName: null, - - // swaps the names - swapNames: function() { - if (this.get('isFrozen')) throw Ember.FROZEN_ERROR; - var tmp = this.get('firstName'); - this.set('firstName', this.get('lastName')); - this.set('lastName', tmp); - return this; - } - - }); - - c = Context.create({ firstName: "John", lastName: "Doe" }); - c.swapNames(); // returns c - c.freeze(); - c.swapNames(); // EXCEPTION - ``` - - ## Copying - - Usually the `Ember.Freezable` protocol is implemented in cooperation with the - `Ember.Copyable` protocol, which defines a `frozenCopy()` method that will - return a frozen object, if the object implements this method as well. - - @class Freezable - @namespace Ember - @since Ember 0.9 -*/ -Ember.Freezable = Ember.Mixin.create(/** @scope Ember.Freezable.prototype */ { - - /** - Set to `true` when the object is frozen. Use this property to detect - whether your object is frozen or not. - - @property isFrozen - @type Boolean - */ - isFrozen: false, - - /** - Freezes the object. Once this method has been called the object should - no longer allow any properties to be edited. - - @method freeze - @return {Object} receiver - */ - freeze: function() { - if (get(this, 'isFrozen')) return this; - set(this, 'isFrozen', true); - return this; - } - -}); - -Ember.FROZEN_ERROR = "Frozen object cannot be modified."; - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var forEach = Ember.EnumerableUtils.forEach; +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; +// .......................................................... +// ARRAY +// /** - This mixin defines the API for modifying generic enumerables. These methods - can be applied to an object regardless of whether it is ordered or - unordered. - - Note that an Enumerable can change even if it does not implement this mixin. - For example, a MappedEnumerable cannot be directly modified but if its - underlying enumerable changes, it will change also. - - ## Adding Objects - - To add an object to an enumerable, use the `addObject()` method. This - method will only add the object to the enumerable if the object is not - already present and is of a type supported by the enumerable. - - ```javascript - set.addObject(contact); - ``` + This module implements Observer-friendly Array-like behavior. This mixin is + picked up by the Array class as well as other controllers, etc. that want to + appear to be arrays. - ## Removing Objects + Unlike `Ember.Enumerable,` this mixin defines methods specifically for + collections that provide index-ordered access to their contents. When you + are designing code that needs to accept any kind of Array-like object, you + should use these methods instead of Array primitives because these will + properly notify observers of changes to the array. - To remove an object from an enumerable, use the `removeObject()` method. This - will only remove the object if it is present in the enumerable, otherwise - this method has no effect. + Although these methods are efficient, they do add a layer of indirection to + your application so it is a good idea to use them only when you need the + flexibility of using both true JavaScript arrays and "virtual" arrays such + as controllers and collections. - ```javascript - set.removeObject(contact); - ``` + You can use the methods defined in this module to access and modify array + contents in a KVO-friendly way. You can also be notified whenever the + membership of an array changes by changing the syntax of the property to + `.observes('*myProperty.[]')`. - ## Implementing In Your Own Code + To support `Ember.Array` in your own class, you must override two + primitives to use it: `replace()` and `objectAt()`. - If you are implementing an object and want to support this API, just include - this mixin in your class and implement the required methods. In your unit - tests, be sure to apply the Ember.MutableEnumerableTests to your object. + Note that the Ember.Array mixin also incorporates the `Ember.Enumerable` + mixin. All `Ember.Array`-like objects are also enumerable. - @class MutableEnumerable + @class Array @namespace Ember @uses Ember.Enumerable + @since Ember 0.9.0 */ -Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { - - /** - __Required.__ You must implement this method to apply this mixin. - - Attempts to add the passed object to the receiver if the object is not - already present in the collection. If the object is present, this method - has no effect. - - If the passed object is of a type not supported by the receiver, - then this method should raise an exception. - - @method addObject - @param {Object} object The object to add to the enumerable. - @return {Object} the passed object - */ - addObject: Ember.required(Function), +Ember.Array = Ember.Mixin.create(Ember.Enumerable, { /** - Adds each object in the passed enumerable to the receiver. + Your array must support the `length` property. Your replace methods should + set this property whenever it changes. - @method addObjects - @param {Ember.Enumerable} objects the objects to add. - @return {Object} receiver + @property {Number} length */ - addObjects: function(objects) { - Ember.beginPropertyChanges(this); - forEach(objects, function(obj) { this.addObject(obj); }, this); - Ember.endPropertyChanges(this); - return this; - }, + length: Ember.required(), /** - __Required.__ You must implement this method to apply this mixin. + Returns the object at the given `index`. If the given `index` is negative + or is greater or equal than the array length, returns `undefined`. - Attempts to remove the passed object from the receiver collection if the - object is present in the collection. If the object is not present, - this method has no effect. + This is one of the primitives you must implement to support `Ember.Array`. + If your object supports retrieving the value of an array item using `get()` + (i.e. `myArray.get(0)`), then you do not need to implement this method + yourself. - If the passed object is of a type not supported by the receiver, - then this method should raise an exception. + ```javascript + var arr = ['a', 'b', 'c', 'd']; + arr.objectAt(0); // "a" + arr.objectAt(3); // "d" + arr.objectAt(-1); // undefined + arr.objectAt(4); // undefined + arr.objectAt(5); // undefined + ``` - @method removeObject - @param {Object} object The object to remove from the enumerable. - @return {Object} the passed object + @method objectAt + @param {Number} idx The index of the item to return. + @return {*} item at index or undefined */ - removeObject: Ember.required(Function), - + objectAt: function(idx) { + if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; + return get(this, idx); + }, /** - Removes each object in the passed enumerable from the receiver. - - @method removeObjects - @param {Ember.Enumerable} objects the objects to remove - @return {Object} receiver - */ - removeObjects: function(objects) { - Ember.beginPropertyChanges(this); - forEach(objects, function(obj) { this.removeObject(obj); }, this); - Ember.endPropertyChanges(this); - return this; - } - -}); - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ -// .......................................................... -// CONSTANTS -// - -var OUT_OF_RANGE_EXCEPTION = "Index out of range" ; -var EMPTY = []; - -// .......................................................... -// HELPERS -// - -var get = Ember.get, set = Ember.set; + This returns the objects at the specified indexes, using `objectAt`. -/** - This mixin defines the API for modifying array-like objects. These methods - can be applied only to a collection that keeps its items in an ordered set. + ```javascript + var arr = ['a', 'b', 'c', 'd']; + arr.objectsAt([0, 1, 2]); // ["a", "b", "c"] + arr.objectsAt([2, 3, 4]); // ["c", "d", undefined] + ``` - Note that an Array can change even if it does not implement this mixin. - For example, one might implement a SparseArray that cannot be directly - modified, but if its underlying enumerable changes, it will change also. + @method objectsAt + @param {Array} indexes An array of indexes of items to return. + @return {Array} + */ + objectsAt: function(indexes) { + var self = this; + return map(indexes, function(idx) { return self.objectAt(idx); }); + }, - @class MutableArray - @namespace Ember - @uses Ember.Array - @uses Ember.MutableEnumerable -*/ -Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** @scope Ember.MutableArray.prototype */ { + // overrides Ember.Enumerable version + nextObject: function(idx) { + return this.objectAt(idx); + }, /** - __Required.__ You must implement this method to apply this mixin. + This is the handler for the special array content property. If you get + this property, it will return this. If you set this property it a new + array, it will replace the current content. - This is one of the primitives you must implement to support `Ember.Array`. - You should replace amt objects started at idx with the objects in the - passed array. You should also call `this.enumerableContentDidChange()` + This property overrides the default property defined in `Ember.Enumerable`. - @method replace - @param {Number} idx Starting index in the array to replace. If - idx >= length, then append to the end of the array. - @param {Number} amt Number of elements that should be removed from - the array, starting at *idx*. - @param {Array} objects An array of zero or more objects that should be - inserted into the array at *idx* + @property [] + @return this */ - replace: Ember.required(), + '[]': Ember.computed(function(key, value) { + if (value !== undefined) this.replace(0, get(this, 'length'), value) ; + return this ; + }), - /** - Remove all elements from self. This is useful if you - want to reuse an existing array without having to recreate it. + firstObject: Ember.computed(function() { + return this.objectAt(0); + }), - ```javascript - var colors = ["red", "green", "blue"]; - color.length(); // 3 - colors.clear(); // [] - colors.length(); // 0 - ``` + lastObject: Ember.computed(function() { + return this.objectAt(get(this, 'length')-1); + }), - @method clear - @return {Ember.Array} An empty Array. - */ - clear: function () { - var len = get(this, 'length'); - if (len === 0) return this; - this.replace(0, len, EMPTY); - return this; + // optimized version from Enumerable + contains: function(obj) { + return this.indexOf(obj) >= 0; }, + // Add any extra methods to Ember.Array that are native to the built-in Array. /** - This will use the primitive `replace()` method to insert an object at the - specified index. + Returns a new array that is a slice of the receiver. This implementation + uses the observable array methods to retrieve the objects for the new + slice. ```javascript - var colors = ["red", "green", "blue"]; - colors.insertAt(2, "yellow"); // ["red", "green", "yellow", "blue"] - colors.insertAt(5, "orange"); // Error: Index out of range + var arr = ['red', 'green', 'blue']; + arr.slice(0); // ['red', 'green', 'blue'] + arr.slice(0, 2); // ['red', 'green'] + arr.slice(1, 100); // ['green', 'blue'] ``` - @method insertAt - @param {Number} idx index of insert the object at. - @param {Object} object object to insert - @return this + @method slice + @param {Integer} beginIndex (Optional) index to begin slicing from. + @param {Integer} endIndex (Optional) index to end the slice at (but not included). + @return {Array} New array with specified slice */ - insertAt: function(idx, object) { - if (idx > get(this, 'length')) throw new Error(OUT_OF_RANGE_EXCEPTION) ; - this.replace(idx, 0, [object]) ; - return this ; + slice: function(beginIndex, endIndex) { + var ret = Ember.A(); + var length = get(this, 'length') ; + if (isNone(beginIndex)) beginIndex = 0 ; + if (isNone(endIndex) || (endIndex > length)) endIndex = length ; + + if (beginIndex < 0) beginIndex = length + beginIndex; + if (endIndex < 0) endIndex = length + endIndex; + + while(beginIndex < endIndex) { + ret[ret.length] = this.objectAt(beginIndex++) ; + } + return ret ; }, /** - Remove an object at the specified index using the `replace()` primitive - method. You can pass either a single index, or a start and a length. - - If you pass a start and length that is beyond the - length this method will throw an `OUT_OF_RANGE_EXCEPTION` + Returns the index of the given object's first occurrence. + If no `startAt` argument is given, the starting location to + search is 0. If it's negative, will count backward from + the end of the array. Returns -1 if no match is found. ```javascript - var colors = ["red", "green", "blue", "yellow", "orange"]; - colors.removeAt(0); // ["green", "blue", "yellow", "orange"] - colors.removeAt(2, 2); // ["green", "blue"] - colors.removeAt(4, 2); // Error: Index out of range + var arr = ["a", "b", "c", "d", "a"]; + arr.indexOf("a"); // 0 + arr.indexOf("z"); // -1 + arr.indexOf("a", 2); // 4 + arr.indexOf("a", -1); // 4 + arr.indexOf("b", 3); // -1 + arr.indexOf("a", 100); // -1 ``` - @method removeAt - @param {Number} start index, start of range - @param {Number} len length of passing range - @return {Object} receiver + @method indexOf + @param {Object} object the item to search for + @param {Number} startAt optional starting location to search, default 0 + @return {Number} index or -1 if not found */ - removeAt: function(start, len) { - if ('number' === typeof start) { + indexOf: function(object, startAt) { + var idx, len = get(this, 'length'); - if ((start < 0) || (start >= get(this, 'length'))) { - throw new Error(OUT_OF_RANGE_EXCEPTION); - } + if (startAt === undefined) startAt = 0; + if (startAt < 0) startAt += len; - // fast case - if (len === undefined) len = 1; - this.replace(start, len, EMPTY); + for(idx=startAt;idx= len) startAt = len-1; + if (startAt < 0) startAt += len; + + for(idx=startAt;idx>=0;idx--) { + if (this.objectAt(idx) === object) return idx ; + } + return -1; }, + // .......................................................... + // ARRAY OBSERVERS + // + /** - Add the objects in the passed numerable to the end of the array. Defers - notifying observers of the change until all objects are added. + Adds an array observer to the receiving array. The array observer object + normally must implement two methods: - ```javascript - var colors = ["red", "green", "blue"]; - colors.pushObjects(["black"]); // ["red", "green", "blue", "black"] - colors.pushObjects(["yellow", "orange"]); // ["red", "green", "blue", "black", "yellow", "orange"] - ``` + * `arrayWillChange(observedObj, start, removeCount, addCount)` - This method will be + called just before the array is modified. + * `arrayDidChange(observedObj, start, removeCount, addCount)` - This method will be + called just after the array is modified. - @method pushObjects - @param {Ember.Enumerable} objects the objects to add + Both callbacks will be passed the observed object, starting index of the + change as well a a count of the items to be removed and added. You can use + these callbacks to optionally inspect the array during the change, clear + caches, or do any other bookkeeping necessary. + + In addition to passing a target, you can also include an options hash + which you can use to override the method names that will be invoked on the + target. + + @method addArrayObserver + @param {Object} target The observer object. + @param {Hash} opts Optional hash of configuration options including + `willChange` and `didChange` option. @return {Ember.Array} receiver */ - pushObjects: function(objects) { - if (!(Ember.Enumerable.detect(objects) || Ember.isArray(objects))) { - throw new TypeError("Must pass Ember.Enumerable to Ember.MutableArray#pushObjects"); - } - this.replace(get(this, 'length'), 0, objects); + addArrayObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'arrayWillChange', + didChange = (opts && opts.didChange) || 'arrayDidChange'; + + var hasObservers = get(this, 'hasArrayObservers'); + if (!hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); + Ember.addListener(this, '@array:before', target, willChange); + Ember.addListener(this, '@array:change', target, didChange); + if (!hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); return this; }, /** - Pop object from array or nil if none are left. Works just like `pop()` but - it is KVO-compliant. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.popObject(); // "blue" - console.log(colors); // ["red", "green"] - ``` + Removes an array observer from the object if the observer is current + registered. Calling this method multiple times with the same object will + have no effect. - @method popObject - @return object + @method removeArrayObserver + @param {Object} target The object observing the array. + @param {Hash} opts Optional hash of configuration options including + `willChange` and `didChange` option. + @return {Ember.Array} receiver */ - popObject: function() { - var len = get(this, 'length') ; - if (len === 0) return null ; + removeArrayObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'arrayWillChange', + didChange = (opts && opts.didChange) || 'arrayDidChange'; - var ret = this.objectAt(len-1) ; - this.removeAt(len-1, 1) ; - return ret ; + var hasObservers = get(this, 'hasArrayObservers'); + if (hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); + Ember.removeListener(this, '@array:before', target, willChange); + Ember.removeListener(this, '@array:change', target, didChange); + if (hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); + return this; }, /** - Shift an object from start of array or nil if none are left. Works just - like `shift()` but it is KVO-compliant. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.shiftObject(); // "red" - console.log(colors); // ["green", "blue"] - ``` + Becomes true whenever the array currently has observers watching changes + on the array. - @method shiftObject - @return object + @property {Boolean} hasArrayObservers */ - shiftObject: function() { - if (get(this, 'length') === 0) return null ; - var ret = this.objectAt(0) ; - this.removeAt(0) ; - return ret ; - }, + hasArrayObservers: Ember.computed(function() { + return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before'); + }), /** - Unshift an object to start of array. Works just like `unshift()` but it is - KVO-compliant. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.unshiftObject("yellow"); // ["yellow", "red", "green", "blue"] - colors.unshiftObject(["black", "white"]); // [["black", "white"], "yellow", "red", "green", "blue"] - ``` + If you are implementing an object that supports `Ember.Array`, call this + method just before the array content changes to notify any observers and + invalidate any related properties. Pass the starting index of the change + as well as a delta of the amounts to change. - @method unshiftObject - @param {*} obj object to unshift - @return {*} the same obj passed as param + @method arrayContentWillChange + @param {Number} startIdx The starting index in the array that will change. + @param {Number} removeAmt The number of items that will be removed. If you + pass `null` assumes 0 + @param {Number} addAmt The number of items that will be added. If you + pass `null` assumes 0. + @return {Ember.Array} receiver */ - unshiftObject: function(obj) { - this.insertAt(0, obj) ; - return obj ; - }, + arrayContentWillChange: function(startIdx, removeAmt, addAmt) { - /** - Adds the named objects to the beginning of the array. Defers notifying - observers until all objects have been added. + // if no args are passed assume everything changes + if (startIdx===undefined) { + startIdx = 0; + removeAmt = addAmt = -1; + } else { + if (removeAmt === undefined) removeAmt=-1; + if (addAmt === undefined) addAmt=-1; + } - ```javascript - var colors = ["red", "green", "blue"]; - colors.unshiftObjects(["black", "white"]); // ["black", "white", "red", "green", "blue"] - colors.unshiftObjects("yellow"); // Type Error: 'undefined' is not a function - ``` + // Make sure the @each proxy is set up if anyone is observing @each + if (Ember.isWatching(this, '@each')) { get(this, '@each'); } + + Ember.sendEvent(this, '@array:before', [this, startIdx, removeAmt, addAmt]); + + var removing, lim; + if (startIdx>=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) { + removing = []; + lim = startIdx+removeAmt; + for(var idx=startIdx;idx=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) { + adding = []; + lim = startIdx+addAmt; + for(var idx=startIdx;idx= 0) { - var curObject = this.objectAt(loc) ; - if (curObject === obj) this.removeAt(loc) ; - } - return this ; - }, + /** + Returns a special object that can be used to observe individual properties + on the array. Just get an equivalent property on this object and it will + return an enumerable that maps automatically to the named key on the + member objects. - addObject: function(obj) { - if (!this.contains(obj)) this.pushObject(obj); - return this ; - } + If you merely want to watch for any items being added or removed to the array, + use the `[]` property instead of `@each`. -}); + @property @each + */ + '@each': Ember.computed(function() { + if (!this.__each) this.__each = new Ember.EachProxy(this); + return this.__each; + }) + +}) ; })(); (function() { -/** -@module ember -@submodule ember-runtime +var e_get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + metaFor = Ember.meta, + propertyWillChange = Ember.propertyWillChange, + propertyDidChange = Ember.propertyDidChange, + addBeforeObserver = Ember.addBeforeObserver, + removeBeforeObserver = Ember.removeBeforeObserver, + addObserver = Ember.addObserver, + removeObserver = Ember.removeObserver, + ComputedProperty = Ember.ComputedProperty, + a_slice = [].slice, + o_create = Ember.create, + forEach = Ember.EnumerableUtils.forEach, + // Here we explicitly don't allow `@each.foo`; it would require some special + // testing, but there's no particular reason why it should be disallowed. + eachPropertyPattern = /^(.*)\.@each\.(.*)/, + doubleEachPropertyPattern = /(.*\.@each){2,}/, + arrayBracketPattern = /\.\[\]$/; + +var expandProperties = Ember.expandProperties; + +function get(obj, key) { + if (key === '@this') { + return obj; + } + + return e_get(obj, key); +} + +/* + Tracks changes to dependent arrays, as well as to properties of items in + dependent arrays. + + @class DependentArraysObserver */ +function DependentArraysObserver(callbacks, cp, instanceMeta, context, propertyName, sugarMeta) { + // user specified callbacks for `addedItem` and `removedItem` + this.callbacks = callbacks; -var get = Ember.get, - set = Ember.set, - slice = Array.prototype.slice, - getProperties = Ember.getProperties; + // the computed property: remember these are shared across instances + this.cp = cp; -/** - ## Overview + // the ReduceComputedPropertyInstanceMeta this DependentArraysObserver is + // associated with + this.instanceMeta = instanceMeta; - This mixin provides properties and property observing functionality, core - features of the Ember object model. + // A map of array guids to dependentKeys, for the given context. We track + // this because we want to set up the computed property potentially before the + // dependent array even exists, but when the array observer fires, we lack + // enough context to know what to update: we can recover that context by + // getting the dependentKey. + this.dependentKeysByGuid = {}; - Properties and observers allow one object to observe changes to a - property on another object. This is one of the fundamental ways that - models, controllers and views communicate with each other in an Ember - application. + // a map of dependent array guids -> Ember.TrackedArray instances. We use + // this to lazily recompute indexes for item property observers. + this.trackedArraysByGuid = {}; - Any object that has this mixin applied can be used in observer - operations. That includes `Ember.Object` and most objects you will - interact with as you write your Ember application. + // We suspend observers to ignore replacements from `reset` when totally + // recomputing. Unfortunately we cannot properly suspend the observers + // because we only have the key; instead we make the observers no-ops + this.suspended = false; - Note that you will not generally apply this mixin to classes yourself, - but you will use the features provided by this module frequently, so it - is important to understand how to use it. + // This is used to coalesce item changes from property observers. + this.changedItems = {}; +} - ## Using `get()` and `set()` +function ItemPropertyObserverContext (dependentArray, index, trackedArray) { + Ember.assert("Internal error: trackedArray is null or undefined", trackedArray); - Because of Ember's support for bindings and observers, you will always - access properties using the get method, and set properties using the - set method. This allows the observing objects to be notified and - computed properties to be handled properly. + this.dependentArray = dependentArray; + this.index = index; + this.item = dependentArray.objectAt(index); + this.trackedArray = trackedArray; + this.beforeObserver = null; + this.observer = null; - More documentation about `get` and `set` are below. + this.destroyed = false; +} - ## Observing Property Changes +DependentArraysObserver.prototype = { + setValue: function (newValue) { + this.instanceMeta.setValue(newValue, true); + }, + getValue: function () { + return this.instanceMeta.getValue(); + }, - You typically observe property changes simply by adding the `observes` - call to the end of your method declarations in classes that you write. - For example: + setupObservers: function (dependentArray, dependentKey) { + Ember.assert("dependent array must be an `Ember.Array`", Ember.Array.detect(dependentArray)); - ```javascript - Ember.Object.extend({ - valueObserver: function() { - // Executes whenever the "value" property changes - }.observes('value') - }); - ``` + this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; - Although this is the most common way to add an observer, this capability - is actually built into the `Ember.Object` class on top of two methods - defined in this mixin: `addObserver` and `removeObserver`. You can use - these two methods to add and remove observers yourself if you need to - do so at runtime. + dependentArray.addArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); - To add an observer for a property, call: + if (this.cp._itemPropertyKeys[dependentKey]) { + this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); + } + }, - ```javascript - object.addObserver('propertyKey', targetObject, targetAction) - ``` + teardownObservers: function (dependentArray, dependentKey) { + var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; - This will call the `targetAction` method on the `targetObject` whenever - the value of the `propertyKey` changes. + delete this.dependentKeysByGuid[guidFor(dependentArray)]; - Note that if `propertyKey` is a computed property, the observer will be - called when any of the property dependencies are changed, even if the - resulting value of the computed property is unchanged. This is necessary - because computed properties are not computed until `get` is called. + this.teardownPropertyObservers(dependentKey, itemPropertyKeys); - @class Observable - @namespace Ember -*/ -Ember.Observable = Ember.Mixin.create({ + dependentArray.removeArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); + }, - /** - Retrieves the value of a property from the object. + suspendArrayObservers: function (callback, binding) { + var oldSuspended = this.suspended; + this.suspended = true; + callback.call(binding); + this.suspended = oldSuspended; + }, - This method is usually similar to using `object[keyName]` or `object.keyName`, - however it supports both computed properties and the unknownProperty - handler. + setupPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArray = get(this.instanceMeta.context, dependentKey), + length = get(dependentArray, 'length'), + observerContexts = new Array(length); - Because `get` unifies the syntax for accessing all these kinds - of properties, it can make many refactorings easier, such as replacing a - simple property with a computed property, or vice versa. + this.resetTransformations(dependentKey, observerContexts); - ### Computed Properties + forEach(dependentArray, function (item, index) { + var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); + observerContexts[index] = observerContext; - Computed properties are methods defined with the `property` modifier - declared at the end, such as: + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + }, this); + }, - ```javascript - fullName: function() { - return this.getEach('firstName', 'lastName').compact().join(' '); - }.property('firstName', 'lastName') - ``` + teardownPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArrayObserver = this, + trackedArray = this.trackedArraysByGuid[dependentKey], + beforeObserver, + observer, + item; - When you call `get` on a computed property, the function will be - called and the return value will be returned instead of the function - itself. + if (!trackedArray) { return; } - ### Unknown Properties + trackedArray.apply(function (observerContexts, offset, operation) { + if (operation === Ember.TrackedArray.DELETE) { return; } - Likewise, if you try to call `get` on a property whose value is - `undefined`, the `unknownProperty()` method will be called on the object. - If this method returns any value other than `undefined`, it will be returned - instead. This allows you to implement "virtual" properties that are - not defined upfront. + forEach(observerContexts, function (observerContext) { + observerContext.destroyed = true; + beforeObserver = observerContext.beforeObserver; + observer = observerContext.observer; + item = observerContext.item; - @method get - @param {String} keyName The property to retrieve - @return {Object} The property value or undefined. - */ - get: function(keyName) { - return get(this, keyName); + forEach(itemPropertyKeys, function (propertyKey) { + removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); + removeObserver(item, propertyKey, dependentArrayObserver, observer); + }); + }); + }); }, - /** - To get multiple properties at once, call `getProperties` - with a list of strings or an array: + createPropertyObserverContext: function (dependentArray, index, trackedArray) { + var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); - ```javascript - record.getProperties('firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } - ``` + this.createPropertyObserver(observerContext); - is equivalent to: + return observerContext; + }, - ```javascript - record.getProperties(['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } - ``` + createPropertyObserver: function (observerContext) { + var dependentArrayObserver = this; - @method getProperties - @param {String...|Array} list of keys to get - @return {Hash} - */ - getProperties: function() { - return getProperties.apply(null, [this].concat(slice.call(arguments))); + observerContext.beforeObserver = function (obj, keyName) { + return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + observerContext.observer = function (obj, keyName) { + return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext); + }; }, - /** - Sets the provided key or path to the value. + resetTransformations: function (dependentKey, observerContexts) { + this.trackedArraysByGuid[dependentKey] = new Ember.TrackedArray(observerContexts); + }, - This method is generally very similar to calling `object[key] = value` or - `object.key = value`, except that it provides support for computed - properties, the `setUnknownProperty()` method and property observers. + trackAdd: function (dependentKey, index, newItems) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + if (trackedArray) { + trackedArray.addItems(index, newItems); + } + }, - ### Computed Properties + trackRemove: function (dependentKey, index, removedCount) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; - If you try to set a value on a key that has a computed property handler - defined (see the `get()` method for an example), then `set()` will call - that method, passing both the value and key instead of simply changing - the value itself. This is useful for those times when you need to - implement a property that is composed of one or more member - properties. + if (trackedArray) { + return trackedArray.removeItems(index, removedCount); + } - ### Unknown Properties + return []; + }, - If you try to set a value on a key that is undefined in the target - object, then the `setUnknownProperty()` handler will be called instead. This - gives you an opportunity to implement complex "virtual" properties that - are not predefined on the object. If `setUnknownProperty()` returns - undefined, then `set()` will simply set the value on the object. + updateIndexes: function (trackedArray, array) { + var length = get(array, 'length'); + // OPTIMIZE: we could stop updating once we hit the object whose observer + // fired; ie partially apply the transformations + trackedArray.apply(function (observerContexts, offset, operation) { + // we don't even have observer contexts for removed items, even if we did, + // they no longer have any index in the array + if (operation === Ember.TrackedArray.DELETE) { return; } + if (operation === Ember.TrackedArray.RETAIN && observerContexts.length === length && offset === 0) { + // If we update many items we don't want to walk the array each time: we + // only need to update the indexes at most once per run loop. + return; + } - ### Property Observers + forEach(observerContexts, function (context, index) { + context.index = index + offset; + }); + }); + }, - In addition to changing the property, `set()` will also register a property - change with the object. Unless you have placed this call inside of a - `beginPropertyChanges()` and `endPropertyChanges(),` any "local" observers - (i.e. observer methods declared on the same object), will be called - immediately. Any "remote" observers (i.e. observer methods declared on - another object) will be placed in a queue and called at a later time in a - coalesced manner. + dependentArrayWillChange: function (dependentArray, index, removedCount, addedCount) { + if (this.suspended) { return; } - ### Chaining + var removedItem = this.callbacks.removedItem, + changeMeta, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || [], + length = get(dependentArray, 'length'), + normalizedIndex = normalizeIndex(index, length, 0), + normalizedRemoveCount = normalizeRemoveCount(normalizedIndex, length, removedCount), + item, + itemIndex, + sliceIndex, + observerContexts; - In addition to property changes, `set()` returns the value of the object - itself so you can do chaining like this: + observerContexts = this.trackRemove(dependentKey, normalizedIndex, normalizedRemoveCount); - ```javascript - record.set('firstName', 'Charles').set('lastName', 'Jolley'); - ``` + function removeObservers(propertyKey) { + observerContexts[sliceIndex].destroyed = true; + removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); + removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); + } - @method set - @param {String} keyName The property to set - @param {Object} value The value to set or `null`. - @return {Ember.Observable} - */ - set: function(keyName, value) { - set(this, keyName, value); - return this; + for (sliceIndex = normalizedRemoveCount - 1; sliceIndex >= 0; --sliceIndex) { + itemIndex = normalizedIndex + sliceIndex; + if (itemIndex >= length) { break; } + + item = dependentArray.objectAt(itemIndex); + + forEach(itemPropertyKeys, removeObservers, this); + + changeMeta = createChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( removedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + } }, - /** - To set multiple properties at once, call `setProperties` - with a Hash: + dependentArrayDidChange: function (dependentArray, index, removedCount, addedCount) { + if (this.suspended) { return; } + + var addedItem = this.callbacks.addedItem, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + observerContexts = new Array(addedCount), + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey], + length = get(dependentArray, 'length'), + normalizedIndex = normalizeIndex(index, length, addedCount), + changeMeta, + observerContext; + + forEach(dependentArray.slice(normalizedIndex, normalizedIndex + addedCount), function (item, sliceIndex) { + if (itemPropertyKeys) { + observerContext = + observerContexts[sliceIndex] = + this.createPropertyObserverContext(dependentArray, normalizedIndex + sliceIndex, this.trackedArraysByGuid[dependentKey]); + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + } + + changeMeta = createChangeMeta(dependentArray, item, normalizedIndex + sliceIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( addedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + }, this); + + this.trackAdd(dependentKey, normalizedIndex, observerContexts); + }, + + itemPropertyWillChange: function (obj, keyName, array, observerContext) { + var guid = guidFor(obj); + + if (!this.changedItems[guid]) { + this.changedItems[guid] = { + array: array, + observerContext: observerContext, + obj: obj, + previousValues: {} + }; + } + + this.changedItems[guid].previousValues[keyName] = get(obj, keyName); + }, + + itemPropertyDidChange: function(obj, keyName, array, observerContext) { + this.flushChanges(); + }, + + flushChanges: function() { + var changedItems = this.changedItems, key, c, changeMeta; + + for (key in changedItems) { + c = changedItems[key]; + if (c.observerContext.destroyed) { continue; } + + this.updateIndexes(c.observerContext.trackedArray, c.observerContext.dependentArray); + + changeMeta = createChangeMeta(c.array, c.obj, c.observerContext.index, this.instanceMeta.propertyName, this.cp, c.previousValues); + this.setValue( + this.callbacks.removedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + this.setValue( + this.callbacks.addedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + } + this.changedItems = {}; + } +}; + +function normalizeIndex(index, length, newItemsOffset) { + if (index < 0) { + return Math.max(0, length + index); + } else if (index < length) { + return index; + } else /* index > length */ { + return Math.min(length - newItemsOffset, index); + } +} + +function normalizeRemoveCount(index, length, removedCount) { + return Math.min(removedCount, length - index); +} + +function createChangeMeta(dependentArray, item, index, propertyName, property, previousValues) { + var meta = { + arrayChanged: dependentArray, + index: index, + item: item, + propertyName: propertyName, + property: property + }; + + if (previousValues) { + // previous values only available for item property changes + meta.previousValues = previousValues; + } + + return meta; +} + +function addItems (dependentArray, callbacks, cp, propertyName, meta) { + forEach(dependentArray, function (item, index) { + meta.setValue( callbacks.addedItem.call( + this, meta.getValue(), item, createChangeMeta(dependentArray, item, index, propertyName, cp), meta.sugarMeta)); + }, this); +} + +function reset(cp, propertyName) { + var callbacks = cp._callbacks(), + meta; + + if (cp._hasInstanceMeta(this, propertyName)) { + meta = cp._instanceMeta(this, propertyName); + meta.setValue(cp.resetValue(meta.getValue())); + } else { + meta = cp._instanceMeta(this, propertyName); + } + + if (cp.options.initialize) { + cp.options.initialize.call(this, meta.getValue(), { property: cp, propertyName: propertyName }, meta.sugarMeta); + } +} + +function partiallyRecomputeFor(obj, dependentKey) { + if (arrayBracketPattern.test(dependentKey)) { + return false; + } + + var value = get(obj, dependentKey); + return Ember.Array.detect(value); +} + +function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { + this.context = context; + this.propertyName = propertyName; + this.cache = metaFor(context).cache; + + this.dependentArrays = {}; + this.sugarMeta = {}; + + this.initialValue = initialValue; +} + +ReduceComputedPropertyInstanceMeta.prototype = { + getValue: function () { + if (this.propertyName in this.cache) { + return this.cache[this.propertyName]; + } else { + return this.initialValue; + } + }, + + setValue: function(newValue, triggerObservers) { + // This lets sugars force a recomputation, handy for very simple + // implementations of eg max. + if (newValue === this.cache[this.propertyName]) { + return; + } + + if (triggerObservers) { + propertyWillChange(this.context, this.propertyName); + } + + if (newValue === undefined) { + delete this.cache[this.propertyName]; + } else { + this.cache[this.propertyName] = newValue; + } + + if (triggerObservers) { + propertyDidChange(this.context, this.propertyName); + } + } +}; + +/** + A computed property whose dependent keys are arrays and which is updated with + "one at a time" semantics. + + @class ReduceComputedProperty + @namespace Ember + @extends Ember.ComputedProperty + @constructor +*/ +function ReduceComputedProperty(options) { + var cp = this; + + this.options = options; + this._instanceMetas = {}; + + this._dependentKeys = null; + // A map of dependentKey -> [itemProperty, ...] that tracks what properties of + // items in the array we must track to update this property. + this._itemPropertyKeys = {}; + this._previousItemPropertyKeys = {}; + + this.readOnly(); + this.cacheable(); + + this.recomputeOnce = function(propertyName) { + // What we really want to do is coalesce by . + // We need a form of `scheduleOnce` that accepts an arbitrary token to + // coalesce by, in addition to the target and method. + Ember.run.once(this, recompute, propertyName); + }; + var recompute = function(propertyName) { + var dependentKeys = cp._dependentKeys, + meta = cp._instanceMeta(this, propertyName), + callbacks = cp._callbacks(); + + reset.call(this, cp, propertyName); + + meta.dependentArraysObserver.suspendArrayObservers(function () { + forEach(cp._dependentKeys, function (dependentKey) { + if (!partiallyRecomputeFor(this, dependentKey)) { return; } + + var dependentArray = get(this, dependentKey), + previousDependentArray = meta.dependentArrays[dependentKey]; + + if (dependentArray === previousDependentArray) { + // The array may be the same, but our item property keys may have + // changed, so we set them up again. We can't easily tell if they've + // changed: the array may be the same object, but with different + // contents. + if (cp._previousItemPropertyKeys[dependentKey]) { + delete cp._previousItemPropertyKeys[dependentKey]; + meta.dependentArraysObserver.setupPropertyObservers(dependentKey, cp._itemPropertyKeys[dependentKey]); + } + } else { + meta.dependentArrays[dependentKey] = dependentArray; + + if (previousDependentArray) { + meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); + } + + if (dependentArray) { + meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); + } + } + }, this); + }, this); + + forEach(cp._dependentKeys, function(dependentKey) { + if (!partiallyRecomputeFor(this, dependentKey)) { return; } + + var dependentArray = get(this, dependentKey); + if (dependentArray) { + addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); + } + }, this); + }; + + + this.func = function (propertyName) { + Ember.assert("Computed reduce values require at least one dependent key", cp._dependentKeys); + + recompute.call(this, propertyName); + + return cp._instanceMeta(this, propertyName).getValue(); + }; +} + +Ember.ReduceComputedProperty = ReduceComputedProperty; +ReduceComputedProperty.prototype = o_create(ComputedProperty.prototype); + +function defaultCallback(computedValue) { + return computedValue; +} + +ReduceComputedProperty.prototype._callbacks = function () { + if (!this.callbacks) { + var options = this.options; + this.callbacks = { + removedItem: options.removedItem || defaultCallback, + addedItem: options.addedItem || defaultCallback + }; + } + return this.callbacks; +}; + +ReduceComputedProperty.prototype._hasInstanceMeta = function (context, propertyName) { + var guid = guidFor(context), + key = guid + ':' + propertyName; + + return !!this._instanceMetas[key]; +}; + +ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { + var guid = guidFor(context), + key = guid + ':' + propertyName, + meta = this._instanceMetas[key]; + + if (!meta) { + meta = this._instanceMetas[key] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); + meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); + } + + return meta; +}; + +ReduceComputedProperty.prototype.initialValue = function () { + if (typeof this.options.initialValue === 'function') { + return this.options.initialValue(); + } + else { + return this.options.initialValue; + } +}; + +ReduceComputedProperty.prototype.resetValue = function (value) { + return this.initialValue(); +}; + +ReduceComputedProperty.prototype.itemPropertyKey = function (dependentArrayKey, itemPropertyKey) { + this._itemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey] || []; + this._itemPropertyKeys[dependentArrayKey].push(itemPropertyKey); +}; + +ReduceComputedProperty.prototype.clearItemPropertyKeys = function (dependentArrayKey) { + if (this._itemPropertyKeys[dependentArrayKey]) { + this._previousItemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey]; + this._itemPropertyKeys[dependentArrayKey] = []; + } +}; + +ReduceComputedProperty.prototype.property = function () { + var cp = this, + args = a_slice.call(arguments), + propertyArgs = new Ember.Set(), + match, + dependentArrayKey, + itemPropertyKey; + + forEach(a_slice.call(arguments), function (dependentKey) { + if (doubleEachPropertyPattern.test(dependentKey)) { + throw new Ember.Error("Nested @each properties not supported: " + dependentKey); + } else if (match = eachPropertyPattern.exec(dependentKey)) { + dependentArrayKey = match[1]; + + var itemPropertyKeyPattern = match[2], + addItemPropertyKey = function (itemPropertyKey) { + cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); + }; + + expandProperties(itemPropertyKeyPattern, addItemPropertyKey); + propertyArgs.add(dependentArrayKey); + } else { + propertyArgs.add(dependentKey); + } + }); + + return ComputedProperty.prototype.property.apply(this, propertyArgs.toArray()); + +}; + +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) a reduce computed only operates + on the change instead of re-evaluating the entire array. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following four properties: + + `initialValue` - A value or function that will be used as the initial + value for the computed. If this property is a function the result of calling + the function will be used as the initial value. This property is required. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is removed + from the array. + + `addedItem` - A function that is called each time an element is added to + the array. + + + The `initialize` function has the following signature: + + ```javascript + function (initialValue, changeMeta, instanceMeta) + ``` + + `initialValue` - The value of the `initialValue` property from the + options object. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or `initialValue`. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Note that observers will be fired if either of these functions return a value + that differs from the accumulated value. When returning an object that + mutates in response to array changes, for example an array that maps + everything from some other array (see `Ember.computed.map`), it is usually + important that the *same* array be returned to avoid accidentally triggering observers. + + Example + + ```javascript + Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed(dependentKey, { + initialValue: -Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); + }; + ``` + + Dependent keys may refer to `@this` to observe changes to the object itself, + which must be array-like, rather than a property of the object. This is + mostly useful for array proxies, to ensure objects are retrieved via + `objectAtContent`. This is how you could sort items by properties defined on an item controller. + + Example + + ```javascript + App.PeopleController = Ember.ArrayController.extend({ + itemController: 'person', + + sortedPeople: Ember.computed.sort('@this.@each.reversedName', function(personA, personB) { + // `reversedName` isn't defined on Person, but we have access to it via + // the item controller App.PersonController. If we'd used + // `content.@each.reversedName` above, we would be getting the objects + // directly and not have access to `reversedName`. + // + var reversedNameA = get(personA, 'reversedName'), + reversedNameB = get(personB, 'reversedName'); + + return Ember.compare(reversedNameA, reversedNameB); + }) + }); + + App.PersonController = Ember.ObjectController.extend({ + reversedName: function () { + return reverse(get(this, 'name')); + }.property('name') + }) + ``` + + Dependent keys whose values are not arrays are treated as regular + dependencies: when they change, the computed property is completely + recalculated. It is sometimes useful to have dependent arrays with similar + semantics. Dependent keys which end in `.[]` do not use "one at a time" + semantics. When an item is added or removed from such a dependency, the + computed property is completely recomputed. + + Example + + ```javascript + Ember.Object.extend({ + // When `string` is changed, `computed` is completely recomputed. + string: 'a string', + + // When an item is added to `array`, `addedItem` is called. + array: [], + + // When an item is added to `anotherArray`, `computed` is completely + // recomputed. + anotherArray: [], + + computed: Ember.reduceComputed('string', 'array', 'anotherArray.[]', { + addedItem: addedItemCallback, + removedItem: removedItemCallback + }) + }); + ``` + + @method reduceComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.reduceComputed = function (options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new Ember.Error("Reduce Computed Property declared without an options hash"); + } + + if (!('initialValue' in options)) { + throw new Ember.Error("Reduce Computed Property declared without an initial value"); + } + + var cp = new ReduceComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; +}; + +})(); + + + +(function() { +var ReduceComputedProperty = Ember.ReduceComputedProperty, + a_slice = [].slice, + o_create = Ember.create, + forEach = Ember.EnumerableUtils.forEach; + +function ArrayComputedProperty() { + var cp = this; + + ReduceComputedProperty.apply(this, arguments); + + this.func = (function(reduceFunc) { + return function (propertyName) { + if (!cp._hasInstanceMeta(this, propertyName)) { + // When we recompute an array computed property, we need already + // retrieved arrays to be updated; we can't simply empty the cache and + // hope the array is re-retrieved. + forEach(cp._dependentKeys, function(dependentKey) { + Ember.addObserver(this, dependentKey, function() { + cp.recomputeOnce.call(this, propertyName); + }); + }, this); + } + + return reduceFunc.apply(this, arguments); + }; + })(this.func); + + return this; +} +Ember.ArrayComputedProperty = ArrayComputedProperty; +ArrayComputedProperty.prototype = o_create(ReduceComputedProperty.prototype); +ArrayComputedProperty.prototype.initialValue = function () { + return Ember.A(); +}; +ArrayComputedProperty.prototype.resetValue = function (array) { + array.clear(); + return array; +}; + +// This is a stopgap to keep the reference counts correct with lazy CPs. +ArrayComputedProperty.prototype.didChange = function (obj, keyName) { + return; +}; + +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) an array computed only operates + on the change instead of re-evaluating the entire array. This should + return an array, if you'd like to use "one at a time" semantics and + compute some value other then an array look at + `Ember.reduceComputed`. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following three properties. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is + removed from the array. + + `addedItem` - A function that is called each time an element is + added to the array. + + + The `initialize` function has the following signature: + + ```javascript + function (array, changeMeta, instanceMeta) + ``` + + `array` - The initial value of the arrayComputed, an empty array. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or an empty array. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Example + + ```javascript + Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback(item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); + }; + ``` + + @method arrayComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.arrayComputed = function (options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new Ember.Error("Array Computed Property declared without an options hash"); + } + + var cp = new ArrayComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; +}; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + merge = Ember.merge, + a_slice = [].slice, + forEach = Ember.EnumerableUtils.forEach, + map = Ember.EnumerableUtils.map, + SearchProxy; + +/** + A computed property that returns the sum of the value + in the dependent array. + + @method computed.sum + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the sum of all values in the dependentKey's array +*/ + +Ember.computed.sum = function(dependentKey){ + return Ember.reduceComputed(dependentKey, { + initialValue: 0, + + addedItem: function(accumulatedValue, item, changeMeta, instanceMeta){ + return accumulatedValue + item; + }, + + removedItem: function(accumulatedValue, item, changeMeta, instanceMeta){ + return accumulatedValue - item; + } + }); +}; + +/** + A computed property that calculates the maximum value in the + dependent array. This will return `-Infinity` when the dependent + array is empty. + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + maxChildAge: Ember.computed.max('childAges') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('maxChildAge'); // -Infinity + lordByron.get('children').pushObject({ + name: 'Augusta Ada Byron', age: 7 + }); + lordByron.get('maxChildAge'); // 7 + lordByron.get('children').pushObjects([{ + name: 'Allegra Byron', + age: 5 + }, { + name: 'Elizabeth Medora Leigh', + age: 8 + }]); + lordByron.get('maxChildAge'); // 8 + ``` + + @method computed.max + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the largest value in the dependentKey's array +*/ +Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed(dependentKey, { + initialValue: -Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); +}; + +/** + A computed property that calculates the minimum value in the + dependent array. This will return `Infinity` when the dependent + array is empty. + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + minChildAge: Ember.computed.min('childAges') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('minChildAge'); // Infinity + lordByron.get('children').pushObject({ + name: 'Augusta Ada Byron', age: 7 + }); + lordByron.get('minChildAge'); // 7 + lordByron.get('children').pushObjects([{ + name: 'Allegra Byron', + age: 5 + }, { + name: 'Elizabeth Medora Leigh', + age: 8 + }]); + lordByron.get('minChildAge'); // 5 + ``` + + @method computed.min + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array +*/ +Ember.computed.min = function (dependentKey) { + return Ember.reduceComputed(dependentKey, { + initialValue: Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.min(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item > accumulatedValue) { + return accumulatedValue; + } + } + }); +}; + +/** + Returns an array mapped via the callback + + The callback method you provide should have the following signature. + `item` is the current item in the iteration. + + ```javascript + function(item); + ``` + + Example + + ```javascript + App.Hamster = Ember.Object.extend({ + excitingChores: Ember.computed.map('chores', function(chore) { + return chore.toUpperCase() + '!'; + }) + }); + + var hamster = App.Hamster.create({ + chores: ['clean', 'write more unit tests'] + }); + hamster.get('excitingChores'); // ['CLEAN!', 'WRITE MORE UNIT TESTS!'] + ``` + + @method computed.map + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} an array mapped via the callback +*/ +Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback.call(this, item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); +}; + +/** + Returns an array mapped to the specified key. + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('childAges'); // [] + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('childAges'); // [7] + lordByron.get('children').pushObjects([{ + name: 'Allegra Byron', + age: 5 + }, { + name: 'Elizabeth Medora Leigh', + age: 8 + }]); + lordByron.get('childAges'); // [7, 5, 8] + ``` + + @method computed.mapBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @return {Ember.ComputedProperty} an array mapped to the specified key +*/ +Ember.computed.mapBy = function(dependentKey, propertyKey) { + var callback = function(item) { return get(item, propertyKey); }; + return Ember.computed.map(dependentKey + '.@each.' + propertyKey, callback); +}; + +/** + @method computed.mapProperty + @for Ember + @deprecated Use `Ember.computed.mapBy` instead + @param dependentKey + @param propertyKey +*/ +Ember.computed.mapProperty = Ember.computed.mapBy; + +/** + Filters the array by the callback. + + The callback method you provide should have the following signature. + `item` is the current item in the iteration. + + ```javascript + function(item); + ``` + + ```javascript + App.Hamster = Ember.Object.extend({ + remainingChores: Ember.computed.filter('chores', function(chore) { + return !chore.done; + }) + }); + + var hamster = App.Hamster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hamster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filter + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filter = function(dependentKey, callback) { + var options = { + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.filteredArrayIndexes = new Ember.SubArray(); + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var match = !!callback.call(this, item), + filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); + + if (match) { + array.insertAt(filterIndex, item); + } + + return array; + }, + + removedItem: function(array, item, changeMeta, instanceMeta) { + var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); + + if (filterIndex > -1) { + array.removeAt(filterIndex); + } + + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); +}; + +/** + Filters the array by the property and value + + ```javascript + App.Hamster = Ember.Object.extend({ + remainingChores: Ember.computed.filterBy('chores', 'done', false) + }); + + var hamster = App.Hamster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hamster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filterBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @param {String} value + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filterBy = function(dependentKey, propertyKey, value) { + var callback; + + if (arguments.length === 2) { + callback = function(item) { + return get(item, propertyKey); + }; + } else { + callback = function(item) { + return get(item, propertyKey) === value; + }; + } + + return Ember.computed.filter(dependentKey + '.@each.' + propertyKey, callback); +}; + +/** + @method computed.filterProperty + @for Ember + @param dependentKey + @param propertyKey + @param value + @deprecated Use `Ember.computed.filterBy` instead +*/ +Ember.computed.filterProperty = Ember.computed.filterBy; + +/** + A computed property which returns a new array with all the unique + elements from one or more dependent arrays. + + Example + + ```javascript + App.Hamster = Ember.Object.extend({ + uniqueFruits: Ember.computed.uniq('fruits') + }); + + var hamster = App.Hamster.create({fruits: [ + 'banana', + 'grape', + 'kale', + 'banana' + ]}); + hamster.get('uniqueFruits'); // ['banana', 'grape', 'kale'] + ``` + + @method computed.uniq + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.uniq = function() { + var args = a_slice.call(arguments); + args.push({ + initialize: function(array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var guid = guidFor(item); + + if (!instanceMeta.itemCounts[guid]) { + instanceMeta.itemCounts[guid] = 1; + } else { + ++instanceMeta.itemCounts[guid]; + } + array.addObject(item); + return array; + }, + removedItem: function(array, item, _, instanceMeta) { + var guid = guidFor(item), + itemCounts = instanceMeta.itemCounts; + + if (--itemCounts[guid] === 0) { + array.removeObject(item); + } + return array; + } + }); + return Ember.arrayComputed.apply(null, args); +}; + +/** + Alias for [Ember.computed.uniq](/api/#method_computed_uniq). + + @method computed.union + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.union = Ember.computed.uniq; + +/** + A computed property which returns a new array with all the duplicated + elements from two or more dependent arrays. + + Example + + ```javascript + var obj = Ember.Object.createWithMixins({ + adaFriends: ['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'], + charlesFriends: ['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock'], + friendsInCommon: Ember.computed.intersect('adaFriends', 'charlesFriends') + }); + + obj.get('friendsInCommon'); // ['William King', 'Mary Somerville'] + ``` + + @method computed.intersect + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + duplicated elements from the dependent arrays +*/ +Ember.computed.intersect = function () { + var getDependentKeyGuids = function (changeMeta) { + return map(changeMeta.property._dependentKeys, function (dependentKey) { + return guidFor(dependentKey); + }); + }; + + var args = a_slice.call(arguments); + args.push({ + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + itemCounts = instanceMeta.itemCounts; + + if (!itemCounts[itemGuid]) { itemCounts[itemGuid] = {}; } + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + + if (++itemCounts[itemGuid][dependentGuid] === 1 && + numberOfDependentArrays === Ember.keys(itemCounts[itemGuid]).length) { + + array.addObject(item); + } + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + numberOfArraysItemAppearsIn, + itemCounts = instanceMeta.itemCounts; + + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + if (--itemCounts[itemGuid][dependentGuid] === 0) { + delete itemCounts[itemGuid][dependentGuid]; + numberOfArraysItemAppearsIn = Ember.keys(itemCounts[itemGuid]).length; + + if (numberOfArraysItemAppearsIn === 0) { + delete itemCounts[itemGuid]; + } + array.removeObject(item); + } + return array; + } + }); + return Ember.arrayComputed.apply(null, args); +}; + +/** + A computed property which returns a new array with all the + properties from the first dependent array that are not in the second + dependent array. + + Example + + ```javascript + App.Hamster = Ember.Object.extend({ + likes: ['banana', 'grape', 'kale'], + wants: Ember.computed.setDiff('likes', 'fruits') + }); + + var hamster = App.Hamster.create({fruits: [ + 'grape', + 'kale', + ]}); + hamster.get('wants'); // ['banana'] + ``` + + @method computed.setDiff + @for Ember + @param {String} setAProperty + @param {String} setBProperty + @return {Ember.ComputedProperty} computes a new array with all the + items from the first dependent array that are not in the second + dependent array +*/ +Ember.computed.setDiff = function (setAProperty, setBProperty) { + if (arguments.length !== 2) { + throw new Ember.Error("setDiff requires exactly two dependent arrays."); + } + return Ember.arrayComputed(setAProperty, setBProperty, { + addedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setA) { + if (!setB.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + }, + + removedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setB) { + if (setA.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + } + }); +}; + +function binarySearch(array, item, low, high) { + var mid, midItem, res, guidMid, guidItem; + + if (arguments.length < 4) { high = get(array, 'length'); } + if (arguments.length < 3) { low = 0; } + + if (low === high) { + return low; + } + + mid = low + Math.floor((high - low) / 2); + midItem = array.objectAt(mid); + + guidMid = _guidFor(midItem); + guidItem = _guidFor(item); + + if (guidMid === guidItem) { + return mid; + } + + res = this.order(midItem, item); + if (res === 0) { + res = guidMid < guidItem ? -1 : 1; + } + + + if (res < 0) { + return this.binarySearch(array, item, mid+1, high); + } else if (res > 0) { + return this.binarySearch(array, item, low, mid); + } + + return mid; + + function _guidFor(item) { + if (SearchProxy.detectInstance(item)) { + return guidFor(get(item, 'content')); + } + return guidFor(item); + } +} + + +SearchProxy = Ember.ObjectProxy.extend(); + +/** + A computed property which returns a new array with all the + properties from the first dependent array sorted based on a property + or sort function. + + The callback method you provide should have the following signature: + + ```javascript + function(itemA, itemB); + ``` + + - `itemA` the first item to compare. + - `itemB` the second item to compare. + + This function should return `-1` when `itemA` should come before + `itemB`. It should return `1` when `itemA` should come after + `itemB`. If the `itemA` and `itemB` are equal this function should return `0`. + + Example + + ```javascript + var ToDoList = Ember.Object.extend({ + todosSorting: ['name'], + sortedTodos: Ember.computed.sort('todos', 'todosSorting'), + priorityTodos: Ember.computed.sort('todos', function(a, b){ + if (a.priority > b.priority) { + return 1; + } else if (a.priority < b.priority) { + return -1; + } + return 0; + }), + }); + var todoList = ToDoList.create({todos: [ + {name: 'Unit Test', priority: 2}, + {name: 'Documentation', priority: 3}, + {name: 'Release', priority: 1} + ]}); + + todoList.get('sortedTodos'); // [{name:'Documentation', priority:3}, {name:'Release', priority:1}, {name:'Unit Test', priority:2}] + todoList.get('priorityTodos'); // [{name:'Release', priority:1}, {name:'Unit Test', priority:2}, {name:'Documentation', priority:3}] + ``` + + @method computed.sort + @for Ember + @param {String} dependentKey + @param {String or Function} sortDefinition a dependent key to an + array of sort properties or a function to use when sorting + @return {Ember.ComputedProperty} computes a new sorted array based + on the sort property array or callback function +*/ +Ember.computed.sort = function (itemsKey, sortDefinition) { + Ember.assert("Ember.computed.sort requires two arguments: an array key to sort and either a sort properties key or sort function", arguments.length === 2); + + var initFn, sortPropertiesKey; + + if (typeof sortDefinition === 'function') { + initFn = function (array, changeMeta, instanceMeta) { + instanceMeta.order = sortDefinition; + instanceMeta.binarySearch = binarySearch; + }; + } else { + sortPropertiesKey = sortDefinition; + initFn = function (array, changeMeta, instanceMeta) { + function setupSortProperties() { + var sortPropertyDefinitions = get(this, sortPropertiesKey), + sortProperty, + sortProperties = instanceMeta.sortProperties = [], + sortPropertyAscending = instanceMeta.sortPropertyAscending = {}, + idx, + asc; + + Ember.assert("Cannot sort: '" + sortPropertiesKey + "' is not an array.", Ember.isArray(sortPropertyDefinitions)); + + changeMeta.property.clearItemPropertyKeys(itemsKey); + + forEach(sortPropertyDefinitions, function (sortPropertyDefinition) { + if ((idx = sortPropertyDefinition.indexOf(':')) !== -1) { + sortProperty = sortPropertyDefinition.substring(0, idx); + asc = sortPropertyDefinition.substring(idx+1).toLowerCase() !== 'desc'; + } else { + sortProperty = sortPropertyDefinition; + asc = true; + } + + sortProperties.push(sortProperty); + sortPropertyAscending[sortProperty] = asc; + changeMeta.property.itemPropertyKey(itemsKey, sortProperty); + }); + + sortPropertyDefinitions.addObserver('@each', this, updateSortPropertiesOnce); + } + + function updateSortPropertiesOnce() { + Ember.run.once(this, updateSortProperties, changeMeta.propertyName); + } + + function updateSortProperties(propertyName) { + setupSortProperties.call(this); + changeMeta.property.recomputeOnce.call(this, propertyName); + } + + Ember.addObserver(this, sortPropertiesKey, updateSortPropertiesOnce); + + setupSortProperties.call(this); + + + instanceMeta.order = function (itemA, itemB) { + var sortProperty, result, asc; + for (var i = 0; i < this.sortProperties.length; ++i) { + sortProperty = this.sortProperties[i]; + result = Ember.compare(get(itemA, sortProperty), get(itemB, sortProperty)); + + if (result !== 0) { + asc = this.sortPropertyAscending[sortProperty]; + return asc ? result : (-1 * result); + } + } + + return 0; + }; - ```javascript - record.setProperties({ firstName: 'Charles', lastName: 'Jolley' }); - ``` + instanceMeta.binarySearch = binarySearch; + }; + } - @method setProperties - @param {Hash} hash the hash of keys and values to set - @return {Ember.Observable} - */ - setProperties: function(hash) { - return Ember.setProperties(this, hash); - }, + return Ember.arrayComputed(itemsKey, { + initialize: initFn, - /** - Begins a grouping of property changes. + addedItem: function (array, item, changeMeta, instanceMeta) { + var index = instanceMeta.binarySearch(array, item); + array.insertAt(index, item); + return array; + }, - You can use this method to group property changes so that notifications - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call this - method at the beginning of the changes to begin deferring change - notifications. When you are done making changes, call - `endPropertyChanges()` to deliver the deferred change notifications and end - deferring. + removedItem: function (array, item, changeMeta, instanceMeta) { + var proxyProperties, index, searchItem; - @method beginPropertyChanges - @return {Ember.Observable} - */ - beginPropertyChanges: function() { - Ember.beginPropertyChanges(); - return this; - }, + if (changeMeta.previousValues) { + proxyProperties = merge({ content: item }, changeMeta.previousValues); - /** - Ends a grouping of property changes. + searchItem = SearchProxy.create(proxyProperties); + } else { + searchItem = item; + } - You can use this method to group property changes so that notifications - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call - `beginPropertyChanges()` at the beginning of the changes to defer change - notifications. When you are done making changes, call this method to - deliver the deferred change notifications and end deferring. + index = instanceMeta.binarySearch(array, searchItem); + array.removeAt(index); + return array; + } + }); +}; - @method endPropertyChanges - @return {Ember.Observable} - */ - endPropertyChanges: function() { - Ember.endPropertyChanges(); - return this; - }, +})(); - /** - Notify the observer system that a property is about to change. - Sometimes you need to change a value directly or indirectly without - actually calling `get()` or `set()` on it. In this case, you can use this - method and `propertyDidChange()` instead. Calling these two methods - together will notify all observers that the property has potentially - changed value. - Note that you must always call `propertyWillChange` and `propertyDidChange` - as a pair. If you do not, it may get the property change groups out of - order and cause notifications to be delivered more often than you would - like. +(function() { +Ember.RSVP = requireModule('rsvp'); - @method propertyWillChange - @param {String} keyName The property key that is about to change. - @return {Ember.Observable} - */ - propertyWillChange: function(keyName) { - Ember.propertyWillChange(this, keyName); - return this; - }, +Ember.RSVP.onerrorDefault = function(error) { + if (error instanceof Error) { + if (Ember.testing) { + if (Ember.Test && Ember.Test.adapter) { + Ember.Test.adapter.exception(error); + } else { + throw error; + } + } else { + Ember.Logger.error(error.stack); + Ember.assert(error, false); + } + } +}; - /** - Notify the observer system that a property has just changed. +Ember.RSVP.on('error', Ember.RSVP.onerrorDefault); - Sometimes you need to change a value directly or indirectly without - actually calling `get()` or `set()` on it. In this case, you can use this - method and `propertyWillChange()` instead. Calling these two methods - together will notify all observers that the property has potentially - changed value. +})(); - Note that you must always call `propertyWillChange` and `propertyDidChange` - as a pair. If you do not, it may get the property change groups out of - order and cause notifications to be delivered more often than you would - like. - @method propertyDidChange - @param {String} keyName The property key that has just changed. - @return {Ember.Observable} - */ - propertyDidChange: function(keyName) { - Ember.propertyDidChange(this, keyName); - return this; - }, - /** - Convenience method to call `propertyWillChange` and `propertyDidChange` in - succession. +(function() { +/** +@module ember +@submodule ember-runtime +*/ - @method notifyPropertyChange - @param {String} keyName The property key to be notified about. - @return {Ember.Observable} - */ - notifyPropertyChange: function(keyName) { - this.propertyWillChange(keyName); - this.propertyDidChange(keyName); - return this; - }, +var a_slice = Array.prototype.slice; - addBeforeObserver: function(key, target, method) { - Ember.addBeforeObserver(this, key, target, method); - }, +var expandProperties = Ember.expandProperties; + +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { /** - Adds an observer on a property. + The `property` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + `true`, which is the default. - This is the core method used to register an observer for a property. + Computed properties allow you to treat a function like a property: - Once you call this method, any time the key's value is set, your observer - will be notified. Note that the observers are triggered any time the - value is set, regardless of whether it has actually changed. Your - observer should be prepared to handle that. + ```javascript + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', - You can also pass an optional context parameter to this method. The - context will be passed to your observer method whenever it is triggered. - Note that if you add the same target/method pair on a key multiple times - with different context parameters, your observer will only be called once - with the last context you passed. + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); - ### Observer Methods + // Call this flag to mark the function as a property + }.property() + }); - Observer methods you pass should generally have the following signature if - you do not pass a `context` parameter: + var president = MyApp.President.create({ + firstName: "Barack", + lastName: "Obama" + }); - ```javascript - fooDidChange: function(sender, key, value, rev) { }; + president.get('fullName'); // "Barack Obama" ``` - The sender is the object that changed. The key is the property that - changes. The value property is currently reserved and unused. The rev - is the last property revision of the object when it changed, which you can - use to detect if the key value has really changed or not. + Treating a function like a property is useful because they can work with + bindings, just like any other property. - If you pass a `context` parameter, the context will be passed before the - revision like so: + Many computed properties have dependencies on other properties. For + example, in the above example, the `fullName` property depends on + `firstName` and `lastName` to determine its value. You can tell Ember + about these dependencies like this: ```javascript - fooDidChange: function(sender, key, value, context, rev) { }; - ``` - - Usually you will not need the value, context or revision parameters at - the end. In this case, it is common to write observer methods that take - only a sender and key value as parameters or, if you aren't interested in - any of these values, to write an observer that has no parameters at all. - - @method addObserver - @param {String} key The key to observer - @param {Object} target The target object to invoke - @param {String|Function} method The method to invoke. - @return {Ember.Object} self - */ - addObserver: function(key, target, method) { - Ember.addObserver(this, key, target, method); - }, + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', - /** - Remove an observer you have previously registered on this object. Pass - the same key, target, and method you passed to `addObserver()` and your - target will no longer receive notifications. + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); - @method removeObserver - @param {String} key The key to observer - @param {Object} target The target object to invoke - @param {String|Function} method The method to invoke. - @return {Ember.Observable} receiver - */ - removeObserver: function(key, target, method) { - Ember.removeObserver(this, key, target, method); - }, + // Tell Ember.js that this computed property depends on firstName + // and lastName + }.property('firstName', 'lastName') + }); + ``` - /** - Returns `true` if the object currently has observers registered for a - particular key. You can use this method to potentially defer performing - an expensive action until someone begins observing a particular property - on the object. + Make sure you list these dependencies so Ember knows when to update + bindings that connect to a computed property. Changing a dependency + will not immediately trigger an update of the computed property, but + will instead clear the cache so that it is updated when the next `get` + is called on the property. - @method hasObserverFor - @param {String} key Key to check - @return {Boolean} - */ - hasObserverFor: function(key) { - return Ember.hasListeners(this, key+':change'); - }, + See [Ember.ComputedProperty](/api/classes/Ember.ComputedProperty.html), [Ember.computed](/api/#method_computed). - /** - @deprecated - @method getPath - @param {String} path The property path to retrieve - @return {Object} The property value or undefined. + @method property + @for Function */ - getPath: function(path) { - Ember.deprecate("getPath is deprecated since get now supports paths"); - return this.get(path); - }, + Function.prototype.property = function() { + var ret = Ember.computed(this); + // ComputedProperty.prototype.property expands properties; no need for us to + // do so here. + return ret.property.apply(ret, arguments); + }; /** - @deprecated - @method setPath - @param {String} path The path to the property that will be set - @param {Object} value The value to set or `null`. - @return {Ember.Observable} - */ - setPath: function(path, value) { - Ember.deprecate("setPath is deprecated since set now supports paths"); - return this.set(path, value); - }, + The `observes` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + true, which is the default. - /** - Retrieves the value of a property, or a default value in the case that the - property returns `undefined`. + You can observe property changes simply by adding the `observes` + call to the end of your method declarations in classes that you write. + For example: ```javascript - person.getWithDefault('lastName', 'Doe'); + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); ``` - @method getWithDefault - @param {String} keyName The name of the property to retrieve - @param {Object} defaultValue The value to return if the property value is undefined - @return {Object} The property value or the defaultValue. - */ - getWithDefault: function(keyName, defaultValue) { - return Ember.getWithDefault(this, keyName, defaultValue); - }, - - /** - Set the value of a property to the current value plus some amount. + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `observesImmediately`. - ```javascript - person.incrementProperty('age'); - team.incrementProperty('score', 2); - ``` + See `Ember.observer`. - @method incrementProperty - @param {String} keyName The name of the property to increment - @param {Number} increment The amount to increment by. Defaults to 1 - @return {Number} The new property value + @method observes + @for Function */ - incrementProperty: function(keyName, increment) { - if (Ember.isNone(increment)) { increment = 1; } - Ember.assert("Must pass a numeric value to incrementProperty", (!isNaN(parseFloat(increment)) && isFinite(increment))); - set(this, keyName, (get(this, keyName) || 0) + increment); - return get(this, keyName); - }, + Function.prototype.observes = function() { + var addWatchedProperty = function (obs) { watched.push(obs); }; + var watched = []; - /** - Set the value of a property to the current value minus some amount. + for (var i=0; i b` - You can also chain multiple event subscriptions: + Default implementation raises an exception. - ```javascript - person.on('greet', function() { - console.log('Our person has greeted'); - }).one('greet', function() { - console.log('Offer one-time special'); - }).off('event', this, forgetThis); - ``` + @method compare + @param a {Object} the first object to compare + @param b {Object} the second object to compare + @return {Integer} the result of the comparison + */ + compare: Ember.required(Function) - @class Evented - @namespace Ember - */ -Ember.Evented = Ember.Mixin.create({ +}); - /** - Subscribes to a named event with given function. - ```javascript - person.on('didLoad', function() { - // fired once the person has loaded - }); - ``` +})(); - An optional target can be passed in as the 2nd argument that will - be set as the "this" for the callback. This is a good way to give your - function access to the object triggering the event. When the target - parameter is used the callback becomes the third argument. - @method on - @param {String} name The name of the event - @param {Object} [target] The "this" binding for the callback - @param {Function} method The callback to execute - @return this - */ - on: function(name, target, method) { - Ember.addListener(this, name, target, method); - return this; - }, - /** - Subscribes a function to a named event and then cancels the subscription - after the first time the event is triggered. It is good to use ``one`` when - you only care about the first time an event has taken place. +(function() { +/** +@module ember +@submodule ember-runtime +*/ - This function takes an optional 2nd argument that will become the "this" - value for the callback. If this argument is passed then the 3rd argument - becomes the function. - @method one - @param {String} name The name of the event - @param {Object} [target] The "this" binding for the callback - @param {Function} method The callback to execute - @return this - */ - one: function(name, target, method) { - if (!method) { - method = target; - target = null; - } - Ember.addListener(this, name, target, method, true); - return this; - }, +var get = Ember.get, set = Ember.set; - /** - Triggers a named event for the object. Any additional arguments - will be passed as parameters to the functions that are subscribed to the - event. +/** + Implements some standard methods for copying an object. Add this mixin to + any object you create that can create a copy of itself. This mixin is + added automatically to the built-in array. - ```javascript - person.on('didEat', function(food) { - console.log('person ate some ' + food); - }); + You should generally implement the `copy()` method to return a copy of the + receiver. - person.trigger('didEat', 'broccoli'); + Note that `frozenCopy()` will only work if you also implement + `Ember.Freezable`. - // outputs: person ate some broccoli - ``` - @method trigger - @param {String} name The name of the event - @param {Object...} args Optional arguments to pass on - */ - trigger: function(name) { - var args = [], i, l; - for (i = 1, l = arguments.length; i < l; i++) { - args.push(arguments[i]); - } - Ember.sendEvent(this, name, args); - }, + @class Copyable + @namespace Ember + @since Ember 0.9 +*/ +Ember.Copyable = Ember.Mixin.create({ /** - Cancels subscription for given name, target, and method. + Override to return a copy of the receiver. Default implementation raises + an exception. - @method off - @param {String} name The name of the event - @param {Object} target The target of the subscription - @param {Function} method The function of the subscription - @return this + @method copy + @param {Boolean} deep if `true`, a deep copy of the object should be made + @return {Object} copy of receiver */ - off: function(name, target, method) { - Ember.removeListener(this, name, target, method); - return this; - }, + copy: Ember.required(Function), /** - Checks to see if object has any subscriptions for named event. + If the object implements `Ember.Freezable`, then this will return a new + copy if the object is not frozen and the receiver if the object is frozen. - @method has - @param {String} name The name of the event - @return {Boolean} does the object have a subscription for event - */ - has: function(name) { - return Ember.hasListeners(this, name); + Raises an exception if you try to call this method on a object that does + not support freezing. + + You should use this method whenever you want a copy of a freezable object + since a freezable object can simply return itself without actually + consuming more memory. + + @method frozenCopy + @return {Object} copy of receiver or receiver + */ + frozenCopy: function() { + if (Ember.Freezable && Ember.Freezable.detect(this)) { + return get(this, 'isFrozen') ? this : this.copy().freeze(); + } else { + throw new Ember.Error(Ember.String.fmt("%@ does not support freezing", [this])); + } } }); @@ -14451,81 +17240,97 @@ Ember.Evented = Ember.Mixin.create({ (function() { -var RSVP = requireModule("rsvp"); - -RSVP.configure('async', function(callback, promise) { - Ember.run.schedule('actions', promise, callback, promise); -}); - /** @module ember @submodule ember-runtime */ -var get = Ember.get; + +var get = Ember.get, set = Ember.set; /** - @class Deferred - @namespace Ember - */ -Ember.DeferredMixin = Ember.Mixin.create({ - /** - Add handlers to be called when the Deferred object is resolved or rejected. + The `Ember.Freezable` mixin implements some basic methods for marking an + object as frozen. Once an object is frozen it should be read only. No changes + may be made the internal state of the object. - @method then - @param {Function} resolve a callback function to be called when done - @param {Function} reject a callback function to be called when failed - */ - then: function(resolve, reject) { - var deferred, promise, entity; + ## Enforcement - entity = this; - deferred = get(this, '_deferred'); - promise = deferred.promise; + To fully support freezing in your subclass, you must include this mixin and + override any method that might alter any property on the object to instead + raise an exception. You can check the state of an object by checking the + `isFrozen` property. - function fulfillmentHandler(fulfillment) { - if (fulfillment === promise) { - return resolve(entity); - } else { - return resolve(fulfillment); - } + Although future versions of JavaScript may support language-level freezing + object objects, that is not the case today. Even if an object is freezable, + it is still technically possible to modify the object, even though it could + break other parts of your application that do not expect a frozen object to + change. It is, therefore, very important that you always respect the + `isFrozen` property on all freezable objects. + + ## Example Usage + + The example below shows a simple object that implement the `Ember.Freezable` + protocol. + + ```javascript + Contact = Ember.Object.extend(Ember.Freezable, { + firstName: null, + lastName: null, + + // swaps the names + swapNames: function() { + if (this.get('isFrozen')) throw Ember.FROZEN_ERROR; + var tmp = this.get('firstName'); + this.set('firstName', this.get('lastName')); + this.set('lastName', tmp); + return this; } - return promise.then(resolve && fulfillmentHandler, reject); - }, + }); - /** - Resolve a Deferred object and call any `doneCallbacks` with the given args. + c = Contact.create({ firstName: "John", lastName: "Doe" }); + c.swapNames(); // returns c + c.freeze(); + c.swapNames(); // EXCEPTION + ``` - @method resolve - */ - resolve: function(value) { - var deferred, promise; + ## Copying - deferred = get(this, '_deferred'); - promise = deferred.promise; + Usually the `Ember.Freezable` protocol is implemented in cooperation with the + `Ember.Copyable` protocol, which defines a `frozenCopy()` method that will + return a frozen object, if the object implements this method as well. - if (value === this) { - deferred.resolve(promise); - } else { - deferred.resolve(value); - } - }, + @class Freezable + @namespace Ember + @since Ember 0.9 +*/ +Ember.Freezable = Ember.Mixin.create({ /** - Reject a Deferred object and call any `failCallbacks` with the given args. + Set to `true` when the object is frozen. Use this property to detect + whether your object is frozen or not. - @method reject + @property isFrozen + @type Boolean */ - reject: function(value) { - get(this, '_deferred').reject(value); - }, + isFrozen: false, + + /** + Freezes the object. Once this method has been called the object should + no longer allow any properties to be edited. + + @method freeze + @return {Object} receiver + */ + freeze: function() { + if (get(this, 'isFrozen')) return this; + set(this, 'isFrozen', true); + return this; + } - _deferred: Ember.computed(function() { - return RSVP.defer(); - }) }); +Ember.FROZEN_ERROR = "Frozen object cannot be modified."; })(); @@ -14537,59 +17342,108 @@ Ember.DeferredMixin = Ember.Mixin.create({ @submodule ember-runtime */ -var get = Ember.get; +var forEach = Ember.EnumerableUtils.forEach; /** - The `Ember.ActionHandler` mixin implements support for moving an `actions` - property to an `_actions` property at extend time, and adding `_actions` - to the object's mergedProperties list. + This mixin defines the API for modifying generic enumerables. These methods + can be applied to an object regardless of whether it is ordered or + unordered. - `Ember.ActionHandler` is used internally by Ember in `Ember.View`, - `Ember.Controller`, and `Ember.Route`. + Note that an Enumerable can change even if it does not implement this mixin. + For example, a MappedEnumerable cannot be directly modified but if its + underlying enumerable changes, it will change also. - @class ActionHandler + ## Adding Objects + + To add an object to an enumerable, use the `addObject()` method. This + method will only add the object to the enumerable if the object is not + already present and is of a type supported by the enumerable. + + ```javascript + set.addObject(contact); + ``` + + ## Removing Objects + + To remove an object from an enumerable, use the `removeObject()` method. This + will only remove the object if it is present in the enumerable, otherwise + this method has no effect. + + ```javascript + set.removeObject(contact); + ``` + + ## Implementing In Your Own Code + + If you are implementing an object and want to support this API, just include + this mixin in your class and implement the required methods. In your unit + tests, be sure to apply the Ember.MutableEnumerableTests to your object. + + @class MutableEnumerable @namespace Ember + @uses Ember.Enumerable */ -Ember.ActionHandler = Ember.Mixin.create({ - mergedProperties: ['_actions'], +Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { /** - @private + __Required.__ You must implement this method to apply this mixin. - Moves `actions` to `_actions` at extend time. Note that this currently - modifies the mixin themselves, which is technically dubious but - is practically of little consequence. This may change in the future. + Attempts to add the passed object to the receiver if the object is not + already present in the collection. If the object is present, this method + has no effect. - @method willMergeMixin + If the passed object is of a type not supported by the receiver, + then this method should raise an exception. + + @method addObject + @param {Object} object The object to add to the enumerable. + @return {Object} the passed object */ - willMergeMixin: function(props) { - if (props.actions && !props._actions) { - props._actions = Ember.merge(props._actions || {}, props.actions); - delete props.actions; - } + addObject: Ember.required(Function), + + /** + Adds each object in the passed enumerable to the receiver. + + @method addObjects + @param {Ember.Enumerable} objects the objects to add. + @return {Object} receiver + */ + addObjects: function(objects) { + Ember.beginPropertyChanges(this); + forEach(objects, function(obj) { this.addObject(obj); }, this); + Ember.endPropertyChanges(this); + return this; }, - send: function(actionName) { - var args = [].slice.call(arguments, 1), target; + /** + __Required.__ You must implement this method to apply this mixin. - if (this._actions && this._actions[actionName]) { - if (this._actions[actionName].apply(this, args) === true) { - // handler returned true, so this action will bubble - } else { - return; - } - } else if (this.deprecatedSend && this.deprecatedSendHandles && this.deprecatedSendHandles(actionName)) { - if (this.deprecatedSend.apply(this, [].slice.call(arguments)) === true) { - // handler return true, so this action will bubble - } else { - return; - } - } + Attempts to remove the passed object from the receiver collection if the + object is present in the collection. If the object is not present, + this method has no effect. - if (target = get(this, 'target')) { - Ember.assert("The `target` for " + this + " (" + target + ") does not have a `send` method", typeof target.send === 'function'); - target.send.apply(target, arguments); - } + If the passed object is of a type not supported by the receiver, + then this method should raise an exception. + + @method removeObject + @param {Object} object The object to remove from the enumerable. + @return {Object} the passed object + */ + removeObject: Ember.required(Function), + + + /** + Removes each object in the passed enumerable from the receiver. + + @method removeObjects + @param {Ember.Enumerable} objects the objects to remove + @return {Object} receiver + */ + removeObjects: function(objects) { + Ember.beginPropertyChanges(this); + forEach(objects, function(obj) { this.removeObject(obj); }, this); + Ember.endPropertyChanges(this); + return this; } }); @@ -14599,604 +17453,686 @@ Ember.ActionHandler = Ember.Mixin.create({ (function() { -var set = Ember.set, get = Ember.get, - resolve = Ember.RSVP.resolve, - rethrow = Ember.RSVP.rethrow, - not = Ember.computed.not, - or = Ember.computed.or; - /** - @module ember - @submodule ember-runtime - */ +@module ember +@submodule ember-runtime +*/ +// .......................................................... +// CONSTANTS +// -function installPromise(proxy, promise) { - promise.then(function(value) { - set(proxy, 'isFulfilled', true); - set(proxy, 'content', value); +var OUT_OF_RANGE_EXCEPTION = "Index out of range" ; +var EMPTY = []; - return value; - }, function(reason) { - set(proxy, 'isRejected', true); - set(proxy, 'reason', reason); - }).fail(rethrow); -} +// .......................................................... +// HELPERS +// + +var get = Ember.get, set = Ember.set; /** - A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. + This mixin defines the API for modifying array-like objects. These methods + can be applied only to a collection that keeps its items in an ordered set. - ```javascript - var ObjectPromiseController = Ember.ObjectController.extend(Ember.PromiseProxyMixin); + Note that an Array can change even if it does not implement this mixin. + For example, one might implement a SparseArray that cannot be directly + modified, but if its underlying enumerable changes, it will change also. - var controller = ObjectPromiseController.create({ - promise: $.getJSON('/some/remote/data.json') - }); + @class MutableArray + @namespace Ember + @uses Ember.Array + @uses Ember.MutableEnumerable +*/ +Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, { - controller.then(function(json){ - // the json - }, function(reason) { - // the reason why you have no json - }); - ``` + /** + __Required.__ You must implement this method to apply this mixin. - the controller has bindable attributes which - track the promises life cycle + This is one of the primitives you must implement to support `Ember.Array`. + You should replace amt objects started at idx with the objects in the + passed array. You should also call `this.enumerableContentDidChange()` + + @method replace + @param {Number} idx Starting index in the array to replace. If + idx >= length, then append to the end of the array. + @param {Number} amt Number of elements that should be removed from + the array, starting at *idx*. + @param {Array} objects An array of zero or more objects that should be + inserted into the array at *idx* + */ + replace: Ember.required(), + + /** + Remove all elements from self. This is useful if you + want to reuse an existing array without having to recreate it. + + ```javascript + var colors = ["red", "green", "blue"]; + color.length(); // 3 + colors.clear(); // [] + colors.length(); // 0 + ``` + + @method clear + @return {Ember.Array} An empty Array. + */ + clear: function () { + var len = get(this, 'length'); + if (len === 0) return this; + this.replace(0, len, EMPTY); + return this; + }, + + /** + This will use the primitive `replace()` method to insert an object at the + specified index. - ```javascript - controller.get('isPending') //=> true - controller.get('isSettled') //=> false - controller.get('isRejected') //=> false - controller.get('isFulfilled') //=> false - ``` + ```javascript + var colors = ["red", "green", "blue"]; + colors.insertAt(2, "yellow"); // ["red", "green", "yellow", "blue"] + colors.insertAt(5, "orange"); // Error: Index out of range + ``` - When the the $.getJSON completes, and the promise is fulfilled - with json, the life cycle attributes will update accordingly. + @method insertAt + @param {Number} idx index of insert the object at. + @param {Object} object object to insert + @return this + */ + insertAt: function(idx, object) { + if (idx > get(this, 'length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION) ; + this.replace(idx, 0, [object]) ; + return this ; + }, - ```javascript - controller.get('isPending') //=> false - controller.get('isSettled') //=> true - controller.get('isRejected') //=> false - controller.get('isFulfilled') //=> true - ``` + /** + Remove an object at the specified index using the `replace()` primitive + method. You can pass either a single index, or a start and a length. - As the controller is an ObjectController, and the json now its content, - all the json properties will be available directly from the controller. + If you pass a start and length that is beyond the + length this method will throw an `OUT_OF_RANGE_EXCEPTION`. - ```javascript - // Assuming the following json: - { - firstName: 'Stefan', - lastName: 'Penner' - } + ```javascript + var colors = ["red", "green", "blue", "yellow", "orange"]; + colors.removeAt(0); // ["green", "blue", "yellow", "orange"] + colors.removeAt(2, 2); // ["green", "blue"] + colors.removeAt(4, 2); // Error: Index out of range + ``` - // both properties will accessible on the controller - controller.get('firstName') //=> 'Stefan' - controller.get('lastName') //=> 'Penner' - ``` + @method removeAt + @param {Number} start index, start of range + @param {Number} len length of passing range + @return {Object} receiver + */ + removeAt: function(start, len) { + if ('number' === typeof start) { - If the controller is backing a template, the attributes are - bindable from within that template - ```handlebars - {{#if isPending}} - loading... - {{else}} - firstName: {{firstName}} - lastName: {{lastName}} - {{/if}} - ``` - @class Ember.PromiseProxyMixin -*/ -Ember.PromiseProxyMixin = Ember.Mixin.create({ - reason: null, - isPending: not('isSettled').readOnly(), - isSettled: or('isRejected', 'isFulfilled').readOnly(), - isRejected: false, - isFulfilled: false, + if ((start < 0) || (start >= get(this, 'length'))) { + throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); + } - promise: Ember.computed(function(key, promise) { - if (arguments.length === 2) { - promise = resolve(promise); - installPromise(this, promise); - return promise; - } else { - throw new Error("PromiseProxy's promise must be set"); + // fast case + if (len === undefined) len = 1; + this.replace(start, len, EMPTY); } - }), - then: function(fulfill, reject) { - return get(this, 'promise').then(fulfill, reject); - } -}); + return this ; + }, + /** + Push the object onto the end of the array. Works just like `push()` but it + is KVO-compliant. -})(); + ```javascript + var colors = ["red", "green"]; + colors.pushObject("black"); // ["red", "green", "black"] + colors.pushObject(["yellow"]); // ["red", "green", ["yellow"]] + ``` + @method pushObject + @param {*} obj object to push + @return The same obj passed as param + */ + pushObject: function(obj) { + this.insertAt(get(this, 'length'), obj) ; + return obj; + }, + /** + Add the objects in the passed numerable to the end of the array. Defers + notifying observers of the change until all objects are added. -(function() { + ```javascript + var colors = ["red"]; + colors.pushObjects(["yellow", "orange"]); // ["red", "yellow", "orange"] + ``` -})(); + @method pushObjects + @param {Ember.Enumerable} objects the objects to add + @return {Ember.Array} receiver + */ + pushObjects: function(objects) { + if (!(Ember.Enumerable.detect(objects) || Ember.isArray(objects))) { + throw new TypeError("Must pass Ember.Enumerable to Ember.MutableArray#pushObjects"); + } + this.replace(get(this, 'length'), 0, objects); + return this; + }, + /** + Pop object from array or nil if none are left. Works just like `pop()` but + it is KVO-compliant. + ```javascript + var colors = ["red", "green", "blue"]; + colors.popObject(); // "blue" + console.log(colors); // ["red", "green"] + ``` -(function() { -var get = Ember.get, - forEach = Ember.EnumerableUtils.forEach, - RETAIN = 'r', - INSERT = 'i', - DELETE = 'd'; + @method popObject + @return object + */ + popObject: function() { + var len = get(this, 'length') ; + if (len === 0) return null ; -/** - An `Ember.TrackedArray` tracks array operations. It's useful when you want to - lazily compute the indexes of items in an array after they've been shifted by - subsequent operations. + var ret = this.objectAt(len-1) ; + this.removeAt(len-1, 1) ; + return ret ; + }, - @class TrackedArray - @namespace Ember - @param {array} [items=[]] The array to be tracked. This is used just to get - the initial items for the starting state of retain:n. -*/ -Ember.TrackedArray = function (items) { - if (arguments.length < 1) { items = []; } + /** + Shift an object from start of array or nil if none are left. Works just + like `shift()` but it is KVO-compliant. - var length = get(items, 'length'); + ```javascript + var colors = ["red", "green", "blue"]; + colors.shiftObject(); // "red" + console.log(colors); // ["green", "blue"] + ``` - if (length) { - this._content = [new ArrayOperation(RETAIN, length, items)]; - } else { - this._content = []; - } -}; + @method shiftObject + @return object + */ + shiftObject: function() { + if (get(this, 'length') === 0) return null ; + var ret = this.objectAt(0) ; + this.removeAt(0) ; + return ret ; + }, -Ember.TrackedArray.RETAIN = RETAIN; -Ember.TrackedArray.INSERT = INSERT; -Ember.TrackedArray.DELETE = DELETE; + /** + Unshift an object to start of array. Works just like `unshift()` but it is + KVO-compliant. -Ember.TrackedArray.prototype = { + ```javascript + var colors = ["red"]; + colors.unshiftObject("yellow"); // ["yellow", "red"] + colors.unshiftObject(["black"]); // [["black"], "yellow", "red"] + ``` + + @method unshiftObject + @param {*} obj object to unshift + @return The same obj passed as param + */ + unshiftObject: function(obj) { + this.insertAt(0, obj) ; + return obj ; + }, /** - Track that `newItems` were added to the tracked array at `index`. + Adds the named objects to the beginning of the array. Defers notifying + observers until all objects have been added. - @method addItems - @param index - @param newItems - */ - addItems: function (index, newItems) { - var count = get(newItems, 'length'), - match = this._findArrayOperation(index), - arrayOperation = match.operation, - arrayOperationIndex = match.index, - arrayOperationRangeStart = match.rangeStart, - composeIndex, - splitIndex, - splitItems, - splitArrayOperation, - newArrayOperation; + ```javascript + var colors = ["red"]; + colors.unshiftObjects(["black", "white"]); // ["black", "white", "red"] + colors.unshiftObjects("yellow"); // Type Error: 'undefined' is not a function + ``` - newArrayOperation = new ArrayOperation(INSERT, count, newItems); + @method unshiftObjects + @param {Ember.Enumerable} objects the objects to add + @return {Ember.Array} receiver + */ + unshiftObjects: function(objects) { + this.replace(0, 0, objects); + return this; + }, - if (arrayOperation) { - if (!match.split) { - // insert left of arrayOperation - this._content.splice(arrayOperationIndex, 0, newArrayOperation); - composeIndex = arrayOperationIndex; - } else { - this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); - composeIndex = arrayOperationIndex + 1; - } - } else { - // insert at end - this._content.push(newArrayOperation); - composeIndex = arrayOperationIndex; - } + /** + Reverse objects in the array. Works just like `reverse()` but it is + KVO-compliant. - this._composeInsert(composeIndex); + @method reverseObjects + @return {Ember.Array} receiver + */ + reverseObjects: function() { + var len = get(this, 'length'); + if (len === 0) return this; + var objects = this.toArray().reverse(); + this.replace(0, len, objects); + return this; }, /** - Track that `count` items were removed at `index`. + Replace all the the receiver's content with content of the argument. + If argument is an empty array receiver will be cleared. - @method removeItems - @param index - @param count - */ - removeItems: function (index, count) { - var match = this._findArrayOperation(index), - arrayOperation = match.operation, - arrayOperationIndex = match.index, - arrayOperationRangeStart = match.rangeStart, - newArrayOperation, - composeIndex; + ```javascript + var colors = ["red", "green", "blue"]; + colors.setObjects(["black", "white"]); // ["black", "white"] + colors.setObjects([]); // [] + ``` - newArrayOperation = new ArrayOperation(DELETE, count); - if (!match.split) { - // insert left of arrayOperation - this._content.splice(arrayOperationIndex, 0, newArrayOperation); - composeIndex = arrayOperationIndex; - } else { - this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); - composeIndex = arrayOperationIndex + 1; - } + @method setObjects + @param {Ember.Array} objects array whose content will be used for replacing + the content of the receiver + @return {Ember.Array} receiver with the new content + */ + setObjects: function(objects) { + if (objects.length === 0) return this.clear(); - return this._composeDelete(composeIndex); + var len = get(this, 'length'); + this.replace(0, len, objects); + return this; }, - /** - Apply all operations, reducing them to retain:n, for `n`, the number of - items in the array. + // .......................................................... + // IMPLEMENT Ember.MutableEnumerable + // - `callback` will be called for each operation and will be passed the following arguments: - - {array} items The items for the given operation - - {number} offset The computed offset of the items, ie the index in the - array of the first item for this operation. - - {string} operation The type of the operation. One of - `Ember.TrackedArray.{RETAIN, DELETE, INSERT}` + removeObject: function(obj) { + var loc = get(this, 'length') || 0; + while(--loc >= 0) { + var curObject = this.objectAt(loc) ; + if (curObject === obj) this.removeAt(loc) ; + } + return this ; + }, - @method apply + addObject: function(obj) { + if (!this.contains(obj)) this.pushObject(obj); + return this ; + } - @param {function} callback - */ - apply: function (callback) { - var items = [], - offset = 0; +}); - forEach(this._content, function (arrayOperation) { - callback(arrayOperation.items, offset, arrayOperation.operation); +})(); - if (arrayOperation.operation !== DELETE) { - offset += arrayOperation.count; - items = items.concat(arrayOperation.items); - } - }); - this._content = [new ArrayOperation(RETAIN, items.length, items)]; - }, - /** - Return an ArrayOperationMatch for the operation that contains the item at `index`. +(function() { +/** +@module ember +@submodule ember-runtime +*/ - @method _findArrayOperation +var get = Ember.get, set = Ember.set; - @param {number} index the index of the item whose operation information - should be returned. - @private - */ - _findArrayOperation: function (index) { - var arrayOperationIndex, - len, - split = false, - arrayOperation, - arrayOperationRangeStart, - arrayOperationRangeEnd; +/** +`Ember.TargetActionSupport` is a mixin that can be included in a class +to add a `triggerAction` method with semantics similar to the Handlebars +`{{action}}` helper. In normal Ember usage, the `{{action}}` helper is +usually the best choice. This mixin is most often useful when you are +doing more complex event handling in View objects. - // OPTIMIZE: we could search these faster if we kept a balanced tree. - // find leftmost arrayOperation to the right of `index` - for (arrayOperationIndex = arrayOperationRangeStart = 0, len = this._content.length; arrayOperationIndex < len; ++arrayOperationIndex) { - arrayOperation = this._content[arrayOperationIndex]; +See also `Ember.ViewTargetActionSupport`, which has +view-aware defaults for target and actionContext. - if (arrayOperation.operation === DELETE) { continue; } +@class TargetActionSupport +@namespace Ember +@extends Ember.Mixin +*/ +Ember.TargetActionSupport = Ember.Mixin.create({ + target: null, + action: null, + actionContext: null, - arrayOperationRangeEnd = arrayOperationRangeStart + arrayOperation.count - 1; + targetObject: Ember.computed(function() { + var target = get(this, 'target'); - if (index === arrayOperationRangeStart) { - break; - } else if (index > arrayOperationRangeStart && index <= arrayOperationRangeEnd) { - split = true; - break; - } else { - arrayOperationRangeStart = arrayOperationRangeEnd + 1; - } + if (Ember.typeOf(target) === "string") { + var value = get(this, target); + if (value === undefined) { value = get(Ember.lookup, target); } + return value; + } else { + return target; } + }).property('target'), - return new ArrayOperationMatch(arrayOperation, arrayOperationIndex, split, arrayOperationRangeStart); - }, - - _split: function (arrayOperationIndex, splitIndex, newArrayOperation) { - var arrayOperation = this._content[arrayOperationIndex], - splitItems = arrayOperation.items.slice(splitIndex), - splitArrayOperation = new ArrayOperation(arrayOperation.operation, splitItems.length, splitItems); + actionContextObject: Ember.computed(function() { + var actionContext = get(this, 'actionContext'); - // truncate LHS - arrayOperation.count = splitIndex; - arrayOperation.items = arrayOperation.items.slice(0, splitIndex); + if (Ember.typeOf(actionContext) === "string") { + var value = get(this, actionContext); + if (value === undefined) { value = get(Ember.lookup, actionContext); } + return value; + } else { + return actionContext; + } + }).property('actionContext'), - this._content.splice(arrayOperationIndex + 1, 0, newArrayOperation, splitArrayOperation); - }, + /** + Send an `action` with an `actionContext` to a `target`. The action, actionContext + and target will be retrieved from properties of the object. For example: - // TODO: unify _composeInsert, _composeDelete - // see SubArray for a better implementation. - _composeInsert: function (index) { - var newArrayOperation = this._content[index], - leftArrayOperation = this._content[index-1], // may be undefined - rightArrayOperation = this._content[index+1], // may be undefined - leftOp = leftArrayOperation && leftArrayOperation.operation, - rightOp = rightArrayOperation && rightArrayOperation.operation; + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + action: 'save', + actionContext: Ember.computed.alias('context'), + click: function() { + this.triggerAction(); // Sends the `save` action, along with the current context + // to the current controller + } + }); + ``` - if (leftOp === INSERT) { - // merge left - leftArrayOperation.count += newArrayOperation.count; - leftArrayOperation.items = leftArrayOperation.items.concat(newArrayOperation.items); + The `target`, `action`, and `actionContext` can be provided as properties of + an optional object argument to `triggerAction` as well. - if (rightOp === INSERT) { - // also merge right - leftArrayOperation.count += rightArrayOperation.count; - leftArrayOperation.items = leftArrayOperation.items.concat(rightArrayOperation.items); - this._content.splice(index, 2); - } else { - // only merge left - this._content.splice(index, 1); - } - } else if (rightOp === INSERT) { - // merge right - newArrayOperation.count += rightArrayOperation.count; - newArrayOperation.items = newArrayOperation.items.concat(rightArrayOperation.items); - this._content.splice(index + 1, 1); + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + click: function() { + this.triggerAction({ + action: 'save', + target: this.get('controller'), + actionContext: this.get('context'), + }); // Sends the `save` action, along with the current context + // to the current controller } - }, + }); + ``` - _composeDelete: function (index) { - var arrayOperation = this._content[index], - deletesToGo = arrayOperation.count, - leftArrayOperation = this._content[index-1], // may be undefined - leftOp = leftArrayOperation && leftArrayOperation.operation, - nextArrayOperation, - nextOp, - nextCount, - removedItems = []; + The `actionContext` defaults to the object you mixing `TargetActionSupport` into. + But `target` and `action` must be specified either as properties or with the argument + to `triggerAction`, or a combination: - if (leftOp === DELETE) { - arrayOperation = leftArrayOperation; - index -= 1; + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + click: function() { + this.triggerAction({ + action: 'save' + }); // Sends the `save` action, along with a reference to `this`, + // to the current controller } + }); + ``` - for (var i = index + 1; deletesToGo > 0; ++i) { - nextArrayOperation = this._content[i]; - nextOp = nextArrayOperation.operation; - nextCount = nextArrayOperation.count; + @method triggerAction + @param opts {Hash} (optional, with the optional keys action, target and/or actionContext) + @return {Boolean} true if the action was sent successfully and did not return false + */ + triggerAction: function(opts) { + opts = opts || {}; + var action = opts.action || get(this, 'action'), + target = opts.target || get(this, 'targetObject'), + actionContext = opts.actionContext; - if (nextOp === DELETE) { - arrayOperation.count += nextCount; - continue; - } + function args(options, actionName) { + var ret = []; + if (actionName) { ret.push(actionName); } - if (nextCount > deletesToGo) { - removedItems = removedItems.concat(nextArrayOperation.items.splice(0, deletesToGo)); - nextArrayOperation.count -= deletesToGo; + return ret.concat(options); + } - // In the case where we truncate the last arrayOperation, we don't need to - // remove it; also the deletesToGo reduction is not the entirety of - // nextCount - i -= 1; - nextCount = deletesToGo; + if (typeof actionContext === 'undefined') { + actionContext = get(this, 'actionContextObject') || this; + } - deletesToGo = 0; + if (target && action) { + var ret; + + if (target.send) { + ret = target.send.apply(target, args(actionContext, action)); } else { - removedItems = removedItems.concat(nextArrayOperation.items); - deletesToGo -= nextCount; + Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); + ret = target[action].apply(target, args(actionContext)); } - if (nextOp === INSERT) { - arrayOperation.count -= nextCount; - } - } + if (ret !== false) ret = true; - if (arrayOperation.count > 0) { - this._content.splice(index+1, i-1-index); + return ret; } else { - // The delete operation can go away; it has merely reduced some other - // operation, as in D:3 I:4 - this._content.splice(index, 1); + return false; } - - return removedItems; } -}; +}); -function ArrayOperation (operation, count, items) { - this.operation = operation; // RETAIN | INSERT | DELETE - this.count = count; - this.items = items; -} +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ /** - Internal data structure used to include information when looking up operations - by item index. + This mixin allows for Ember objects to subscribe to and emit events. - @method ArrayOperationMatch - @private - @property {ArrayOperation} operation - @property {number} index The index of `operation` in the array of operations. - @property {boolean} split Whether or not the item index searched for would - require a split for a new operation type. - @property {number} rangeStart The index of the first item in the operation, - with respect to the tracked array. The index of the last item can be computed - from `rangeStart` and `operation.count`. -*/ -function ArrayOperationMatch(operation, index, split, rangeStart) { - this.operation = operation; - this.index = index; - this.split = split; - this.rangeStart = rangeStart; -} + ```javascript + App.Person = Ember.Object.extend(Ember.Evented, { + greet: function() { + // ... + this.trigger('greet'); + } + }); -})(); + var person = App.Person.create(); + person.on('greet', function() { + console.log('Our person has greeted'); + }); + person.greet(); -(function() { -var get = Ember.get, - forEach = Ember.EnumerableUtils.forEach, - RETAIN = 'r', - FILTER = 'f'; + // outputs: 'Our person has greeted' + ``` -function Operation (type, count) { - this.type = type; - this.count = count; -} + You can also chain multiple event subscriptions: -/** - An `Ember.SubArray` tracks an array in a way similar to, but more specialized - than, `Ember.TrackedArray`. It is useful for keeping track of the indexes of - items within a filtered array. + ```javascript + person.on('greet', function() { + console.log('Our person has greeted'); + }).one('greet', function() { + console.log('Offer one-time special'); + }).off('event', this, forgetThis); + ``` - @class SubArray + @class Evented @namespace Ember -*/ -Ember.SubArray = function (length) { - if (arguments.length < 1) { length = 0; } - - if (length > 0) { - this._operations = [new Operation(RETAIN, length)]; - } else { - this._operations = []; - } -}; + */ +Ember.Evented = Ember.Mixin.create({ -Ember.SubArray.prototype = { /** - Track that an item was added to the tracked array. + Subscribes to a named event with given function. - @method addItem + ```javascript + person.on('didLoad', function() { + // fired once the person has loaded + }); + ``` - @param {number} index The index of the item in the tracked array. - @param {boolean} match `true` iff the item is included in the subarray. + An optional target can be passed in as the 2nd argument that will + be set as the "this" for the callback. This is a good way to give your + function access to the object triggering the event. When the target + parameter is used the callback becomes the third argument. - @returns {number} The index of the item in the subarray. + @method on + @param {String} name The name of the event + @param {Object} [target] The "this" binding for the callback + @param {Function} method The callback to execute + @return this */ - addItem: function(index, match) { - var returnValue = -1, - itemType = match ? RETAIN : FILTER, - self = this; - - this._findOperation(index, function(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { - var newOperation, splitOperation; + on: function(name, target, method) { + Ember.addListener(this, name, target, method); + return this; + }, - if (itemType === operation.type) { - ++operation.count; - } else if (index === rangeStart) { - // insert to the left of `operation` - self._operations.splice(operationIndex, 0, new Operation(itemType, 1)); - } else { - newOperation = new Operation(itemType, 1); - splitOperation = new Operation(operation.type, rangeEnd - index + 1); - operation.count = index - rangeStart; + /** + Subscribes a function to a named event and then cancels the subscription + after the first time the event is triggered. It is good to use ``one`` when + you only care about the first time an event has taken place. - self._operations.splice(operationIndex + 1, 0, newOperation, splitOperation); - } + This function takes an optional 2nd argument that will become the "this" + value for the callback. If this argument is passed then the 3rd argument + becomes the function. - if (match) { - if (operation.type === RETAIN) { - returnValue = seenInSubArray + (index - rangeStart); - } else { - returnValue = seenInSubArray; - } - } + @method one + @param {String} name The name of the event + @param {Object} [target] The "this" binding for the callback + @param {Function} method The callback to execute + @return this + */ + one: function(name, target, method) { + if (!method) { + method = target; + target = null; + } - self._composeAt(operationIndex); - }, function(seenInSubArray) { - self._operations.push(new Operation(itemType, 1)); + Ember.addListener(this, name, target, method, true); + return this; + }, - if (match) { - returnValue = seenInSubArray; - } + /** + Triggers a named event for the object. Any additional arguments + will be passed as parameters to the functions that are subscribed to the + event. - self._composeAt(self._operations.length-1); + ```javascript + person.on('didEat', function(food) { + console.log('person ate some ' + food); }); - return returnValue; + person.trigger('didEat', 'broccoli'); + + // outputs: person ate some broccoli + ``` + @method trigger + @param {String} name The name of the event + @param {Object...} args Optional arguments to pass on + */ + trigger: function(name) { + var args = [], i, l; + for (i = 1, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + Ember.sendEvent(this, name, args); }, /** - Track that an item was removed from the tracked array. + Cancels subscription for given name, target, and method. - @method removeItem + @method off + @param {String} name The name of the event + @param {Object} target The target of the subscription + @param {Function} method The function of the subscription + @return this + */ + off: function(name, target, method) { + Ember.removeListener(this, name, target, method); + return this; + }, - @param {number} index The index of the item in the tracked array. + /** + Checks to see if object has any subscriptions for named event. - @returns {number} The index of the item in the subarray, or `-1` if the item - was not in the subarray. - */ - removeItem: function(index) { - var returnValue = -1, - self = this; + @method has + @param {String} name The name of the event + @return {Boolean} does the object have a subscription for event + */ + has: function(name) { + return Ember.hasListeners(this, name); + } +}); - this._findOperation(index, function (operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { - if (operation.type === RETAIN) { - returnValue = seenInSubArray + (index - rangeStart); - } +})(); - if (operation.count > 1) { - --operation.count; - } else { - self._operations.splice(operationIndex, 1); - self._composeAt(operationIndex); - } - }); - return returnValue; - }, +(function() { +var RSVP = requireModule("rsvp"); - _findOperation: function (index, foundCallback, notFoundCallback) { - var operationIndex, - len, - operation, - rangeStart, - rangeEnd, - seenInSubArray = 0; +RSVP.configure('async', function(callback, promise) { + Ember.run.schedule('actions', promise, callback, promise); +}); - // OPTIMIZE: change to balanced tree - // find leftmost operation to the right of `index` - for (operationIndex = rangeStart = 0, len = this._operations.length; operationIndex < len; rangeStart = rangeEnd + 1, ++operationIndex) { - operation = this._operations[operationIndex]; - rangeEnd = rangeStart + operation.count - 1; +RSVP.Promise.prototype.fail = function(callback, label){ + Ember.deprecate('RSVP.Promise.fail has been renamed as RSVP.Promise.catch'); + return this['catch'](callback, label); +}; - if (index >= rangeStart && index <= rangeEnd) { - foundCallback(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray); - return; - } else if (operation.type === RETAIN) { - seenInSubArray += operation.count; +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get; + +/** + @class Deferred + @namespace Ember + */ +Ember.DeferredMixin = Ember.Mixin.create({ + /** + Add handlers to be called when the Deferred object is resolved or rejected. + + @method then + @param {Function} resolve a callback function to be called when done + @param {Function} reject a callback function to be called when failed + */ + then: function(resolve, reject, label) { + var deferred, promise, entity; + + entity = this; + deferred = get(this, '_deferred'); + promise = deferred.promise; + + function fulfillmentHandler(fulfillment) { + if (fulfillment === promise) { + return resolve(entity); + } else { + return resolve(fulfillment); } } - notFoundCallback(seenInSubArray); + return promise.then(resolve && fulfillmentHandler, reject, label); }, - _composeAt: function(index) { - var op = this._operations[index], - otherOp; + /** + Resolve a Deferred object and call any `doneCallbacks` with the given args. - if (!op) { - // Composing out of bounds is a no-op, as when removing the last operation - // in the list. - return; - } + @method resolve + */ + resolve: function(value) { + var deferred, promise; - if (index > 0) { - otherOp = this._operations[index-1]; - if (otherOp.type === op.type) { - op.count += otherOp.count; - this._operations.splice(index-1, 1); - } - } + deferred = get(this, '_deferred'); + promise = deferred.promise; - if (index < this._operations.length-1) { - otherOp = this._operations[index+1]; - if (otherOp.type === op.type) { - op.count += otherOp.count; - this._operations.splice(index+1, 1); - } + if (value === this) { + deferred.resolve(promise); + } else { + deferred.resolve(value); } - } -}; + }, -})(); + /** + Reject a Deferred object and call any `failCallbacks` with the given args. + @method reject + */ + reject: function(value) { + get(this, '_deferred').reject(value); + }, + _deferred: Ember.computed(function() { + return RSVP.defer('Ember: DeferredMixin - ' + this); + }) +}); -(function() { -Ember.Container = requireModule('container'); -Ember.Container.set = Ember.set; })(); @@ -15208,853 +18144,956 @@ Ember.Container.set = Ember.set; @submodule ember-runtime */ +var get = Ember.get, typeOf = Ember.typeOf; -// NOTE: this object should never be included directly. Instead use `Ember.Object`. -// We only define this separately so that `Ember.Set` can depend on it. +/** + The `Ember.ActionHandler` mixin implements support for moving an `actions` + property to an `_actions` property at extend time, and adding `_actions` + to the object's mergedProperties list. + `Ember.ActionHandler` is used internally by Ember in `Ember.View`, + `Ember.Controller`, and `Ember.Route`. -var set = Ember.set, get = Ember.get, - o_create = Ember.create, - o_defineProperty = Ember.platform.defineProperty, - GUID_KEY = Ember.GUID_KEY, - guidFor = Ember.guidFor, - generateGuid = Ember.generateGuid, - meta = Ember.meta, - rewatch = Ember.rewatch, - finishChains = Ember.finishChains, - sendEvent = Ember.sendEvent, - destroy = Ember.destroy, - schedule = Ember.run.schedule, - Mixin = Ember.Mixin, - applyMixin = Mixin._apply, - finishPartial = Mixin.finishPartial, - reopen = Mixin.prototype.reopen, - MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, - indexOf = Ember.EnumerableUtils.indexOf; + @class ActionHandler + @namespace Ember +*/ +Ember.ActionHandler = Ember.Mixin.create({ + mergedProperties: ['_actions'], -var undefinedDescriptor = { - configurable: true, - writable: true, - enumerable: false, - value: undefined -}; + /** + The collection of functions, keyed by name, available on this + `ActionHandler` as action targets. -function makeCtor() { + These functions will be invoked when a matching `{{action}}` is triggered + from within a template and the application's current route is this route. - // Note: avoid accessing any properties on the object since it makes the - // method a lot faster. This is glue code so we want it to be as fast as - // possible. + Actions can also be invoked from other parts of your application + via `ActionHandler#send`. - var wasApplied = false, initMixins, initProperties; + The `actions` hash will inherit action handlers from + the `actions` hash defined on extended parent classes + or mixins rather than just replace the entire hash, e.g.: - var Class = function() { - if (!wasApplied) { - Class.proto(); // prepare prototype... - } - o_defineProperty(this, GUID_KEY, undefinedDescriptor); - o_defineProperty(this, '_super', undefinedDescriptor); - var m = meta(this), proto = m.proto; - m.proto = this; - if (initMixins) { - // capture locally so we can clear the closed over variable - var mixins = initMixins; - initMixins = null; - this.reopen.apply(this, mixins); - } - if (initProperties) { - // capture locally so we can clear the closed over variable - var props = initProperties; - initProperties = null; + ```js + App.CanDisplayBanner = Ember.Mixin.create({ + actions: { + displayBanner: function(msg) { + // ... + } + } + }); - var concatenatedProperties = this.concatenatedProperties; + App.WelcomeRoute = Ember.Route.extend(App.CanDisplayBanner, { + actions: { + playMusic: function() { + // ... + } + } + }); - for (var i = 0, l = props.length; i < l; i++) { - var properties = props[i]; + // `WelcomeRoute`, when active, will be able to respond + // to both actions, since the actions hash is merged rather + // then replaced when extending mixins / parent classes. + this.send('displayBanner'); + this.send('playMusic'); + ``` - Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); + Within a Controller, Route, View or Component's action handler, + the value of the `this` context is the Controller, Route, View or + Component object: - for (var keyName in properties) { - if (!properties.hasOwnProperty(keyName)) { continue; } + ```js + App.SongRoute = Ember.Route.extend({ + actions: { + myAction: function() { + this.controllerFor("song"); + this.transitionTo("other.route"); + ... + } + } + }); + ``` - var value = properties[keyName], - IS_BINDING = Ember.IS_BINDING; + It is also possible to call `this._super()` from within an + action handler if it overrides a handler defined on a parent + class or mixin: - if (IS_BINDING.test(keyName)) { - var bindings = m.bindings; - if (!bindings) { - bindings = m.bindings = {}; - } else if (!m.hasOwnProperty('bindings')) { - bindings = m.bindings = o_create(m.bindings); - } - bindings[keyName] = value; - } + Take for example the following routes: - var desc = m.descs[keyName]; + ```js + App.DebugRoute = Ember.Mixin.create({ + actions: { + debugRouteInformation: function() { + console.debug("trololo"); + } + } + }); - Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty)); - Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1)); - Ember.assert("`actions` must be provided at extend time, not at create time, when Ember.ActionHandler is used (i.e. views, controllers & routes).", !((keyName === 'actions') && Ember.ActionHandler.detect(this))); + App.AnnoyingDebugRoute = Ember.Route.extend(App.DebugRoute, { + actions: { + debugRouteInformation: function() { + // also call the debugRouteInformation of mixed in App.DebugRoute + this._super(); - if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) { - var baseValue = this[keyName]; + // show additional annoyance + window.alert(...); + } + } + }); + ``` - if (baseValue) { - if ('function' === typeof baseValue.concat) { - value = baseValue.concat(value); - } else { - value = Ember.makeArray(baseValue).concat(value); - } - } else { - value = Ember.makeArray(value); - } - } + ## Bubbling - if (desc) { - desc.set(this, keyName, value); - } else { - if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) { - this.setUnknownProperty(keyName, value); - } else if (MANDATORY_SETTER) { - Ember.defineProperty(this, keyName, null, value); // setup mandatory setter - } else { - this[keyName] = value; - } - } + By default, an action will stop bubbling once a handler defined + on the `actions` hash handles it. To continue bubbling the action, + you must return `true` from the handler: + + ```js + App.Router.map(function() { + this.resource("album", function() { + this.route("song"); + }); + }); + + App.AlbumRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { } } - } - finishPartial(this, m); - this.init.apply(this, arguments); - m.proto = proto; - finishChains(this); - sendEvent(this, "init"); - }; + }); - Class.toString = Mixin.prototype.toString; - Class.willReopen = function() { - if (wasApplied) { - Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin); - } + App.AlbumSongRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { + // ... - wasApplied = false; - }; - Class._initMixins = function(args) { initMixins = args; }; - Class._initProperties = function(args) { initProperties = args; }; + if (actionShouldAlsoBeTriggeredOnParentRoute) { + return true; + } + } + } + }); + ``` - Class.proto = function() { - var superclass = Class.superclass; - if (superclass) { superclass.proto(); } + @property actions + @type Hash + @default null + */ - if (!wasApplied) { - wasApplied = true; - Class.PrototypeMixin.applyPartial(Class.prototype); - rewatch(Class.prototype); - } + /** + Moves `actions` to `_actions` at extend time. Note that this currently + modifies the mixin themselves, which is technically dubious but + is practically of little consequence. This may change in the future. - return this.prototype; - }; + @private + @method willMergeMixin + */ + willMergeMixin: function(props) { + var hashName; - return Class; + if (!props._actions) { + Ember.assert("'actions' should not be a function", typeof(props.actions) !== 'function'); -} + if (typeOf(props.actions) === 'object') { + hashName = 'actions'; + } else if (typeOf(props.events) === 'object') { + Ember.deprecate('Action handlers contained in an `events` object are deprecated in favor of putting them in an `actions` object', false); + hashName = 'events'; + } -/** - @class CoreObject - @namespace Ember -*/ -var CoreObject = makeCtor(); -CoreObject.toString = function() { return "Ember.CoreObject"; }; + if (hashName) { + props._actions = Ember.merge(props._actions || {}, props[hashName]); + } -CoreObject.PrototypeMixin = Mixin.create({ - reopen: function() { - applyMixin(this, arguments, true); - return this; + delete props[hashName]; + } }, /** - An overridable method called when objects are instantiated. By default, - does nothing unless it is overridden during class definition. + Triggers a named action on the `ActionHandler`. Any parameters + supplied after the `actionName` string will be passed as arguments + to the action target function. - Example: + If the `ActionHandler` has its `target` property set, actions may + bubble to the `target`. Bubbling happens when an `actionName` can + not be found in the `ActionHandler`'s `actions` hash or if the + action target function returns `true`. - ```javascript - App.Person = Ember.Object.extend({ - init: function() { - this._super(); - alert('Name is ' + this.get('name')); + Example + + ```js + App.WelcomeRoute = Ember.Route.extend({ + actions: { + playTheme: function() { + this.send('playMusic', 'theme.mp3'); + }, + playMusic: function(track) { + // ... + } } }); + ``` - var steve = App.Person.create({ - name: "Steve" - }); + @method send + @param {String} actionName The action to trigger + @param {*} context a context to send with the action + */ + send: function(actionName) { + var args = [].slice.call(arguments, 1), target; - // alerts 'Name is Steve'. - ``` + if (this._actions && this._actions[actionName]) { + if (this._actions[actionName].apply(this, args) === true) { + // handler returned true, so this action will bubble + } else { + return; + } + } else if (this.deprecatedSend && this.deprecatedSendHandles && this.deprecatedSendHandles(actionName)) { + if (this.deprecatedSend.apply(this, [].slice.call(arguments)) === true) { + // handler return true, so this action will bubble + } else { + return; + } + } - NOTE: If you do override `init` for a framework class like `Ember.View` or - `Ember.ArrayController`, be sure to call `this._super()` in your - `init` declaration! If you don't, Ember may not have an opportunity to - do important setup work, and you'll see strange behavior in your - application. + if (target = get(this, 'target')) { + Ember.assert("The `target` for " + this + " (" + target + ") does not have a `send` method", typeof target.send === 'function'); + target.send.apply(target, arguments); + } + } - @method init - */ - init: function() {}, +}); - /** - Defines the properties that will be concatenated from the superclass - (instead of overridden). +})(); - By default, when you extend an Ember class a property defined in - the subclass overrides a property with the same name that is defined - in the superclass. However, there are some cases where it is preferable - to build up a property's value by combining the superclass' property - value with the subclass' value. An example of this in use within Ember - is the `classNames` property of `Ember.View`. - Here is some sample code showing the difference between a concatenated - property and a normal one: - ```javascript - App.BarView = Ember.View.extend({ - someNonConcatenatedProperty: ['bar'], - classNames: ['bar'] - }); +(function() { +var set = Ember.set, get = Ember.get, + not = Ember.computed.not, + or = Ember.computed.or; - App.FooBarView = App.BarView.extend({ - someNonConcatenatedProperty: ['foo'], - classNames: ['foo'], - }); +/** + @module ember + @submodule ember-runtime + */ - var fooBarView = App.FooBarView.create(); - fooBarView.get('someNonConcatenatedProperty'); // ['foo'] - fooBarView.get('classNames'); // ['ember-view', 'bar', 'foo'] - ``` +function tap(proxy, promise) { + return promise.then(function(value) { + set(proxy, 'isFulfilled', true); + set(proxy, 'content', value); + return value; + }, function(reason) { + set(proxy, 'isRejected', true); + set(proxy, 'reason', reason); + throw reason; + }, "Ember: PromiseProxy"); +} - This behavior extends to object creation as well. Continuing the - above example: +/** + A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. - ```javascript - var view = App.FooBarView.create({ - someNonConcatenatedProperty: ['baz'], - classNames: ['baz'] - }) - view.get('someNonConcatenatedProperty'); // ['baz'] - view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] - ``` - Adding a single property that is not an array will just add it in the array: + ```javascript + var ObjectPromiseController = Ember.ObjectController.extend(Ember.PromiseProxyMixin); - ```javascript - var view = App.FooBarView.create({ - classNames: 'baz' - }) - view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] - ``` + var controller = ObjectPromiseController.create({ + promise: $.getJSON('/some/remote/data.json') + }); - Using the `concatenatedProperties` property, we can tell to Ember that mix - the content of the properties. + controller.then(function(json){ + // the json + }, function(reason) { + // the reason why you have no json + }); + ``` - In `Ember.View` the `classNameBindings` and `attributeBindings` properties - are also concatenated, in addition to `classNames`. + the controller has bindable attributes which + track the promises life cycle - This feature is available for you to use throughout the Ember object model, - although typical app developers are likely to use it infrequently. + ```javascript + controller.get('isPending') //=> true + controller.get('isSettled') //=> false + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> false + ``` - @property concatenatedProperties - @type Array + When the the $.getJSON completes, and the promise is fulfilled + with json, the life cycle attributes will update accordingly. + + ```javascript + controller.get('isPending') //=> false + controller.get('isSettled') //=> true + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> true + ``` + + As the controller is an ObjectController, and the json now its content, + all the json properties will be available directly from the controller. + + ```javascript + // Assuming the following json: + { + firstName: 'Stefan', + lastName: 'Penner' + } + + // both properties will accessible on the controller + controller.get('firstName') //=> 'Stefan' + controller.get('lastName') //=> 'Penner' + ``` + + If the controller is backing a template, the attributes are + bindable from within that template + + ```handlebars + {{#if isPending}} + loading... + {{else}} + firstName: {{firstName}} + lastName: {{lastName}} + {{/if}} + ``` + @class Ember.PromiseProxyMixin +*/ +Ember.PromiseProxyMixin = Ember.Mixin.create({ + /** + If the proxied promise is rejected this will contain the reason + provided. + + @property reason @default null */ - concatenatedProperties: null, + reason: null, /** - Destroyed object property flag. + Once the proxied promise has settled this will become `false`. - if this property is `true` the observers and bindings were already - removed by the effect of calling the `destroy()` method. + @property isPending + @default true + */ + isPending: not('isSettled').readOnly(), - @property isDestroyed + /** + Once the proxied promise has settled this will become `true`. + + @property isSettled @default false */ - isDestroyed: false, + isSettled: or('isRejected', 'isFulfilled').readOnly(), /** - Destruction scheduled flag. The `destroy()` method has been called. - - The object stays intact until the end of the run loop at which point - the `isDestroyed` flag is set. + Will become `true` if the proxied promise is rejected. - @property isDestroying + @property isRejected @default false */ - isDestroying: false, + isRejected: false, /** - Destroys an object by setting the `isDestroyed` flag and removing its - metadata, which effectively destroys observers and bindings. + Will become `true` if the proxied promise is fulfilled. - If you try to set a property on a destroyed object, an exception will be - raised. + @property isFullfilled + @default false + */ + isFulfilled: false, - Note that destruction is scheduled for the end of the run loop and does not - happen immediately. It will set an isDestroying flag immediately. + /** + The promise whose fulfillment value is being proxied by this object. - @method destroy - @return {Ember.Object} receiver - */ - destroy: function() { - if (this.isDestroying) { return; } - this.isDestroying = true; + This property must be specified upon creation, and should not be + changed once created. - schedule('actions', this, this.willDestroy); - schedule('destroy', this, this._scheduledDestroy); - return this; - }, + Example: - /** - Override to implement teardown. + ```javascript + Ember.ObjectController.extend(Ember.PromiseProxyMixin).create({ + promise: + }); + ``` - @method willDestroy - */ - willDestroy: Ember.K, + @property promise + */ + promise: Ember.computed(function(key, promise) { + if (arguments.length === 2) { + return tap(this, promise); + } else { + throw new Ember.Error("PromiseProxy's promise must be set"); + } + }), /** - @private + An alias to the proxied promise's `then`. - Invoked by the run loop to actually destroy the object. This is - scheduled for execution by the `destroy` method. + See RSVP.Promise.then. - @method _scheduledDestroy + @method then + @param {Function} callback + @return {RSVP.Promise} */ - _scheduledDestroy: function() { - if (this.isDestroyed) { return; } - destroy(this); - this.isDestroyed = true; - }, - - bind: function(to, from) { - if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } - from.to(to).connect(this); - return from; - }, + then: promiseAlias('then'), /** - Returns a string representation which attempts to provide more information - than Javascript's `toString` typically does, in a generic way for all Ember - objects. + An alias to the proxied promise's `catch`. - App.Person = Em.Object.extend() - person = App.Person.create() - person.toString() //=> "" + See RSVP.Promise.catch. - If the object's class is not defined on an Ember namespace, it will - indicate it is a subclass of the registered superclass: - - Student = App.Person.extend() - student = Student.create() - student.toString() //=> "<(subclass of App.Person):ember1025>" + @method catch + @param {Function} callback + @return {RSVP.Promise} + */ + 'catch': promiseAlias('catch'), - If the method `toStringExtension` is defined, its return value will be - included in the output. + /** + An alias to the proxied promise's `finally`. - App.Teacher = App.Person.extend({ - toStringExtension: function() { - return this.get('fullName'); - } - }); - teacher = App.Teacher.create() - teacher.toString(); //=> "" + See RSVP.Promise.finally. - @method toString - @return {String} string representation + @method finally + @param {Function} callback + @return {RSVP.Promise} */ - toString: function toString() { - var hasToStringExtension = typeof this.toStringExtension === 'function', - extension = hasToStringExtension ? ":" + this.toStringExtension() : ''; - var ret = '<'+this.constructor.toString()+':'+guidFor(this)+extension+'>'; - this.toString = makeToString(ret); - return ret; - } -}); + 'finally': promiseAlias('finally') -CoreObject.PrototypeMixin.ownerConstructor = CoreObject; +}); -function makeToString(ret) { - return function() { return ret; }; +function promiseAlias(name) { + return function () { + var promise = get(this, 'promise'); + return promise[name].apply(promise, arguments); + }; } -if (Ember.config.overridePrototypeMixin) { - Ember.config.overridePrototypeMixin(CoreObject.PrototypeMixin); -} +})(); -CoreObject.__super__ = null; -var ClassMixin = Mixin.create({ - ClassMixin: Ember.required(), +(function() { - PrototypeMixin: Ember.required(), +})(); - isClass: true, - isMethod: false, - extend: function() { - var Class = makeCtor(), proto; - Class.ClassMixin = Mixin.create(this.ClassMixin); - Class.PrototypeMixin = Mixin.create(this.PrototypeMixin); +(function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + INSERT = 'i', + DELETE = 'd'; - Class.ClassMixin.ownerConstructor = Class; - Class.PrototypeMixin.ownerConstructor = Class; +/** + An `Ember.TrackedArray` tracks array operations. It's useful when you want to + lazily compute the indexes of items in an array after they've been shifted by + subsequent operations. - reopen.apply(Class.PrototypeMixin, arguments); + @class TrackedArray + @namespace Ember + @param {array} [items=[]] The array to be tracked. This is used just to get + the initial items for the starting state of retain:n. +*/ +Ember.TrackedArray = function (items) { + if (arguments.length < 1) { items = []; } - Class.superclass = this; - Class.__super__ = this.prototype; + var length = get(items, 'length'); - proto = Class.prototype = o_create(this.prototype); - proto.constructor = Class; - generateGuid(proto, 'ember'); - meta(proto).proto = proto; // this will disable observers on prototype + if (length) { + this._operations = [new ArrayOperation(RETAIN, length, items)]; + } else { + this._operations = []; + } +}; - Class.ClassMixin.apply(Class); - return Class; - }, +Ember.TrackedArray.RETAIN = RETAIN; +Ember.TrackedArray.INSERT = INSERT; +Ember.TrackedArray.DELETE = DELETE; + +Ember.TrackedArray.prototype = { /** - Equivalent to doing `extend(arguments).create()`. - If possible use the normal `create` method instead. + Track that `newItems` were added to the tracked array at `index`. - @method createWithMixins - @static - @param [arguments]* + @method addItems + @param index + @param newItems */ - createWithMixins: function() { - var C = this; - if (arguments.length>0) { this._initMixins(arguments); } - return new C(); - }, + addItems: function (index, newItems) { + var count = get(newItems, 'length'); + if (count < 1) { return; } - /** - Creates an instance of a class. Accepts either no arguments, or an object - containing values to initialize the newly instantiated object with. + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + composeIndex, + splitIndex, + splitItems, + splitArrayOperation, + newArrayOperation; - ```javascript - App.Person = Ember.Object.extend({ - helloWorld: function() { - alert("Hi, my name is " + this.get('name')); - } - }); + newArrayOperation = new ArrayOperation(INSERT, count, newItems); - var tom = App.Person.create({ - name: 'Tom Dale' - }); + if (arrayOperation) { + if (!match.split) { + // insert left of arrayOperation + this._operations.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + } else { + // insert at end + this._operations.push(newArrayOperation); + composeIndex = arrayOperationIndex; + } - tom.helloWorld(); // alerts "Hi, my name is Tom Dale". - ``` + this._composeInsert(composeIndex); + }, - `create` will call the `init` function if defined during - `Ember.AnyObject.extend` + /** + Track that `count` items were removed at `index`. - If no arguments are passed to `create`, it will not set values to the new - instance during initialization: + @method removeItems + @param index + @param count + */ + removeItems: function (index, count) { + if (count < 1) { return; } - ```javascript - var noName = App.Person.create(); - noName.helloWorld(); // alerts undefined - ``` + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + newArrayOperation, + composeIndex; - NOTE: For performance reasons, you cannot declare methods or computed - properties during `create`. You should instead declare methods and computed - properties when using `extend` or use the `createWithMixins` shorthand. + newArrayOperation = new ArrayOperation(DELETE, count); + if (!match.split) { + // insert left of arrayOperation + this._operations.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } - @method create - @static - @param [arguments]* - */ - create: function() { - var C = this; - if (arguments.length>0) { this._initProperties(arguments); } - return new C(); + return this._composeDelete(composeIndex); }, /** - - Augments a constructor's prototype with additional - properties and functions: - - ```javascript - MyObject = Ember.Object.extend({ - name: 'an object' - }); - - o = MyObject.create(); - o.get('name'); // 'an object' - - MyObject.reopen({ - say: function(msg){ - console.log(msg); - } - }) - - o2 = MyObject.create(); - o2.say("hello"); // logs "hello" + Apply all operations, reducing them to retain:n, for `n`, the number of + items in the array. - o.say("goodbye"); // logs "goodbye" - ``` - - To add functions and properties to the constructor itself, - see `reopenClass` + `callback` will be called for each operation and will be passed the following arguments: - @method reopen - */ - reopen: function() { - this.willReopen(); - reopen.apply(this.PrototypeMixin, arguments); - return this; - }, + * {array} items The items for the given operation + * {number} offset The computed offset of the items, ie the index in the + array of the first item for this operation. + * {string} operation The type of the operation. One of + `Ember.TrackedArray.{RETAIN, DELETE, INSERT}` - /** - Augments a constructor's own properties and functions: - - ```javascript - MyObject = Ember.Object.extend({ - name: 'an object' - }); + @method apply + @param {function} callback + */ + apply: function (callback) { + var items = [], + offset = 0; + forEach(this._operations, function (arrayOperation) { + callback(arrayOperation.items, offset, arrayOperation.type); - MyObject.reopenClass({ - canBuild: false + if (arrayOperation.type !== DELETE) { + offset += arrayOperation.count; + items = items.concat(arrayOperation.items); + } }); - - MyObject.canBuild; // false - o = MyObject.create(); - ``` - - To add functions and properties to instances of - a constructor by extending the constructor's prototype - see `reopen` - - @method reopenClass - */ - reopenClass: function() { - reopen.apply(this.ClassMixin, arguments); - applyMixin(this, arguments, false); - return this; - }, - - detect: function(obj) { - if ('function' !== typeof obj) { return false; } - while(obj) { - if (obj===this) { return true; } - obj = obj.superclass; - } - return false; - }, - detectInstance: function(obj) { - return obj instanceof this; + this._operations = [new ArrayOperation(RETAIN, items.length, items)]; }, /** - In some cases, you may want to annotate computed properties with additional - metadata about how they function or what values they operate on. For - example, computed property functions may close over variables that are then - no longer available for introspection. + Return an `ArrayOperationMatch` for the operation that contains the item at `index`. - You can pass a hash of these values to a computed property like this: + @method _findArrayOperation - ```javascript - person: function() { - var personId = this.get('personId'); - return App.Person.create({ id: personId }); - }.property().meta({ type: App.Person }) - ``` + @param {number} index the index of the item whose operation information + should be returned. + @private + */ + _findArrayOperation: function (index) { + var arrayOperationIndex, + len, + split = false, + arrayOperation, + arrayOperationRangeStart, + arrayOperationRangeEnd; - Once you've done this, you can retrieve the values saved to the computed - property from your class like this: + // OPTIMIZE: we could search these faster if we kept a balanced tree. + // find leftmost arrayOperation to the right of `index` + for (arrayOperationIndex = arrayOperationRangeStart = 0, len = this._operations.length; arrayOperationIndex < len; ++arrayOperationIndex) { + arrayOperation = this._operations[arrayOperationIndex]; - ```javascript - MyClass.metaForProperty('person'); - ``` + if (arrayOperation.type === DELETE) { continue; } - This will return the original hash that was passed to `meta()`. + arrayOperationRangeEnd = arrayOperationRangeStart + arrayOperation.count - 1; - @method metaForProperty - @param key {String} property name - */ - metaForProperty: function(key) { - var desc = meta(this.proto(), false).descs[key]; + if (index === arrayOperationRangeStart) { + break; + } else if (index > arrayOperationRangeStart && index <= arrayOperationRangeEnd) { + split = true; + break; + } else { + arrayOperationRangeStart = arrayOperationRangeEnd + 1; + } + } - Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); - return desc._meta || {}; + return new ArrayOperationMatch(arrayOperation, arrayOperationIndex, split, arrayOperationRangeStart); }, - /** - Iterate over each computed property for the class, passing its name - and any associated metadata (see `metaForProperty`) to the callback. + _split: function (arrayOperationIndex, splitIndex, newArrayOperation) { + var arrayOperation = this._operations[arrayOperationIndex], + splitItems = arrayOperation.items.slice(splitIndex), + splitArrayOperation = new ArrayOperation(arrayOperation.type, splitItems.length, splitItems); - @method eachComputedProperty - @param {Function} callback - @param {Object} binding - */ - eachComputedProperty: function(callback, binding) { - var proto = this.proto(), - descs = meta(proto).descs, - empty = {}, - property; + // truncate LHS + arrayOperation.count = splitIndex; + arrayOperation.items = arrayOperation.items.slice(0, splitIndex); - for (var name in descs) { - property = descs[name]; + this._operations.splice(arrayOperationIndex + 1, 0, newArrayOperation, splitArrayOperation); + }, - if (property instanceof Ember.ComputedProperty) { - callback.call(binding || this, name, property._meta || empty); + // see SubArray for a better implementation. + _composeInsert: function (index) { + var newArrayOperation = this._operations[index], + leftArrayOperation = this._operations[index-1], // may be undefined + rightArrayOperation = this._operations[index+1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.type, + rightOp = rightArrayOperation && rightArrayOperation.type; + + if (leftOp === INSERT) { + // merge left + leftArrayOperation.count += newArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(newArrayOperation.items); + + if (rightOp === INSERT) { + // also merge right (we have split an insert with an insert) + leftArrayOperation.count += rightArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(rightArrayOperation.items); + this._operations.splice(index, 2); + } else { + // only merge left + this._operations.splice(index, 1); } + } else if (rightOp === INSERT) { + // merge right + newArrayOperation.count += rightArrayOperation.count; + newArrayOperation.items = newArrayOperation.items.concat(rightArrayOperation.items); + this._operations.splice(index + 1, 1); } - } + }, -}); + _composeDelete: function (index) { + var arrayOperation = this._operations[index], + deletesToGo = arrayOperation.count, + leftArrayOperation = this._operations[index-1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.type, + nextArrayOperation, + nextOp, + nextCount, + removeNewAndNextOp = false, + removedItems = []; -ClassMixin.ownerConstructor = CoreObject; + if (leftOp === DELETE) { + arrayOperation = leftArrayOperation; + index -= 1; + } -if (Ember.config.overrideClassMixin) { - Ember.config.overrideClassMixin(ClassMixin); -} + for (var i = index + 1; deletesToGo > 0; ++i) { + nextArrayOperation = this._operations[i]; + nextOp = nextArrayOperation.type; + nextCount = nextArrayOperation.count; -CoreObject.ClassMixin = ClassMixin; -ClassMixin.apply(CoreObject); + if (nextOp === DELETE) { + arrayOperation.count += nextCount; + continue; + } -Ember.CoreObject = CoreObject; + if (nextCount > deletesToGo) { + // d:2 {r,i}:5 we reduce the retain or insert, but it stays + removedItems = removedItems.concat(nextArrayOperation.items.splice(0, deletesToGo)); + nextArrayOperation.count -= deletesToGo; -})(); + // In the case where we truncate the last arrayOperation, we don't need to + // remove it; also the deletesToGo reduction is not the entirety of + // nextCount + i -= 1; + nextCount = deletesToGo; + + deletesToGo = 0; + } else { + if (nextCount === deletesToGo) { + // Handle edge case of d:2 i:2 in which case both operations go away + // during composition. + removeNewAndNextOp = true; + } + removedItems = removedItems.concat(nextArrayOperation.items); + deletesToGo -= nextCount; + } + if (nextOp === INSERT) { + // d:2 i:3 will result in delete going away + arrayOperation.count -= nextCount; + } + } + if (arrayOperation.count > 0) { + // compose our new delete with possibly several operations to the right of + // disparate types + this._operations.splice(index+1, i-1-index); + } else { + // The delete operation can go away; it has merely reduced some other + // operation, as in d:3 i:4; it may also have eliminated that operation, + // as in d:3 i:3. + this._operations.splice(index, removeNewAndNextOp ? 2 : 1); + } + + return removedItems; + }, + + toString: function () { + var str = ""; + forEach(this._operations, function (operation) { + str += " " + operation.type + ":" + operation.count; + }); + return str.substring(1); + } +}; -(function() { /** -@module ember -@submodule ember-runtime + Internal data structure to represent an array operation. + + @method ArrayOperation + @private + @param {string} type The type of the operation. One of + `Ember.TrackedArray.{RETAIN, INSERT, DELETE}` + @param {number} count The number of items in this operation. + @param {array} items The items of the operation, if included. RETAIN and + INSERT include their items, DELETE does not. */ +function ArrayOperation (operation, count, items) { + this.type = operation; // RETAIN | INSERT | DELETE + this.count = count; + this.items = items; +} /** - `Ember.Object` is the main base class for all Ember objects. It is a subclass - of `Ember.CoreObject` with the `Ember.Observable` mixin applied. For details, - see the documentation for each of these. + Internal data structure used to include information when looking up operations + by item index. - @class Object - @namespace Ember - @extends Ember.CoreObject - @uses Ember.Observable + @method ArrayOperationMatch + @private + @param {ArrayOperation} operation + @param {number} index The index of `operation` in the array of operations. + @param {boolean} split Whether or not the item index searched for would + require a split for a new operation type. + @param {number} rangeStart The index of the first item in the operation, + with respect to the tracked array. The index of the last item can be computed + from `rangeStart` and `operation.count`. */ -Ember.Object = Ember.CoreObject.extend(Ember.Observable); -Ember.Object.toString = function() { return "Ember.Object"; }; +function ArrayOperationMatch(operation, index, split, rangeStart) { + this.operation = operation; + this.index = index; + this.split = split; + this.rangeStart = rangeStart; +} })(); (function() { -/** -@module ember -@submodule ember-runtime -*/ +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + FILTER = 'f'; -var get = Ember.get, indexOf = Ember.ArrayPolyfills.indexOf; +function Operation (type, count) { + this.type = type; + this.count = count; +} /** - A Namespace is an object usually used to contain other objects or methods - such as an application or framework. Create a namespace anytime you want - to define one of these new containers. - - # Example Usage - - ```javascript - MyFramework = Ember.Namespace.create({ - VERSION: '1.0.0' - }); - ``` + An `Ember.SubArray` tracks an array in a way similar to, but more specialized + than, `Ember.TrackedArray`. It is useful for keeping track of the indexes of + items within a filtered array. - @class Namespace + @class SubArray @namespace Ember - @extends Ember.Object */ -var Namespace = Ember.Namespace = Ember.Object.extend({ - isNamespace: true, - - init: function() { - Ember.Namespace.NAMESPACES.push(this); - Ember.Namespace.PROCESSED = false; - }, +Ember.SubArray = function (length) { + if (arguments.length < 1) { length = 0; } - toString: function() { - var name = get(this, 'name'); - if (name) { return name; } + if (length > 0) { + this._operations = [new Operation(RETAIN, length)]; + } else { + this._operations = []; + } +}; - findNamespaces(); - return this[Ember.GUID_KEY+'_name']; - }, +Ember.SubArray.prototype = { + /** + Track that an item was added to the tracked array. - nameClasses: function() { - processNamespace([this.toString()], this, {}); - }, + @method addItem - destroy: function() { - var namespaces = Ember.Namespace.NAMESPACES; - Ember.lookup[this.toString()] = undefined; - namespaces.splice(indexOf.call(namespaces, this), 1); - this._super(); - } -}); + @param {number} index The index of the item in the tracked array. + @param {boolean} match `true` iff the item is included in the subarray. -Namespace.reopenClass({ - NAMESPACES: [Ember], - NAMESPACES_BY_ID: {}, - PROCESSED: false, - processAll: processAllNamespaces, - byName: function(name) { - if (!Ember.BOOTED) { - processAllNamespaces(); - } + @return {number} The index of the item in the subarray. + */ + addItem: function(index, match) { + var returnValue = -1, + itemType = match ? RETAIN : FILTER, + self = this; - return NAMESPACES_BY_ID[name]; - } -}); + this._findOperation(index, function(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + var newOperation, splitOperation; -var NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID; + if (itemType === operation.type) { + ++operation.count; + } else if (index === rangeStart) { + // insert to the left of `operation` + self._operations.splice(operationIndex, 0, new Operation(itemType, 1)); + } else { + newOperation = new Operation(itemType, 1); + splitOperation = new Operation(operation.type, rangeEnd - index + 1); + operation.count = index - rangeStart; -var hasOwnProp = ({}).hasOwnProperty, - guidFor = Ember.guidFor; + self._operations.splice(operationIndex + 1, 0, newOperation, splitOperation); + } -function processNamespace(paths, root, seen) { - var idx = paths.length; + if (match) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } else { + returnValue = seenInSubArray; + } + } - NAMESPACES_BY_ID[paths.join('.')] = root; + self._composeAt(operationIndex); + }, function(seenInSubArray) { + self._operations.push(new Operation(itemType, 1)); - // Loop over all of the keys in the namespace, looking for classes - for(var key in root) { - if (!hasOwnProp.call(root, key)) { continue; } - var obj = root[key]; + if (match) { + returnValue = seenInSubArray; + } - // If we are processing the `Ember` namespace, for example, the - // `paths` will start with `["Ember"]`. Every iteration through - // the loop will update the **second** element of this list with - // the key, so processing `Ember.View` will make the Array - // `['Ember', 'View']`. - paths[idx] = key; + self._composeAt(self._operations.length-1); + }); - // If we have found an unprocessed class - if (obj && obj.toString === classToString) { - // Replace the class' `toString` with the dot-separated path - // and set its `NAME_KEY` - obj.toString = makeToString(paths.join('.')); - obj[NAME_KEY] = paths.join('.'); + return returnValue; + }, - // Support nested namespaces - } else if (obj && obj.isNamespace) { - // Skip aliased namespaces - if (seen[guidFor(obj)]) { continue; } - seen[guidFor(obj)] = true; + /** + Track that an item was removed from the tracked array. - // Process the child namespace - processNamespace(paths, obj, seen); - } - } + @method removeItem - paths.length = idx; // cut out last item -} + @param {number} index The index of the item in the tracked array. -function findNamespaces() { - var Namespace = Ember.Namespace, lookup = Ember.lookup, obj, isNamespace; + @return {number} The index of the item in the subarray, or `-1` if the item + was not in the subarray. + */ + removeItem: function(index) { + var returnValue = -1, + self = this; - if (Namespace.PROCESSED) { return; } + this._findOperation(index, function (operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } - for (var prop in lookup) { - // These don't raise exceptions but can cause warnings - if (prop === "parent" || prop === "top" || prop === "frameElement" || prop === "webkitStorageInfo") { continue; } + if (operation.count > 1) { + --operation.count; + } else { + self._operations.splice(operationIndex, 1); + self._composeAt(operationIndex); + } + }, function() { + throw new Ember.Error("Can't remove an item that has never been added."); + }); - // get(window.globalStorage, 'isNamespace') would try to read the storage for domain isNamespace and cause exception in Firefox. - // globalStorage is a storage obsoleted by the WhatWG storage specification. See https://developer.mozilla.org/en/DOM/Storage#globalStorage - if (prop === "globalStorage" && lookup.StorageList && lookup.globalStorage instanceof lookup.StorageList) { continue; } - // Unfortunately, some versions of IE don't support window.hasOwnProperty - if (lookup.hasOwnProperty && !lookup.hasOwnProperty(prop)) { continue; } + return returnValue; + }, - // At times we are not allowed to access certain properties for security reasons. - // There are also times where even if we can access them, we are not allowed to access their properties. - try { - obj = Ember.lookup[prop]; - isNamespace = obj && obj.isNamespace; - } catch (e) { - continue; - } - if (isNamespace) { - Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); - obj[NAME_KEY] = prop; - } - } -} + _findOperation: function (index, foundCallback, notFoundCallback) { + var operationIndex, + len, + operation, + rangeStart, + rangeEnd, + seenInSubArray = 0; -var NAME_KEY = Ember.NAME_KEY = Ember.GUID_KEY + '_name'; + // OPTIMIZE: change to balanced tree + // find leftmost operation to the right of `index` + for (operationIndex = rangeStart = 0, len = this._operations.length; operationIndex < len; rangeStart = rangeEnd + 1, ++operationIndex) { + operation = this._operations[operationIndex]; + rangeEnd = rangeStart + operation.count - 1; -function superClassString(mixin) { - var superclass = mixin.superclass; - if (superclass) { - if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; } - else { return superClassString(superclass); } - } else { - return; - } -} + if (index >= rangeStart && index <= rangeEnd) { + foundCallback(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray); + return; + } else if (operation.type === RETAIN) { + seenInSubArray += operation.count; + } + } -function classToString() { - if (!Ember.BOOTED && !this[NAME_KEY]) { - processAllNamespaces(); - } + notFoundCallback(seenInSubArray); + }, - var ret; + _composeAt: function(index) { + var op = this._operations[index], + otherOp; - if (this[NAME_KEY]) { - ret = this[NAME_KEY]; - } else if (this._toString) { - ret = this._toString; - } else { - var str = superClassString(this); - if (str) { - ret = "(subclass of " + str + ")"; - } else { - ret = "(unknown mixin)"; + if (!op) { + // Composing out of bounds is a no-op, as when removing the last operation + // in the list. + return; } - this.toString = makeToString(ret); - } - return ret; -} + if (index > 0) { + otherOp = this._operations[index-1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index-1, 1); + --index; + } + } -function processAllNamespaces() { - var unprocessedNamespaces = !Namespace.PROCESSED, - unprocessedMixins = Ember.anyUnprocessedMixins; + if (index < this._operations.length-1) { + otherOp = this._operations[index+1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index+1, 1); + } + } + }, - if (unprocessedNamespaces) { - findNamespaces(); - Namespace.PROCESSED = true; + toString: function () { + var str = ""; + forEach(this._operations, function (operation) { + str += " " + operation.type + ":" + operation.count; + }); + return str.substring(1); } +}; - if (unprocessedNamespaces || unprocessedMixins) { - var namespaces = Namespace.NAMESPACES, namespace; - for (var i=0, l=namespaces.length; i get(this, 'content.length')) throw new Error(OUT_OF_RANGE_EXCEPTION); + if (idx > get(this, 'content.length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); this._replace(idx, 0, [object]); return this; }, @@ -16315,7 +19352,7 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array indices = [], i; if ((start < 0) || (start >= get(this, 'length'))) { - throw new Error(OUT_OF_RANGE_EXCEPTION); + throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); } if (len === undefined) len = 1; @@ -16399,159 +19436,6 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var get = Ember.get, - set = Ember.set, - fmt = Ember.String.fmt, - addBeforeObserver = Ember.addBeforeObserver, - addObserver = Ember.addObserver, - removeBeforeObserver = Ember.removeBeforeObserver, - removeObserver = Ember.removeObserver, - propertyWillChange = Ember.propertyWillChange, - propertyDidChange = Ember.propertyDidChange, - meta = Ember.meta, - defineProperty = Ember.defineProperty; - -function contentPropertyWillChange(content, contentKey) { - var key = contentKey.slice(8); // remove "content." - if (key in this) { return; } // if shadowed in proxy - propertyWillChange(this, key); -} - -function contentPropertyDidChange(content, contentKey) { - var key = contentKey.slice(8); // remove "content." - if (key in this) { return; } // if shadowed in proxy - propertyDidChange(this, key); -} - -/** - `Ember.ObjectProxy` forwards all properties not defined by the proxy itself - to a proxied `content` object. - - ```javascript - object = Ember.Object.create({ - name: 'Foo' - }); - - proxy = Ember.ObjectProxy.create({ - content: object - }); - - // Access and change existing properties - proxy.get('name') // 'Foo' - proxy.set('name', 'Bar'); - object.get('name') // 'Bar' - - // Create new 'description' property on `object` - proxy.set('description', 'Foo is a whizboo baz'); - object.get('description') // 'Foo is a whizboo baz' - ``` - - While `content` is unset, setting a property to be delegated will throw an - Error. - - ```javascript - proxy = Ember.ObjectProxy.create({ - content: null, - flag: null - }); - proxy.set('flag', true); - proxy.get('flag'); // true - proxy.get('foo'); // undefined - proxy.set('foo', 'data'); // throws Error - ``` - - Delegated properties can be bound to and will change when content is updated. - - Computed properties on the proxy itself can depend on delegated properties. - - ```javascript - ProxyWithComputedProperty = Ember.ObjectProxy.extend({ - fullName: function () { - var firstName = this.get('firstName'), - lastName = this.get('lastName'); - if (firstName && lastName) { - return firstName + ' ' + lastName; - } - return firstName || lastName; - }.property('firstName', 'lastName') - }); - - proxy = ProxyWithComputedProperty.create(); - - proxy.get('fullName'); // undefined - proxy.set('content', { - firstName: 'Tom', lastName: 'Dale' - }); // triggers property change for fullName on proxy - - proxy.get('fullName'); // 'Tom Dale' - ``` - - @class ObjectProxy - @namespace Ember - @extends Ember.Object -*/ -Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype */ { - /** - The object whose properties will be forwarded. - - @property content - @type Ember.Object - @default null - */ - content: null, - _contentDidChange: Ember.observer(function() { - Ember.assert("Can't set ObjectProxy's content to itself", this.get('content') !== this); - }, 'content'), - - isTruthy: Ember.computed.bool('content'), - - _debugContainerKey: null, - - willWatchProperty: function (key) { - var contentKey = 'content.' + key; - addBeforeObserver(this, contentKey, null, contentPropertyWillChange); - addObserver(this, contentKey, null, contentPropertyDidChange); - }, - - didUnwatchProperty: function (key) { - var contentKey = 'content.' + key; - removeBeforeObserver(this, contentKey, null, contentPropertyWillChange); - removeObserver(this, contentKey, null, contentPropertyDidChange); - }, - - unknownProperty: function (key) { - var content = get(this, 'content'); - if (content) { - return get(content, key); - } - }, - - setUnknownProperty: function (key, value) { - var m = meta(this); - if (m.proto === this) { - // if marked as prototype then just defineProperty - // rather than delegate - defineProperty(this, key, null, value); - return value; - } - - var content = get(this, 'content'); - Ember.assert(fmt("Cannot delegate set('%@', %@) to the 'content' property of object proxy %@: its 'content' is undefined.", [key, value, this]), content); - return set(content, key, value); - } - -}); - -})(); - - - (function() { /** @module ember @@ -16798,7 +19682,7 @@ var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember var len = objects ? get(objects, 'length') : 0; this.arrayContentWillChange(idx, amt, len); - if (!objects || objects.length === 0) { + if (len === 0) { this.splice(idx, amt); } else { replace(this, idx, amt, objects); @@ -16885,7 +19769,7 @@ Ember.NativeArray = NativeArray; Does not modify the original object. Ember.A is not needed if `Ember.EXTEND_PROTOTYPES` is `true` (the default value). However, it is recommended that you use Ember.A when creating addons for - ember or when you can not garentee that `Ember.EXTEND_PROTOTYPES` + ember or when you can not guarantee that `Ember.EXTEND_PROTOTYPES` will be `true`. Example @@ -17052,7 +19936,7 @@ var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.is @since Ember 0.9 */ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Ember.Freezable, - /** @scope Ember.Set.prototype */ { + { // .......................................................... // IMPLEMENT ENUMERABLE APIS @@ -17082,7 +19966,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb @return {Ember.Set} An empty Set */ clear: function() { - if (this.isFrozen) { throw new Error(Ember.FROZEN_ERROR); } + if (this.isFrozen) { throw new Ember.Error(Ember.FROZEN_ERROR); } var len = get(this, 'length'); if (len === 0) { return this; } @@ -17192,7 +20076,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb @return {Object} The removed object from the set or null. */ pop: function() { - if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); var obj = this.length > 0 ? this[this.length-1] : null; this.remove(obj); return obj; @@ -17309,7 +20193,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb // implements Ember.MutableEnumerable addObject: function(obj) { - if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), @@ -17337,7 +20221,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb // implements Ember.MutableEnumerable removeObject: function(obj) { - if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), @@ -17427,32 +20311,30 @@ Ember.Deferred = Deferred; var forEach = Ember.ArrayPolyfills.forEach; /** -@module ember -@submodule ember-runtime + @module ember + @submodule ember-runtime */ var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; var loaded = {}; /** + Detects when a specific package of Ember (e.g. 'Ember.Handlebars') + has fully loaded and is available for extension. -Detects when a specific package of Ember (e.g. 'Ember.Handlebars') -has fully loaded and is available for extension. - -The provided `callback` will be called with the `name` passed -resolved from a string into the object: - -```javascript -Ember.onLoad('Ember.Handlebars' function(hbars){ - hbars.registerHelper(...); -}); -``` + The provided `callback` will be called with the `name` passed + resolved from a string into the object: + ``` javascript + Ember.onLoad('Ember.Handlebars' function(hbars){ + hbars.registerHelper(...); + }); + ``` -@method onLoad -@for Ember -@param name {String} name of hook -@param callback {Function} callback to be called + @method onLoad + @for Ember + @param name {String} name of hook + @param callback {Function} callback to be called */ Ember.onLoad = function(name, callback) { var object; @@ -17466,14 +20348,13 @@ Ember.onLoad = function(name, callback) { }; /** + Called when an Ember.js package (e.g Ember.Handlebars) has finished + loading. Triggers any callbacks registered for this event. -Called when an Ember.js package (e.g Ember.Handlebars) has finished -loading. Triggers any callbacks registered for this event. - -@method runLoadHooks -@for Ember -@param name {String} name of hook -@param object {Object} object to pass to callbacks + @method runLoadHooks + @for Ember + @param name {String} name of hook + @param object {Object} object to pass to callbacks */ Ember.runLoadHooks = function(name, object) { loaded[name] = object; @@ -17510,6 +20391,7 @@ var get = Ember.get; @class ControllerMixin @namespace Ember + @uses Ember.ActionHandler */ Ember.ControllerMixin = Ember.Mixin.create(Ember.ActionHandler, { /* ducktype as a controller */ @@ -17544,7 +20426,7 @@ Ember.ControllerMixin = Ember.Mixin.create(Ember.ActionHandler, { deprecatedSend: function(actionName) { var args = [].slice.call(arguments, 1); Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); - Ember.deprecate('Action handlers implemented directly on controllers are deprecated in favor of action handlers on an `actions` object (' + actionName + ' on ' + this + ')', false); + Ember.deprecate('Action handlers implemented directly on controllers are deprecated in favor of action handlers on an `actions` object ( action: `' + actionName + '` on ' + this + ')', false); this[actionName].apply(this, args); return; } @@ -17729,7 +20611,7 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { return content; }), - _contentWillChange: Ember.beforeObserver(function() { + _contentWillChange: Ember.beforeObserver('content', function() { var content = get(this, 'content'), sortProperties = get(this, 'sortProperties'); @@ -17742,18 +20624,18 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { } this._super(); - }, 'content'), + }), - sortAscendingWillChange: Ember.beforeObserver(function() { + sortAscendingWillChange: Ember.beforeObserver('sortAscending', function() { this._lastSortAscending = get(this, 'sortAscending'); - }, 'sortAscending'), + }), - sortAscendingDidChange: Ember.observer(function() { + sortAscendingDidChange: Ember.observer('sortAscending', function() { if (get(this, 'sortAscending') !== this._lastSortAscending) { var arrangedContent = get(this, 'arrangedContent'); arrangedContent.reverseObjects(); } - }, 'sortAscending'), + }), contentArrayWillChange: function(array, idx, removedCount, addedCount) { var isSorted = get(this, 'isSorted'); @@ -17931,8 +20813,7 @@ var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach, ``` The itemController instances will have a `parentController` property set to - either the the `parentController` property of the `ArrayController` - or to the `ArrayController` instance itself. + the `ArrayController` instance. @class ArrayController @namespace Ember @@ -18033,6 +20914,15 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, return Ember.A(); }), + /** + * Flag to mark as being "virtual". Used to keep this instance + * from participating in the parentController hierarchy. + * + * @private + * @type Boolean + */ + _isVirtual: false, + controllerAt: function(idx, object, controllerClass) { var container = get(this, 'container'), subControllers = get(this, '_subControllers'), @@ -18044,12 +20934,16 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, fullName = "controller:" + controllerClass; if (!container.has(fullName)) { - throw new Error('Could not resolve itemController: "' + controllerClass + '"'); + throw new Ember.Error('Could not resolve itemController: "' + controllerClass + '"'); } - + var parentController; + if (this._isVirtual) { + parentController = get(this, 'parentController'); + } + parentController = parentController || this; subController = container.lookupFactory(fullName).create({ target: this, - parentController: get(this, 'parentController') || this, + parentController: parentController, content: object }); @@ -18124,8 +21018,12 @@ Ember Runtime @submodule ember-views */ -var jQuery = Ember.imports.jQuery; -Ember.assert("Ember Views require jQuery 1.7, 1.8, 1.9, 1.10, or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(7|8|9|10))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); +var jQuery = (this && this.jQuery) || (Ember.imports && Ember.imports.jQuery); +if (!jQuery && typeof require === 'function') { + jQuery = require('jquery'); +} + +Ember.assert("Ember Views require jQuery between 1.7 and 2.1", jQuery && (jQuery().jquery.match(/^((1\.(7|8|9|10|11))|(2\.(0|1)))(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); /** Alias for jQuery @@ -18171,7 +21069,7 @@ if (Ember.$) { // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ -var needsShy = this.document && (function() { +var needsShy = typeof document !== 'undefined' && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "
"; testEl.firstChild.innerHTML = ""; @@ -18181,7 +21079,7 @@ var needsShy = this.document && (function() { // IE 8 (and likely earlier) likes to move whitespace preceeding // a script tag to appear after it. This means that we can // accidentally remove whitespace when updating a morph. -var movesWhitespace = this.document && (function() { +var movesWhitespace = typeof document !== 'undefined' && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "Test: Value"; return testEl.childNodes[0].nodeValue === 'Test:' && @@ -18366,6 +21264,19 @@ function escapeAttribute(value) { return string.replace(BAD_CHARS_REGEXP, escapeChar); } +// IE 6/7 have bugs around setting names on inputs during creation. +// From http://msdn.microsoft.com/en-us/library/ie/ms536389(v=vs.85).aspx: +// "To include the NAME attribute at run time on objects created with the createElement method, use the eTag." +var canSetNameOnInputs = (function() { + var div = document.createElement('div'), + el = document.createElement('input'); + + el.setAttribute('name', 'foo'); + div.appendChild(el); + + return !!div.innerHTML.match('foo'); +})(); + /** `Ember.RenderBuffer` gathers information regarding the a view and generates the final representation. `Ember.RenderBuffer` will generate HTML which can be pushed @@ -18389,8 +21300,7 @@ Ember._RenderBuffer = function(tagName) { this.buffer = ""; }; -Ember._RenderBuffer.prototype = -/** @scope Ember.RenderBuffer.prototype */ { +Ember._RenderBuffer.prototype = { // The root view's element _element: null, @@ -18398,12 +21308,11 @@ Ember._RenderBuffer.prototype = _hasElement: true, /** - @private - An internal set used to de-dupe class names when `addClass()` is used. After each call to `addClass()`, the `classes` property will be updated. + @private @property elementClasses @type Array @default [] @@ -18533,7 +21442,11 @@ Ember._RenderBuffer.prototype = }, setClasses: function(classNames) { - this.classes = classNames; + this.elementClasses = null; + var len = classNames.length, i; + for (i = 0; i < len; i++) { + this.addClass(classNames[i]); + } }, /** @@ -18587,7 +21500,7 @@ Ember._RenderBuffer.prototype = }, /** - Adds an property which will be rendered to the element. + Adds a property which will be rendered to the element. @method prop @param {String} name The name of the property @@ -18667,6 +21580,7 @@ Ember._RenderBuffer.prototype = if (classes) { buffer += ' class="' + escapeAttribute(classes.join(' ')) + '"'; this.classes = null; + this.elementClasses = null; } if (style) { @@ -18725,14 +21639,22 @@ Ember._RenderBuffer.prototype = generateElement: function() { var tagName = this.tagNames.pop(), // pop since we don't need to close - element = document.createElement(tagName), - $element = Ember.$(element), id = this.elementId, classes = this.classes, attrs = this.elementAttributes, props = this.elementProperties, style = this.elementStyle, - styleBuffer = '', attr, prop; + styleBuffer = '', attr, prop, tagString; + + if (attrs && attrs.name && !canSetNameOnInputs) { + // IE allows passing a tag to createElement. See note on `canSetNameOnInputs` above as well. + tagString = '<'+stripTagName(tagName)+' name="'+escapeAttribute(attrs.name)+'">'; + } else { + tagString = tagName; + } + + var element = document.createElement(tagString), + $element = Ember.$(element); if (id) { $element.attr('id', id); @@ -18741,6 +21663,7 @@ Ember._RenderBuffer.prototype = if (classes) { $element.attr('class', classes.join(' ')); this.classes = null; + this.elementClasses = null; } if (style) { @@ -18840,7 +21763,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; @private @extends Ember.Object */ -Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.prototype */{ +Ember.EventDispatcher = Ember.Object.extend({ /** The set of events names (and associated handler function names) to be setup @@ -18884,8 +21807,6 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro }, /** - @private - The root DOM element to which event listeners should be attached. Event listeners will be attached to the document unless this is overridden. @@ -18894,6 +21815,7 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro The default body is a string since this may be evaluated before document.body exists in the DOM. + @private @property rootElement @type DOMElement @default 'body' @@ -18901,8 +21823,6 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro rootElement: 'body', /** - @private - Sets up event listeners for standard browser events. This will be called after the browser sends a `DOMContentReady` event. By @@ -18910,6 +21830,7 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro would like to register the listeners on a different element, set the event dispatcher's `root` property. + @private @method setup @param addedEvents {Hash} */ @@ -18941,8 +21862,6 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro }, /** - @private - Registers an event listener on the document. If the given event is triggered, the provided event handler will be triggered on the target view. @@ -18957,6 +21876,7 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro setupHandler('mousedown', 'mouseDown'); ``` + @private @method setupHandler @param {Element} rootElement @param {String} event the browser-originated event to listen to @@ -18966,7 +21886,7 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro var self = this; rootElement.on(event + '.ember', '.ember-view', function(evt, triggeringManager) { - return Ember.handleErrors(function() { + return Ember.handleErrors(function handleViewEvent() { var view = Ember.View.views[this.id], result = true, manager = null; @@ -18985,7 +21905,7 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro }); rootElement.on(event + '.ember', '[data-ember-action]', function(evt) { - return Ember.handleErrors(function() { + return Ember.handleErrors(function handleActionEvent() { var actionId = Ember.$(evt.currentTarget).attr('data-ember-action'), action = Ember.Handlebars.ActionHelper.registeredActions[actionId]; @@ -19031,7 +21951,7 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro }, _bubbleEvent: function(view, evt, eventName) { - return Ember.run(function() { + return Ember.run(function bubbleEvent() { return view.handleEvent(eventName, evt); }); }, @@ -19093,7 +22013,7 @@ Ember.ControllerMixin.reopen({ set(this, '_childContainers', {}); }, - _modelDidChange: Ember.observer(function() { + _modelDidChange: Ember.observer('model', function() { var containers = get(this, '_childContainers'); for (var prop in containers) { @@ -19102,7 +22022,7 @@ Ember.ControllerMixin.reopen({ } set(this, '_childContainers', {}); - }, 'model') + }) }); })(); @@ -19127,6 +22047,7 @@ var get = Ember.get, set = Ember.set; var guidFor = Ember.guidFor; var a_forEach = Ember.EnumerableUtils.forEach; var a_addObject = Ember.EnumerableUtils.addObject; +var meta = Ember.meta; var childViewsProperty = Ember.computed(function() { var childViews = this._childViews, ret = Ember.A(), view = this; @@ -19147,7 +22068,7 @@ var childViewsProperty = Ember.computed(function() { Ember.deprecate("Manipulating an Ember.ContainerView through its childViews property is deprecated. Please use the ContainerView instance itself as an Ember.MutableArray."); return view.replace(idx, removedCount, addedViews); } - throw new Error("childViews is immutable"); + throw new Ember.Error("childViews is immutable"); }; return ret; @@ -19179,6 +22100,7 @@ Ember.TEMPLATES = {}; @namespace Ember @extends Ember.Object @uses Ember.Evented + @uses Ember.ActionHandler */ Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { @@ -19226,8 +22148,6 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { }, /** - @private - Invoked by the view system when this view needs to produce an HTML representation. This method will create a new render buffer, if needed, then apply any default attributes, such as class names and visibility. @@ -19242,6 +22162,7 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { @param {Ember.RenderBuffer} buffer the render buffer. If no buffer is passed, a default buffer, using the current view's `tagName`, will be used. + @private */ renderToBuffer: function(parentBuffer, bufferOperation) { var name = 'render.' + this.instrumentName, @@ -19249,7 +22170,7 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { this.instrumentDetails(details); - return Ember.instrument(name, details, function() { + return Ember.instrument(name, details, function instrumentRenderToBuffer() { return this._renderToBuffer(parentBuffer, bufferOperation); }, this); }, @@ -19276,13 +22197,12 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { }, /** - @private - Override the default event firing from `Ember.Evented` to also call methods with the given name. @method trigger @param name {String} + @private */ trigger: function(name) { this._super.apply(this, arguments); @@ -19303,7 +22223,7 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { deprecatedSend: function(actionName) { var args = [].slice.call(arguments, 1); Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); - Ember.deprecate('Action handlers implemented directly on views are deprecated in favor of action handlers on an `actions` object (' + actionName + ' on ' + this + ')', false); + Ember.deprecate('Action handlers implemented directly on views are deprecated in favor of action handlers on an `actions` object ( action: `' + actionName + '` on ' + this + ')', false); this[actionName].apply(this, args); return; }, @@ -19684,6 +22604,22 @@ var EMPTY_ARRAY = []; }); ``` + If you have nested resources, your Handlebars template will look like this: + + ```html + + ``` + + And `templateName` property: + + ```javascript + AView = Ember.View.extend({ + templateName: 'posts/new' + }); + ``` + Using a value for `templateName` that does not have a Handlebars template with a matching `data-template-name` attribute will throw an error. @@ -19952,8 +22888,7 @@ var EMPTY_ARRAY = []; @namespace Ember @extends Ember.CoreView */ -Ember.View = Ember.CoreView.extend( -/** @scope Ember.View.prototype */ { +Ember.View = Ember.CoreView.extend({ concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'], @@ -19961,7 +22896,7 @@ Ember.View = Ember.CoreView.extend( @property isView @type Boolean @default true - @final + @static */ isView: true, @@ -19972,9 +22907,8 @@ Ember.View = Ember.CoreView.extend( /** The name of the template to lookup if no template is provided. - `Ember.View` will look for a template with this name in this view's - `templates` object. By default, this will be a global object - shared in `Ember.TEMPLATES`. + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). @property templateName @type String @@ -19985,9 +22919,8 @@ Ember.View = Ember.CoreView.extend( /** The name of the layout to lookup if no layout is provided. - `Ember.View` will look for a template with this name in this view's - `templates` object. By default, this will be a global object - shared in `Ember.TEMPLATES`. + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). @property layoutName @type String @@ -19995,15 +22928,6 @@ Ember.View = Ember.CoreView.extend( */ layoutName: null, - /** - The hash in which to look for `templateName`. - - @property templates - @type Ember.Object - @default Ember.TEMPLATES - */ - templates: Ember.TEMPLATES, - /** The template used to render the view. This should be a function that accepts an optional context parameter and returns a string of HTML that @@ -20097,8 +23021,6 @@ Ember.View = Ember.CoreView.extend( }).volatile(), /** - @private - Private copy of the view's template context. This can be set directly by Handlebars without triggering the observer that causes the view to be re-rendered. @@ -20114,6 +23036,7 @@ Ember.View = Ember.CoreView.extend( something of a hack and should be revisited. @property _context + @private */ _context: Ember.computed(function(key) { var parentView, controller; @@ -20131,16 +23054,15 @@ Ember.View = Ember.CoreView.extend( }), /** - @private - If a value that affects template rendering changes, the view should be re-rendered to reflect the new value. @method _contextDidChange + @private */ - _contextDidChange: Ember.observer(function() { + _contextDidChange: Ember.observer('context', function() { this.rerender(); - }, 'context'), + }), /** If `false`, the view will appear hidden in DOM. @@ -20152,14 +23074,13 @@ Ember.View = Ember.CoreView.extend( isVisible: true, /** - @private - Array of child views. You should never edit this array directly. Instead, use `appendChild` and `removeFromParent`. @property childViews @type Array @default [] + @private */ childViews: childViewsProperty, @@ -20167,21 +23088,21 @@ Ember.View = Ember.CoreView.extend( // When it's a virtual view, we need to notify the parent that their // childViews will change. - _childViewsWillChange: Ember.beforeObserver(function() { + _childViewsWillChange: Ember.beforeObserver('childViews', function() { if (this.isVirtual) { var parentView = get(this, 'parentView'); if (parentView) { Ember.propertyWillChange(parentView, 'childViews'); } } - }, 'childViews'), + }), // When it's a virtual view, we need to notify the parent that their // childViews did change. - _childViewsDidChange: Ember.observer(function() { + _childViewsDidChange: Ember.observer('childViews', function() { if (this.isVirtual) { var parentView = get(this, 'parentView'); if (parentView) { Ember.propertyDidChange(parentView, 'childViews'); } } - }, 'childViews'), + }), /** Return the nearest ancestor that is an instance of the provided @@ -20226,7 +23147,7 @@ Ember.View = Ember.CoreView.extend( /** Return the nearest ancestor that has a given property. - @property nearestWithProperty + @function nearestWithProperty @param {String} property A property name @return Ember.View */ @@ -20243,7 +23164,7 @@ Ember.View = Ember.CoreView.extend( Return the nearest ancestor whose parent is an instance of `klass`. - @property nearestChildOf + @method nearestChildOf @param {Class} klass Subclass of Ember.View (or Ember.View itself) @return Ember.View */ @@ -20257,13 +23178,12 @@ Ember.View = Ember.CoreView.extend( }, /** - @private - When the parent view changes, recursively invalidate `controller` @method _parentViewDidChange + @private */ - _parentViewDidChange: Ember.observer(function() { + _parentViewDidChange: Ember.observer('_parentView', function() { if (this.isDestroying) { return; } this.trigger('parentViewDidChange'); @@ -20271,9 +23191,9 @@ Ember.View = Ember.CoreView.extend( if (get(this, 'parentView.controller') && !get(this, 'controller')) { this.notifyPropertyChange('controller'); } - }, '_parentView'), + }), - _controllerDidChange: Ember.observer(function() { + _controllerDidChange: Ember.observer('controller', function() { if (this.isDestroying) { return; } this.rerender(); @@ -20281,7 +23201,7 @@ Ember.View = Ember.CoreView.extend( this.forEachChildView(function(view) { view.propertyDidChange('controller'); }); - }, 'controller'), + }), cloneKeywords: function() { var templateData = get(this, 'templateData'); @@ -20376,14 +23296,13 @@ Ember.View = Ember.CoreView.extend( }, /** - @private - Iterates over the view's `classNameBindings` array, inserts the value of the specified property into the `classNames` array, then creates an observer to update the view's element if the bound property ever changes in the future. @method _applyClassNameBindings + @private */ _applyClassNameBindings: function(classBindings) { var classNames = this.classNames, @@ -20394,6 +23313,8 @@ Ember.View = Ember.CoreView.extend( // ('content.isUrgent') a_forEach(classBindings, function(binding) { + Ember.assert("classNameBindings must not have spaces in them. Multiple class name bindings can be provided as elements of an array, e.g. ['foo', ':bar']", binding.indexOf(' ') === -1); + // Variable in which the old class value is saved. The observer function // closes over this variable, so it knows which string to remove when // the property changes. @@ -20454,13 +23375,12 @@ Ember.View = Ember.CoreView.extend( }, /** - @private - Iterates through the view's attribute bindings, sets up observers for each, then applies the current value of the attributes to the passed render buffer. @method _applyAttributeBindings @param {Ember.RenderBuffer} buffer + @private */ _applyAttributeBindings: function(buffer, attributeBindings) { var attributeValue, elem; @@ -20490,8 +23410,6 @@ Ember.View = Ember.CoreView.extend( }, /** - @private - Given a property name, returns a dasherized version of that property name if the property evaluates to a non-falsy value. @@ -20500,6 +23418,7 @@ Ember.View = Ember.CoreView.extend( @method _classStringForProperty @param property + @private */ _classStringForProperty: function(property) { var parsedPath = Ember.View._parsePropertyPath(property); @@ -20618,7 +23537,7 @@ Ember.View = Ember.CoreView.extend( finished synchronizing @method replaceIn - @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object + @param {String|DOMElement|jQuery} target A selector, element, HTML string, or jQuery object @return {Ember.View} received */ replaceIn: function(target) { @@ -20634,8 +23553,6 @@ Ember.View = Ember.CoreView.extend( }, /** - @private - Schedules a DOM operation to occur during the next render phase. This ensures that all bindings have finished synchronizing before the view is rendered. @@ -20655,6 +23572,7 @@ Ember.View = Ember.CoreView.extend( @method _insertElementLater @param {Function} fn the function that inserts the element into the DOM + @private */ _insertElementLater: function(fn) { this._scheduledInsert = Ember.run.scheduleOnce('render', this, '_insertElement', fn); @@ -20766,13 +23684,12 @@ Ember.View = Ember.CoreView.extend( willClearRender: Ember.K, /** - @private - Run this callback on the current view (unless includeSelf is false) and recursively on child views. @method invokeRecursively @param fn {Function} - @param includeSelf (optional, default true) + @param includeSelf {Boolean} Includes itself if true. + @private */ invokeRecursively: function(fn, includeSelf) { var childViews = (includeSelf === false) ? this._childViews : [this]; @@ -20837,8 +23754,8 @@ Ember.View = Ember.CoreView.extend( If you write a `willDestroyElement()` handler, you can assume that your `didInsertElement()` handler was called earlier for the same element. - Normally you will not call or override this method yourself, but you may - want to implement the above callbacks when it is run. + You should not call or override this method yourself, but you may + want to implement the above callbacks. @method destroyElement @return {Ember.View} receiver @@ -20857,8 +23774,6 @@ Ember.View = Ember.CoreView.extend( willDestroyElement: Ember.K, /** - @private - Triggers the `willDestroyElement` event (which invokes the `willDestroyElement()` method if it exists) on this view and all child views. @@ -20867,6 +23782,7 @@ Ember.View = Ember.CoreView.extend( `willClearRender` event recursively. @method _notifyWillDestroyElement + @private */ _notifyWillDestroyElement: function() { var viewCollection = this.viewHierarchyCollection(); @@ -20875,26 +23791,19 @@ Ember.View = Ember.CoreView.extend( return viewCollection; }, - _elementWillChange: Ember.beforeObserver(function() { - this.forEachChildView(function(view) { - Ember.propertyWillChange(view, 'element'); - }); - }, 'element'), - /** - @private - If this view's element changes, we need to invalidate the caches of our child views so that we do not retain references to DOM elements that are no longer needed. @method _elementDidChange + @private */ - _elementDidChange: Ember.observer(function() { + _elementDidChange: Ember.observer('element', function() { this.forEachChildView(function(view) { - Ember.propertyDidChange(view, 'element'); + delete meta(view).cache.element; }); - }, 'element'), + }), /** Called when the parentView property has changed. @@ -21082,14 +23991,14 @@ Ember.View = Ember.CoreView.extend( // /** - @private - Setup a view, but do not finish waking it up. - - configure `childViews` - - register the view with the global views hash, which is used for event + + * configure `childViews` + * register the view with the global views hash, which is used for event dispatch @method init + @private */ init: function() { this.elementId = this.elementId || guidFor(this); @@ -21268,14 +24177,13 @@ Ember.View = Ember.CoreView.extend( becameHidden: Ember.K, /** - @private - When the view's `isVisible` property changes, toggle the visibility element of the actual DOM element. @method _isVisibleDidChange + @private */ - _isVisibleDidChange: Ember.observer(function() { + _isVisibleDidChange: Ember.observer('isVisible', function() { var $el = this.$(); if (!$el) { return; } @@ -21290,7 +24198,7 @@ Ember.View = Ember.CoreView.extend( } else { this._notifyBecameHidden(); } - }, 'isVisible'), + }), _notifyBecameVisible: function() { this.trigger('becameVisible'); @@ -21340,6 +24248,7 @@ Ember.View = Ember.CoreView.extend( if (priorState && priorState.exit) { priorState.exit(this); } if (currentState.enter) { currentState.enter(this); } + if (state === 'inDOM') { delete Ember.meta(this).cache.element; } if (children !== false) { this.forEachChildView(function(view) { @@ -21353,13 +24262,12 @@ Ember.View = Ember.CoreView.extend( // /** - @private - Handle events from `Ember.EventDispatcher` @method handleEvent @param eventName {String} @param evt {Event} + @private */ handleEvent: function(eventName, evt) { return this.currentState.handleEvent(this, eventName, evt); @@ -21371,6 +24279,10 @@ Ember.View = Ember.CoreView.extend( target = null; } + if (!root || typeof root !== 'object') { + return; + } + var view = this, stateCheckedObserver = function() { view.currentState.invokeObserver(this, observer); @@ -21396,6 +24308,8 @@ Ember.View = Ember.CoreView.extend( element was destroyed, it is in the preRender state * inBuffer: once a view has been rendered, but before it has been inserted into the DOM, it is in the inBuffer state + * hasElement: the DOM representation of the view is created, + and is ready to be inserted * inDOM: once a view has been inserted into the DOM it is in the inDOM state. A view spends the vast majority of its existence in this state. @@ -21462,8 +24376,6 @@ Ember.View.reopen({ Ember.View.reopenClass({ /** - @private - Parse a path and return an object which holds the parsed properties. For example a path like "content.isEnabled:enabled:disabled" will return the @@ -21480,6 +24392,7 @@ Ember.View.reopenClass({ @method _parsePropertyPath @static + @private */ _parsePropertyPath: function(path) { var split = path.split(':'), @@ -21506,8 +24419,6 @@ Ember.View.reopenClass({ }, /** - @private - Get the class name for a given value, based on the path, optional `className` and optional `falsyClassName`. @@ -21529,6 +24440,7 @@ Ember.View.reopenClass({ @param className @param falsyClassName @static + @private */ _classStringForValue: function(path, val, className, falsyClassName) { // When using the colon syntax, evaluate the truthiness or falsiness @@ -21605,11 +24517,12 @@ Ember.View.applyAttributeBindings = function(elem, name, value) { elem.attr(name, value); } } else if (name === 'value' || type === 'boolean') { - // We can't set properties to undefined or null - if (Ember.isNone(value)) { value = ''; } - - if (value !== elem.prop(name)) { - // value and booleans should always be properties + if (Ember.isNone(value) || value === false) { + // `null`, `undefined` or `false` should remove attribute + elem.removeAttr(name); + elem.prop(name, ''); + } else if (value !== elem.prop(name)) { + // value should always be properties elem.prop(name, value); } } else if (!value) { @@ -21687,10 +24600,18 @@ Ember.merge(preRender, { var viewCollection = view.viewHierarchyCollection(); viewCollection.trigger('willInsertElement'); - // after createElement, the view will be in the hasElement state. + fn.call(view); - viewCollection.transitionTo('inDOM', false); - viewCollection.trigger('didInsertElement'); + + // We transition to `inDOM` if the element exists in the DOM + var element = view.get('element'); + while (element = element.parentNode) { + if (element === document) { + viewCollection.transitionTo('inDOM', false); + viewCollection.trigger('didInsertElement'); + } + } + }, renderToBufferIfNeeded: function(view, buffer) { @@ -21767,7 +24688,10 @@ Ember.merge(inBuffer, { }, empty: function() { - Ember.assert("Emptying a view in the inBuffer state is not allowed and should not happen under normal circumstances. Most likely there is a bug in your application. This may be due to excessive property change notifications."); + Ember.assert("Emptying a view in the inBuffer state is not allowed and " + + "should not happen under normal circumstances. Most likely " + + "there is a bug in your application. This may be due to " + + "excessive property change notifications."); }, renderToBufferIfNeeded: function (view, buffer) { @@ -21886,7 +24810,17 @@ Ember.merge(hasElement, { observer.call(target); } }); +})(); + + + +(function() { +/** +@module ember +@submodule ember-views +*/ +var hasElement = Ember.View.states.hasElement; var inDOM = Ember.View.states.inDOM = Ember.create(hasElement); Ember.merge(inDOM, { @@ -21899,7 +24833,7 @@ Ember.merge(inDOM, { } view.addBeforeObserver('elementId', function() { - throw new Error("Changing a view's elementId after creation is not allowed"); + throw new Ember.Error("Changing a view's elementId after creation is not allowed"); }); }, @@ -21995,7 +24929,7 @@ var ViewCollection = Ember._ViewCollection; /** A `ContainerView` is an `Ember.View` subclass that implements `Ember.MutableArray` - allowing programatic management of its child views. + allowing programmatic management of its child views. ## Setting Initial Child Views @@ -22035,7 +24969,7 @@ var ViewCollection = Ember._ViewCollection; ## Adding and Removing Child Views - The container view implements `Ember.MutableArray` allowing programatic management of its child views. + The container view implements `Ember.MutableArray` allowing programmatic management of its child views. To remove a view, pass that view into a `removeObject` call on the container view. @@ -22139,30 +25073,6 @@ var ViewCollection = Ember._ViewCollection; or layout being rendered. The HTML contents of a `Ember.ContainerView`'s DOM representation will only be the rendered HTML of its child views. - ## Binding a View to Display - - If you would like to display a single view in your ContainerView, you can set - its `currentView` property. When the `currentView` property is set to a view - instance, it will be added to the ContainerView. If the `currentView` property - is later changed to a different view, the new view will replace the old view. - If `currentView` is set to `null`, the last `currentView` will be removed. - - This functionality is useful for cases where you want to bind the display of - a ContainerView to a controller or state manager. For example, you can bind - the `currentView` of a container to a controller like this: - - ```javascript - App.appController = Ember.Object.create({ - view: Ember.View.create({ - templateName: 'person_template' - }) - }); - ``` - - ```handlebars - {{view Ember.ContainerView currentViewBinding="App.appController.view"}} - ``` - @class ContainerView @namespace Ember @extends Ember.View @@ -22232,10 +25142,9 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { }).volatile(), /** - @private - Instructs each child view to render to the passed render buffer. + @private @method render @param {Ember.RenderBuffer} buffer the buffer to render to */ @@ -22248,14 +25157,13 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { instrumentName: 'container', /** - @private - When a child view is removed, destroy its element so that it is removed from the DOM. The array observer that triggers this action is set up in the `renderToBuffer` method. + @private @method childViewsWillChange @param {Ember.Array} views the child views array before mutation @param {Number} start the start position of the mutation @@ -22278,8 +25186,6 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { }, /** - @private - When a child view is added, make sure the DOM gets updated appropriately. If the view has already rendered an element, we tell the child view to @@ -22288,6 +25194,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { into an element, we insert the string representation of the child into the appropriate place in the buffer. + @private @method childViewsDidChange @param {Ember.Array} views the array of child views afte the mutation has occurred @param {Number} start the start position of the mutation @@ -22319,20 +25226,20 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { currentView: null, - _currentViewWillChange: Ember.beforeObserver(function() { + _currentViewWillChange: Ember.beforeObserver('currentView', function() { var currentView = get(this, 'currentView'); if (currentView) { currentView.destroy(); } - }, 'currentView'), + }), - _currentViewDidChange: Ember.observer(function() { + _currentViewDidChange: Ember.observer('currentView', function() { var currentView = get(this, 'currentView'); if (currentView) { Ember.assert("You tried to set a current view that already has a parent. Make sure you don't have multiple outlets in the same view.", !get(currentView, '_parentView')); this.pushObject(currentView); } - }, 'currentView'), + }), _ensureChildrenAreInDOM: function () { this.currentState.ensureChildrenAreInDOM(this); @@ -22347,7 +25254,7 @@ Ember.merge(states._default, { Ember.merge(states.inBuffer, { childViewsDidChange: function(parentView, views, start, added) { - throw new Error('You cannot modify child views while in the inBuffer state'); + throw new Ember.Error('You cannot modify child views while in the inBuffer state'); } }); @@ -22477,7 +25384,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; Given an empty `` and the following code: ```javascript - anUndorderedListView = Ember.CollectionView.create({ + anUnorderedListView = Ember.CollectionView.create({ tagName: 'ul', content: ['A','B','C'], itemViewClass: Ember.View.extend({ @@ -22485,7 +25392,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; }) }); - anUndorderedListView.appendTo('body'); + anUnorderedListView.appendTo('body'); ``` Will result in the following HTML structure @@ -22505,7 +25412,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; Ember.CollectionView.CONTAINER_MAP['article'] = 'section' ``` - ## Programatic creation of child views + ## Programmatic creation of child views For cases where additional customization beyond the use of a single `itemViewClass` or `tagName` matching is required CollectionView's @@ -22565,7 +25472,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; @extends Ember.ContainerView @since Ember 0.9 */ -Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionView.prototype */ { +Ember.CollectionView = Ember.ContainerView.extend({ /** A list of items to be displayed by the `Ember.CollectionView`. @@ -22577,12 +25484,11 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie content: null, /** - @private - This provides metadata about what kind of empty view class this collection would like if it is being instantiated from another system (like Handlebars) + @private @property emptyViewClass */ emptyViewClass: Ember.View, @@ -22615,32 +25521,30 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie }, /** - @private - Invoked when the content property is about to change. Notifies observers that the entire array content will change. + @private @method _contentWillChange */ - _contentWillChange: Ember.beforeObserver(function() { + _contentWillChange: Ember.beforeObserver('content', function() { var content = this.get('content'); if (content) { content.removeArrayObserver(this); } var len = content ? get(content, 'length') : 0; this.arrayWillChange(content, 0, len); - }, 'content'), + }), /** - @private - Check to make sure that the content has changed, and if so, update the children directly. This is always scheduled asynchronously, to allow the element to be created before bindings have synchronized and vice versa. + @private @method _contentDidChange */ - _contentDidChange: Ember.observer(function() { + _contentDidChange: Ember.observer('content', function() { var content = get(this, 'content'); if (content) { @@ -22650,13 +25554,12 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie var len = content ? get(content, 'length') : 0; this.arrayDidChange(content, 0, null, len); - }, 'content'), + }), /** - @private - Ensure that the content implements Ember.Array + @private @method _assertArrayLike */ _assertArrayLike: function(content) { @@ -22839,7 +25742,73 @@ Ember.CollectionView.CONTAINER_MAP = { (function() { -var get = Ember.get, set = Ember.set, isNone = Ember.isNone; +/** + The ComponentTemplateDeprecation mixin is used to provide a useful + deprecation warning when using either `template` or `templateName` with + a component. The `template` and `templateName` properties specified at + extend time are moved to `layout` and `layoutName` respectively. + + `Ember.ComponentTemplateDeprecation` is used internally by Ember in + `Ember.Component`. + + @class ComponentTemplateDeprecation + @namespace Ember +*/ +Ember.ComponentTemplateDeprecation = Ember.Mixin.create({ + /** + @private + + Moves `templateName` to `layoutName` and `template` to `layout` at extend + time if a layout is not also specified. + + Note that this currently modifies the mixin themselves, which is technically + dubious but is practically of little consequence. This may change in the + future. + + @method willMergeMixin + */ + willMergeMixin: function(props) { + // must call _super here to ensure that the ActionHandler + // mixin is setup properly (moves actions -> _actions) + // + // Calling super is only OK here since we KNOW that + // there is another Mixin loaded first. + this._super.apply(this, arguments); + + var deprecatedProperty, replacementProperty, + layoutSpecified = (props.layoutName || props.layout); + + if (props.templateName && !layoutSpecified) { + deprecatedProperty = 'templateName'; + replacementProperty = 'layoutName'; + + props.layoutName = props.templateName; + delete props['templateName']; + } + + if (props.template && !layoutSpecified) { + deprecatedProperty = 'template'; + replacementProperty = 'layout'; + + props.layout = props.template; + delete props['template']; + } + + if (deprecatedProperty) { + Ember.deprecate('Do not specify ' + deprecatedProperty + ' on a Component, use ' + replacementProperty + ' instead.', false); + } + } +}); + + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, + a_slice = Array.prototype.slice; + /** @module ember @@ -22852,7 +25821,7 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; to the view object and actions are targeted at the view object. There is no access to the surrounding context or outer controller; all - contextual information is passed in. + contextual information must be passed in. The easiest way to create an `Ember.Component` is via a template. If you name a template @@ -22860,30 +25829,34 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; `{{my-foo}}` in other templates, which will make an instance of the isolated component. - ```html + ```handlebars {{app-profile person=currentUser}} ``` - ```html + ```handlebars

{{person.title}}

{{person.signature}}

``` - You can also use `yield` inside a template to - include the **contents** of the custom tag: + You can use `yield` inside a template to + include the **contents** of any block attached to + the component. The block will be executed in the + context of the surrounding context or outer controller: - ```html + ```handlebars {{#app-profile person=currentUser}}

Admin mode

+ {{! Executed in the controller's context. }} {{/app-profile}} ``` - ```html + ```handlebars

{{person.title}}

- {{yield}} + {{! Executed in the components context. }} + {{yield}} {{! block contents }} ``` If you want to customize the component, in order to @@ -22897,15 +25870,17 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; ```javascript App.AppProfileComponent = Ember.Component.extend({ - hello: function(name) { - console.log("Hello", name); + actions: { + hello: function(name) { + console.log("Hello", name); + } } }); ``` And then use it in the component's template: - ```html + ```handlebars

{{person.title}}

@@ -22925,13 +25900,56 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; @namespace Ember @extends Ember.View */ -Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { +Ember.Component = Ember.View.extend(Ember.TargetActionSupport, Ember.ComponentTemplateDeprecation, { init: function() { this._super(); set(this, 'context', this); set(this, 'controller', this); }, + defaultLayout: function(context, options){ + Ember.Handlebars.helpers['yield'].call(context, options); + }, + + /** + A components template property is set by passing a block + during its invocation. It is executed within the parent context. + + Example: + + ```handlebars + {{#my-component}} + // something that is run in the context + // of the parent context + {{/my-component}} + ``` + + Specifying a template directly to a component is deprecated without + also specifying the layout property. + + @deprecated + @property template + */ + template: Ember.computed(function(key, value) { + if (value !== undefined) { return value; } + + var templateName = get(this, 'templateName'), + template = this.templateForName(templateName, 'template'); + + Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template); + + return template || get(this, 'defaultTemplate'); + }).property('templateName'), + + /** + Specifying a components `templateName` is deprecated without also + providing the `layout` or `layoutName` properties. + + @deprecated + @property templateName + */ + templateName: null, + // during render, isolate keywords cloneKeywords: function() { return { @@ -22952,7 +25970,7 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { isVirtual: true, tagName: '', _contextView: parentView, - template: get(this, 'template'), + template: template, context: get(parentView, 'context'), controller: get(parentView, 'controller'), templateData: { keywords: parentView.cloneKeywords() } @@ -22974,68 +25992,101 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { }).property('_parentView'), /** - Sends an action to component's controller. A component inherits its - controller from the context in which it is used. + Triggers a named action on the controller context where the component is used if + this controller has registered for notifications of the action. - By default, calling `sendAction()` will send an action with the name - of the component's `action` property. + For example a component for playing or pausing music may translate click events + into action notifications of "play" or "stop" depending on some internal state + of the component: - For example, if the component had a property `action` with the value - `"addItem"`, calling `sendAction()` would send the `addItem` action - to the component's controller. - If you provide the `action` argument to `sendAction()`, that key will - be used to look up the action name. + ```javascript + App.PlayButtonComponent = Ember.Component.extend({ + click: function(){ + if (this.get('isPlaying')) { + this.sendAction('play'); + } else { + this.sendAction('stop'); + } + } + }); + ``` - For example, if the component had a property `playing` with the value - `didStartPlaying`, calling `sendAction('playing')` would send the - `didStartPlaying` action to the component's controller. + When used inside a template these component actions are configured to + trigger actions in the outer application context: - Whether or not you are using the default action or a named action, if - the action name is not defined on the component, calling `sendAction()` - does not have any effect. + ```handlebars + {{! application.hbs }} + {{play-button play="musicStarted" stop="musicStopped"}} + ``` - For example, if you call `sendAction()` on a component that does not have - an `action` property defined, no action will be sent to the controller, - nor will an exception be raised. + When the component receives a browser `click` event it translate this + interaction into application-specific semantics ("play" or "stop") and + triggers the specified action name on the controller for the template + where the component is used: - You can send a context object with the action by supplying the `context` - argument. The context will be supplied as the first argument in the - target's action method. Example: ```javascript - App.MyTreeComponent = Ember.Component.extend({ - click: function() { - this.sendAction('didClickTreeNode', this.get('node')); + App.ApplicationController = Ember.Controller.extend({ + actions: { + musicStarted: function(){ + // called when the play button is clicked + // and the music started playing + }, + musicStopped: function(){ + // called when the play button is clicked + // and the music stopped playing + } } }); + ``` - App.CategoriesController = Ember.Controller.extend({ - didClickCategory: function(category) { - //Do something with the node/category that was clicked + If no action name is passed to `sendAction` a default name of "action" + is assumed. + + ```javascript + App.NextButtonComponent = Ember.Component.extend({ + click: function(){ + this.sendAction(); } }); ``` ```handlebars - {{! categories.hbs}} - {{my-tree didClickTreeNode='didClickCategory'}} + {{! application.hbs }} + {{next-button action="playNextSongInAlbum"}} + ``` + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + actions: { + playNextSongInAlbum: function(){ + ... + } + } + }); ``` @method sendAction @param [action] {String} the action to trigger @param [context] {*} a context to send with the action */ - sendAction: function(action, context) { - var actionName; + sendAction: function(action) { + var actionName, + contexts = a_slice.call(arguments, 1); // Send the default action if (action === undefined) { actionName = get(this, 'action'); - Ember.assert("The default action was triggered on the component " + this.toString() + ", but the action name (" + actionName + ") was not a string.", isNone(actionName) || typeof actionName === 'string'); + Ember.assert("The default action was triggered on the component " + this.toString() + + ", but the action name (" + actionName + ") was not a string.", + isNone(actionName) || typeof actionName === 'string'); } else { actionName = get(this, action); - Ember.assert("The " + action + " action was triggered on the component " + this.toString() + ", but the action name (" + actionName + ") was not a string.", isNone(actionName) || typeof actionName === 'string'); + Ember.assert("The " + action + " action was triggered on the component " + + this.toString() + ", but the action name (" + actionName + + ") was not a string.", + isNone(actionName) || typeof actionName === 'string'); } // If no action name for that action could be found, just abort. @@ -23043,7 +26094,7 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { this.triggerAction({ action: actionName, - actionContext: context + actionContext: contexts }); } }); @@ -23140,21 +26191,28 @@ define("metamorph", "use strict"; // ========================================================================== // Project: metamorph - // Copyright: ©2011 My Company Inc. All rights reserved. + // Copyright: ©2014 Tilde, Inc. All rights reserved. // ========================================================================== var K = function() {}, guid = 0, - document = this.document, - disableRange = ('undefined' === typeof ENV ? {} : ENV).DISABLE_RANGE_API, + disableRange = (function(){ + if ('undefined' !== typeof MetamorphENV) { + return MetamorphENV.DISABLE_RANGE_API; + } else if ('undefined' !== ENV) { + return ENV.DISABLE_RANGE_API; + } else { + return false; + } + })(), // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges - supportsRange = (!disableRange) && document && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, + supportsRange = (!disableRange) && typeof document !== 'undefined' && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ - needsShy = document && (function() { + needsShy = typeof document !== 'undefined' && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "
"; testEl.firstChild.innerHTML = ""; @@ -23256,6 +26314,14 @@ define("metamorph", range.insertNode(fragment); }; + /** + * @public + * + * Remove this object (including starting and ending + * placeholders). + * + * @method remove + */ removeFunc = function() { // get a range for the current metamorph object including // the starting and ending placeholders. @@ -23296,7 +26362,7 @@ define("metamorph", }; } else { - /** + /* * This code is mostly taken from jQuery, with one exception. In jQuery's case, we * have some HTML and we need to figure out how to convert it into some nodes. * @@ -23350,12 +26416,12 @@ define("metamorph", } }; - /** + /* * Given a parent node and some HTML, generate a set of nodes. Return the first * node, which will allow us to traverse the rest using nextSibling. * * We need to do this because innerHTML in IE does not really parse the nodes. - **/ + */ var firstNodeFor = function(parentNode, html) { var arr = wrapMap[parentNode.tagName.toLowerCase()] || wrapMap._default; var depth = arr[0], start = arr[1], end = arr[2]; @@ -23388,7 +26454,7 @@ define("metamorph", return element; }; - /** + /* * In some cases, Internet Explorer can create an anonymous node in * the hierarchy with no tagName. You can create this scenario via: * @@ -23398,7 +26464,7 @@ define("metamorph", * * If our script markers are inside such a node, we need to find that * node and use *it* as the marker. - **/ + */ var realNode = function(start) { while (start.parentNode.tagName === "") { start = start.parentNode; @@ -23407,7 +26473,7 @@ define("metamorph", return start; }; - /** + /* * When automatically adding a tbody, Internet Explorer inserts the * tbody immediately before the first . Other browsers create it * before the first node, no matter what. @@ -23434,7 +26500,8 @@ define("metamorph", * * This code reparents the first script tag by making it the tbody's * first child. - **/ + * + */ var fixParentage = function(start, end) { if (start.parentNode !== end.parentNode) { end.parentNode.insertBefore(start, end.parentNode.firstChild); @@ -23485,6 +26552,10 @@ define("metamorph", // swallow some of the content. node = firstNodeFor(start.parentNode, html); + if (outerToo) { + start.parentNode.removeChild(start); + } + // copy the nodes for the HTML between the starting and ending // placeholder. while (node) { @@ -23609,13 +26680,19 @@ var objectCreate = Object.create || function(parent) { return new F(); }; -var Handlebars = this.Handlebars || (Ember.imports && Ember.imports.Handlebars); +var Handlebars = (Ember.imports && Ember.imports.Handlebars) || (this && this.Handlebars); if (!Handlebars && typeof require === 'function') { Handlebars = require('handlebars'); } -Ember.assert("Ember Handlebars requires Handlebars version 1.0.0. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars); -Ember.assert("Ember Handlebars requires Handlebars version 1.0.0, COMPILER_REVISION expected: 4, got: " + Handlebars.COMPILER_REVISION + " - Please note: Builds of master may have other COMPILER_REVISION values.", Handlebars.COMPILER_REVISION === 4); +Ember.assert("Ember Handlebars requires Handlebars version 1.0 or 1.1. Include " + + "a SCRIPT tag in the HTML HEAD linking to the Handlebars file " + + "before you link to Ember.", Handlebars); + +Ember.assert("Ember Handlebars requires Handlebars version 1.0 or 1.1, " + + "COMPILER_REVISION expected: 4, got: " + Handlebars.COMPILER_REVISION + + " - Please note: Builds of master may have other COMPILER_REVISION values.", + Handlebars.COMPILER_REVISION === 4); /** Prepares the Handlebars templating library for use inside Ember's view @@ -23633,20 +26710,6 @@ Ember.assert("Ember Handlebars requires Handlebars version 1.0.0, COMPILER_REVIS */ Ember.Handlebars = objectCreate(Handlebars); -function makeBindings(options) { - var hash = options.hash, - hashType = options.hashTypes; - - for (var prop in hash) { - if (hashType[prop] === 'ID') { - hash[prop + 'Binding'] = hash[prop]; - hashType[prop + 'Binding'] = 'STRING'; - delete hash[prop]; - delete hashType[prop]; - } - } -} - /** Register a bound helper or custom view helper. @@ -23704,16 +26767,30 @@ Ember.Handlebars.helper = function(name, value) { Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", !Ember.Component.detect(value) || name.match(/-/)); if (Ember.View.detect(value)) { - Ember.Handlebars.registerHelper(name, function(options) { - Ember.assert("You can only pass attributes (such as name=value) not bare values to a helper for a View", arguments.length < 2); - makeBindings(options); - return Ember.Handlebars.helpers.view.call(this, value, options); - }); + Ember.Handlebars.registerHelper(name, Ember.Handlebars.makeViewHelper(value)); } else { Ember.Handlebars.registerBoundHelper.apply(null, arguments); } }; +/** + Returns a helper function that renders the provided ViewClass. + + Used internally by Ember.Handlebars.helper and other methods + involving helper/component registration. + + @private + @method helper + @for Ember.Handlebars + @param {Function} ViewClass view class constructor +*/ +Ember.Handlebars.makeViewHelper = function(ViewClass) { + return function(options) { + Ember.assert("You can only pass attributes (such as name=value) not bare values to a helper for a View found in '" + ViewClass.toString() + "'", arguments.length < 2); + return Ember.Handlebars.helpers.view.call(this, ViewClass, options); + }; +}; + /** @class helpers @namespace Ember.Handlebars @@ -23754,18 +26831,16 @@ if (Handlebars.JavaScriptCompiler) { Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; - Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { return "''"; }; /** - @private - Override the default buffer for Ember Handlebars. By default, Handlebars creates an empty String at the beginning of each invocation and appends to it. Ember's Handlebars overrides this to append to a single shared buffer. + @private @method appendToBuffer @param string {String} */ @@ -23773,15 +26848,51 @@ Ember.Handlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) return "data.buffer.push("+string+");"; }; +// Hacks ahead: +// Handlebars presently has a bug where the `blockHelperMissing` hook +// doesn't get passed the name of the missing helper name, but rather +// gets passed the value of that missing helper evaluated on the current +// context, which is most likely `undefined` and totally useless. +// +// So we alter the compiled template function to pass the name of the helper +// instead, as expected. +// +// This can go away once the following is closed: +// https://github.com/wycats/handlebars.js/issues/634 + +var DOT_LOOKUP_REGEX = /helpers\.(.*?)\)/, + BRACKET_STRING_LOOKUP_REGEX = /helpers\['(.*?)'/, + INVOCATION_SPLITTING_REGEX = /(.*blockHelperMissing\.call\(.*)(stack[0-9]+)(,.*)/; + +Ember.Handlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation = function(source) { + var helperInvocation = source[source.length - 1], + helperName = (DOT_LOOKUP_REGEX.exec(helperInvocation) || BRACKET_STRING_LOOKUP_REGEX.exec(helperInvocation))[1], + matches = INVOCATION_SPLITTING_REGEX.exec(helperInvocation); + + source[source.length - 1] = matches[1] + "'" + helperName + "'" + matches[3]; +} +var stringifyBlockHelperMissing = Ember.Handlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation; + +var originalBlockValue = Ember.Handlebars.JavaScriptCompiler.prototype.blockValue; +Ember.Handlebars.JavaScriptCompiler.prototype.blockValue = function() { + originalBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); +}; + +var originalAmbiguousBlockValue = Ember.Handlebars.JavaScriptCompiler.prototype.ambiguousBlockValue; +Ember.Handlebars.JavaScriptCompiler.prototype.ambiguousBlockValue = function() { + originalAmbiguousBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); +}; + var prefix = "ember" + (+new Date()), incr = 1; /** - @private - Rewrite simple mustaches from `{{foo}}` to `{{bind "foo"}}`. This means that all simple mustaches in Ember's Handlebars will also set up an observer to keep the DOM up to date when the underlying property changes. + @private @method mustache @for Ember.Handlebars.Compiler @param mustache @@ -23824,7 +26935,7 @@ Ember.Handlebars.precompile = function(string) { knownHelpers: { action: true, unbound: true, - bindAttr: true, + 'bind-attr': true, template: true, view: true, _triageMustache: true @@ -23858,7 +26969,7 @@ if (Handlebars.compile) { var template = Ember.Handlebars.template(templateSpec); template.isMethod = false; //Make sure we don't wrap templates with ._super - + return template; }; } @@ -23867,14 +26978,14 @@ if (Handlebars.compile) { })(); (function() { -var slice = Array.prototype.slice; +var slice = Array.prototype.slice, + originalTemplate = Ember.Handlebars.template; /** - @private - If a path starts with a reserved keyword, returns the root that should be used. + @private @method normalizePath @for Ember @param root {Object} @@ -23928,22 +27039,46 @@ var handlebarsGet = Ember.Handlebars.get = function(root, path, options) { normalizedPath = normalizePath(root, path, data), value; - // In cases where the path begins with a keyword, change the - // root to the value represented by that keyword, and ensure - // the path is relative to it. - root = normalizedPath.root; - path = normalizedPath.path; + + root = normalizedPath.root; + path = normalizedPath.path; - value = Ember.get(root, path); + value = Ember.get(root, path); + + if (value === undefined && root !== Ember.lookup && Ember.isGlobalPath(path)) { + value = Ember.get(Ember.lookup, path); + } + - // If the path starts with a capital letter, look it up on Ember.lookup, - // which defaults to the `window` object in browsers. - if (value === undefined && root !== Ember.lookup && Ember.isGlobalPath(path)) { - value = Ember.get(Ember.lookup, path); - } return value; }; -Ember.Handlebars.getPath = Ember.deprecateFunc('`Ember.Handlebars.getPath` has been changed to `Ember.Handlebars.get` for consistency.', Ember.Handlebars.get); + +/** + This method uses `Ember.Handlebars.get` to lookup a value, then ensures + that the value is escaped properly. + + If `unescaped` is a truthy value then the escaping will not be performed. + + @method getEscaped + @for Ember.Handlebars + @param {Object} root The object to look up the property on + @param {String} path The path to be lookedup + @param {Object} options The template's option hash +*/ +Ember.Handlebars.getEscaped = function(root, path, options) { + var result = handlebarsGet(root, path, options); + + if (result === null || result === undefined) { + result = ""; + } else if (!(result instanceof Handlebars.SafeString)) { + result = String(result); + } + if (!options.hash.unescaped){ + result = Handlebars.Utils.escapeExpression(result); + } + + return result; +}; Ember.Handlebars.resolveParams = function(context, params, options) { var resolvedParams = [], types = options.types, param, type; @@ -23981,8 +27116,6 @@ Ember.Handlebars.resolveHash = function(context, hash, options) { }; /** - @private - Registers a helper in Handlebars that will be called if no property with the given name can be found on the current context object, and no helper with that name is registered. @@ -23990,14 +27123,23 @@ Ember.Handlebars.resolveHash = function(context, hash, options) { This throws an exception with a more helpful error message so the user can track down where the problem is happening. + @private @method helperMissing @for Ember.Handlebars.helpers @param {String} path @param {Hash} options */ -Ember.Handlebars.registerHelper('helperMissing', function(path, options) { +Ember.Handlebars.registerHelper('helperMissing', function(path) { var error, view = ""; + var options = arguments[arguments.length - 1]; + + var helper = Ember.Handlebars.resolveHelper(options.data.view.container, path); + + if (helper) { + return helper.apply(this, slice.call(arguments, 1)); + } + error = "%@ Handlebars error: Could not find property '%@' on object %@."; if (options.data) { view = options.data.view; @@ -24005,6 +27147,41 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { throw new Ember.Error(Ember.String.fmt(error, [view, path, this])); }); +/** + Registers a helper in Handlebars that will be called if no property with the + given name can be found on the current context object, and no helper with + that name is registered. + + This throws an exception with a more helpful error message so the user can + track down where the problem is happening. + + @private + @method helperMissing + @for Ember.Handlebars.helpers + @param {String} path + @param {Hash} options +*/ +Ember.Handlebars.registerHelper('blockHelperMissing', function(path) { + + var options = arguments[arguments.length - 1]; + + Ember.assert("`blockHelperMissing` was invoked without a helper name, which " + + "is most likely due to a mismatch between the version of " + + "Ember.js you're running now and the one used to precompile your " + + "templates. Please make sure the version of " + + "`ember-handlebars-compiler` you're using is up to date.", path); + + var helper = Ember.Handlebars.resolveHelper(options.data.view.container, path); + + if (helper) { + return helper.apply(this, slice.call(arguments, 1)); + } else { + return Handlebars.helpers.helperMissing.call(this, path); + } + + return Handlebars.helpers.blockHelperMissing.apply(this, arguments); +}); + /** Register a bound handlebars helper. Bound helpers behave similarly to regular handlebars helpers, with the added ability to re-render when the underlying data @@ -24115,18 +27292,50 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { @param {String} dependentKeys* */ Ember.Handlebars.registerBoundHelper = function(name, fn) { - var dependentKeys = slice.call(arguments, 2); + var boundHelperArgs = slice.call(arguments, 1), + boundFn = Ember.Handlebars.makeBoundHelper.apply(this, boundHelperArgs); + Ember.Handlebars.registerHelper(name, boundFn); +}; + +/** + A (mostly) private helper function to `registerBoundHelper`. Takes the + provided Handlebars helper function fn and returns it in wrapped + bound helper form. + + The main use case for using this outside of `registerBoundHelper` + is for registering helpers on the container: + + ```js + var boundHelperFn = Ember.Handlebars.makeBoundHelper(function(word) { + return word.toUpperCase(); + }); + + container.register('helper:my-bound-helper', boundHelperFn); + ``` + + In the above example, if the helper function hadn't been wrapped in + `makeBoundHelper`, the registered helper would be unbound. + + @private + @method makeBoundHelper + @for Ember.Handlebars + @param {Function} function + @param {String} dependentKeys* +*/ +Ember.Handlebars.makeBoundHelper = function(fn) { + var dependentKeys = slice.call(arguments, 1); function helper() { var properties = slice.call(arguments, 0, -1), numProperties = properties.length, options = arguments[arguments.length - 1], normalizedProperties = [], - types = options.types, data = options.data, + types = data.isUnbound ? slice.call(options.types, 1) : options.types, hash = options.hash, view = data.view, - currentContext = (options.contexts && options.contexts[0]) || this, + contexts = options.contexts, + currentContext = (contexts && contexts.length) ? contexts[0] : this, prefixPathForDependentKeys = '', loc, len, hashOption, boundOption, property, @@ -24153,7 +27362,11 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { normalizedProperties.push(normalizedProp); watchedProperties.push(normalizedProp); } else { - normalizedProperties.push(null); + if(data.isUnbound) { + normalizedProperties.push({path: properties[loc]}); + }else { + normalizedProperties.push(null); + } } } @@ -24227,14 +27440,13 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { } helper._rawFunction = fn; - Ember.Handlebars.registerHelper(name, helper); + return helper; }; /** - @private - Renders the unbound form of an otherwise bound helper function. + @private @method evaluateUnboundHelper @param {Function} fn @param {Object} context @@ -24242,7 +27454,15 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { @param {String} options */ function evaluateUnboundHelper(context, fn, normalizedProperties, options) { - var args = [], hash = options.hash, boundOptions = hash.boundOptions, loc, len, property, boundOption; + var args = [], + hash = options.hash, + boundOptions = hash.boundOptions, + types = slice.call(options.types, 1), + loc, + len, + property, + propertyType, + boundOption; for (boundOption in boundOptions) { if (!boundOptions.hasOwnProperty(boundOption)) { continue; } @@ -24251,24 +27471,28 @@ function evaluateUnboundHelper(context, fn, normalizedProperties, options) { for(loc = 0, len = normalizedProperties.length; loc < len; ++loc) { property = normalizedProperties[loc]; - args.push(Ember.Handlebars.get(context, property.path, options)); + propertyType = types[loc]; + if(propertyType === "ID") { + args.push(Ember.Handlebars.get(property.root, property.path, options)); + } else { + args.push(property.path); + } } args.push(options); return fn.apply(context, args); } /** - @private - Overrides Handlebars.template so that we can distinguish user-created, top-level templates from inner contexts. + @private @method template @for Ember.Handlebars - @param {String} template spec + @param {String} spec */ Ember.Handlebars.template = function(spec) { - var t = Handlebars.template(spec); + var t = originalTemplate(spec); t.isTop = true; return t; }; @@ -24382,7 +27606,7 @@ var DOMManager = { view.transitionTo('preRender'); - Ember.run.schedule('render', this, function() { + Ember.run.schedule('render', this, function renderMetamorphView() { if (view.isDestroying) { return; } view.clearRenderedChildren(); @@ -24751,6 +27975,8 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ preserveContext = get(this, 'preserveContext'), context = get(this, 'previousContext'); + var _contextController = get(this, '_contextController'); + var inverseTemplate = get(this, 'inverseTemplate'), displayTemplate = get(this, 'displayTemplate'); @@ -24770,6 +27996,10 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ // Otherwise, determine if this is a block bind or not. // If so, pass the specified object to the template if (displayTemplate) { + if (_contextController) { + set(_contextController, 'content', result); + result = _contextController; + } set(this, '_context', result); } else { // This is not a bind block, just push the result of the @@ -24813,7 +28043,9 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; +var handlebarsGetEscaped = Ember.Handlebars.getEscaped; var forEach = Ember.ArrayPolyfills.forEach; +var o_create = Ember.create; var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; @@ -24869,6 +28101,14 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer templateData: options.data }); + if (options.hash.controller) { + bindView.set('_contextController', this.container.lookupFactory('controller:'+options.hash.controller).create({ + container: currentContext.container, + parentController: currentContext, + target: currentContext + })); + } + view.appendChild(bindView); observer = function() { @@ -24891,28 +28131,30 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer } else { // The object is not observable, so just render it out and // be done with it. - data.buffer.push(handlebarsGet(currentContext, property, options)); + data.buffer.push(handlebarsGetEscaped(currentContext, property, options)); } } -function simpleBind(property, options) { +EmberHandlebars.bind = bind; + +function simpleBind(currentContext, property, options) { var data = options.data, view = data.view, - currentContext = this, - normalized, observer; + normalized, observer, pathRoot, output; normalized = normalizePath(currentContext, property, data); + pathRoot = normalized.root; // Set up observers for observable objects - if ('object' === typeof this) { + if (pathRoot && ('object' === typeof pathRoot)) { if (data.insideGroup) { observer = function() { Ember.run.once(view, 'rerender'); }; - var result = handlebarsGet(currentContext, property, options); - if (result === null || result === undefined) { result = ""; } - data.buffer.push(result); + output = handlebarsGetEscaped(currentContext, property, options); + + data.buffer.push(output); } else { var bindView = new Ember._SimpleHandlebarsView( property, currentContext, !options.hash.unescaped, options.data @@ -24936,39 +28178,73 @@ function simpleBind(property, options) { } else { // The object is not observable, so just render it out and // be done with it. - data.buffer.push(handlebarsGet(currentContext, property, options)); + output = handlebarsGetEscaped(currentContext, property, options); + data.buffer.push(output); } } -/** - @private +function shouldDisplayIfHelperContent(result) { + var truthy = result && get(result, 'isTruthy'); + if (typeof truthy === 'boolean') { return truthy; } + + if (Ember.isArray(result)) { + return get(result, 'length') !== 0; + } else { + return !!result; + } +} - '_triageMustache' is used internally select between a binding and helper for +/** + '_triageMustache' is used internally select between a binding, helper, or component for the given context. Until this point, it would be hard to determine if the mustache is a property reference or a regular helper reference. This triage helper resolves that. This would not be typically invoked by directly. + @private @method _triageMustache @for Ember.Handlebars.helpers @param {String} property Property/helperID to triage - @param {Function} fn Context to provide for rendering + @param {Object} options hash of template/rendering options @return {String} HTML string */ -EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { +EmberHandlebars.registerHelper('_triageMustache', function(property, options) { Ember.assert("You cannot pass more than one argument to the _triageMustache helper", arguments.length <= 2); + if (helpers[property]) { - return helpers[property].call(this, fn); + return helpers[property].call(this, options); } - else { - return helpers.bind.apply(this, arguments); + + var helper = Ember.Handlebars.resolveHelper(options.data.view.container, property); + if (helper) { + return helper.call(this, options); } + + return helpers.bind.call(this, property, options); }); -/** - @private +Ember.Handlebars.resolveHelper = function(container, name) { + + if (!container || name.indexOf('-') === -1) { + return; + } + + var helper = container.lookup('helper:' + name); + if (!helper) { + var componentLookup = container.lookup('component-lookup:main'); + Ember.assert("Could not find 'component-lookup:main' on the provided container, which is necessary for performing component lookups", componentLookup); + var Component = componentLookup.lookupFactory(name, container); + if (Component) { + helper = EmberHandlebars.makeViewHelper(Component); + container.register('helper:' + name, helper); + } + } + return helper; +}; + +/** `bind` can be used to display a value, then update that value if it changes. For example, if you wanted to print the `title` property of `content`: @@ -24984,27 +28260,26 @@ EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { relies on Ember's KVO system. For all other browsers this will be handled for you automatically. + @private @method bind @for Ember.Handlebars.helpers @param {String} property Property to bind @param {Function} fn Context to provide for rendering @return {String} HTML string */ -EmberHandlebars.registerHelper('bind', function(property, options) { +EmberHandlebars.registerHelper('bind', function bindHelper(property, options) { Ember.assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2); - var context = (options.contexts && options.contexts[0]) || this; + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this; if (!options.fn) { - return simpleBind.call(context, property, options); + return simpleBind(context, property, options); } return bind.call(context, property, options, false, exists); }); /** - @private - Use the `boundIf` helper to create a conditional that re-evaluates whenever the truthiness of the bound value changes. @@ -25014,38 +28289,131 @@ EmberHandlebars.registerHelper('bind', function(property, options) { {{/boundIf}} ``` + @private @method boundIf @for Ember.Handlebars.helpers @param {String} property Property to bind @param {Function} fn Context to provide for rendering @return {String} HTML string */ -EmberHandlebars.registerHelper('boundIf', function(property, fn) { - var context = (fn.contexts && fn.contexts[0]) || this; - var func = function(result) { - var truthy = result && get(result, 'isTruthy'); - if (typeof truthy === 'boolean') { return truthy; } +EmberHandlebars.registerHelper('boundIf', function boundIfHelper(property, fn) { + var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; - if (Ember.isArray(result)) { - return get(result, 'length') !== 0; - } else { - return !!result; - } - }; + return bind.call(context, property, fn, true, shouldDisplayIfHelperContent, shouldDisplayIfHelperContent, ['isTruthy', 'length']); +}); + + +/** + @private + + Use the `unboundIf` helper to create a conditional that evaluates once. + + ```handlebars + {{#unboundIf "content.shouldDisplayTitle"}} + {{content.title}} + {{/unboundIf}} + ``` - return bind.call(context, property, fn, true, func, func, ['isTruthy', 'length']); + @method unboundIf + @for Ember.Handlebars.helpers + @param {String} property Property to bind + @param {Function} fn Context to provide for rendering + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('unboundIf', function unboundIfHelper(property, fn) { + var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this, + data = fn.data, + template = fn.fn, + inverse = fn.inverse, + normalized, propertyValue, result; + + normalized = normalizePath(context, property, data); + propertyValue = handlebarsGet(context, property, fn); + + if (!shouldDisplayIfHelperContent(propertyValue)) { + template = inverse; + } + + template(context, { data: data }); }); /** + Use the `{{with}}` helper when you want to scope context. Take the following code as an example: + + ```handlebars +
{{user.name}}
+ +
+
{{user.role.label}}
+ {{user.role.id}} + +

{{user.role.description}}

+
+ ``` + + `{{with}}` can be our best friend in these cases, + instead of writing `user.role.*` over and over, we use `{{#with user.role}}`. + Now the context within the `{{#with}} .. {{/with}}` block is `user.role` so you can do the following: + + ```handlebars +
{{user.name}}
+ +
+ {{#with user.role}} +
{{label}}
+ {{id}} + +

{{description}}

+ {{/with}} +
+ ``` + + ### `as` operator + + This operator aliases the scope to a new name. It's helpful for semantic clarity and to retain + default scope or to reference from another `{{with}}` block. + + ```handlebars + // posts might not be + {{#with user.posts as blogPosts}} +
+ There are {{blogPosts.length}} blog posts written by {{user.name}}. +
+ + {{#each post in blogPosts}} +
  • {{post.title}}
  • + {{/each}} + {{/with}} + ``` + + Without the `as` operator, it would be impossible to reference `user.name` in the example above. + + ### `controller` option + + Adding `controller='something'` instructs the `{{with}}` helper to create and use an instance of + the specified controller with the new context as its content. + + This is very similar to using an `itemController` option with the `{{each}}` helper. + + ```handlebars + {{#with users.posts controller='userBlogPosts'}} + {{!- The current context is wrapped in our controller instance }} + {{/with}} + ``` + + In the above example, the template provided to the `{{with}}` block is now wrapped in the + `userBlogPost` controller, which provides a very elegant way to decorate the context with custom + functions/properties. + @method with @for Ember.Handlebars.helpers @param {Function} context @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('with', function(context, options) { +EmberHandlebars.registerHelper('with', function withHelper(context, options) { if (arguments.length === 4) { - var keywordName, path, rootPath, normalized; + var keywordName, path, rootPath, normalized, contextPath; Ember.assert("If you pass more than one argument to the with helper, it must be in the form #with foo as bar", arguments[1] === "as"); options = arguments[3]; @@ -25054,8 +28422,12 @@ EmberHandlebars.registerHelper('with', function(context, options) { Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); + var localizedOptions = o_create(options); + localizedOptions.data = o_create(options.data); + localizedOptions.data.keywords = o_create(options.data.keywords || {}); + if (Ember.isGlobalPath(path)) { - Ember.bind(options.data.keywords, keywordName, path); + contextPath = path; } else { normalized = normalizePath(this, path, options.data); path = normalized.path; @@ -25064,14 +28436,14 @@ EmberHandlebars.registerHelper('with', function(context, options) { // This is a workaround for the fact that you cannot bind separate objects // together. When we implement that functionality, we should use it here. var contextKey = Ember.$.expando + Ember.guidFor(rootPath); - options.data.keywords[contextKey] = rootPath; - + localizedOptions.data.keywords[contextKey] = rootPath; // if the path is '' ("this"), just bind directly to the current context - var contextPath = path ? contextKey + '.' + path : contextKey; - Ember.bind(options.data.keywords, keywordName, contextPath); + contextPath = path ? contextKey + '.' + path : contextKey; } - return bind.call(this, path, options, true, exists); + Ember.bind(localizedOptions.data.keywords, keywordName, contextPath); + + return bind.call(this, path, localizedOptions, true, exists); } else { Ember.assert("You must pass exactly one argument to the with helper", arguments.length === 2); Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); @@ -25082,6 +28454,7 @@ EmberHandlebars.registerHelper('with', function(context, options) { /** See [boundIf](/api/classes/Ember.Handlebars.helpers.html#method_boundIf) + and [unboundIf](/api/classes/Ember.Handlebars.helpers.html#method_unboundIf) @method if @for Ember.Handlebars.helpers @@ -25089,11 +28462,14 @@ EmberHandlebars.registerHelper('with', function(context, options) { @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('if', function(context, options) { +EmberHandlebars.registerHelper('if', function ifHelper(context, options) { Ember.assert("You must pass exactly one argument to the if helper", arguments.length === 2); Ember.assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop); - - return helpers.boundIf.call(options.contexts[0], context, options); + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } }); /** @@ -25103,7 +28479,7 @@ EmberHandlebars.registerHelper('if', function(context, options) { @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('unless', function(context, options) { +EmberHandlebars.registerHelper('unless', function unlessHelper(context, options) { Ember.assert("You must pass exactly one argument to the unless helper", arguments.length === 2); Ember.assert("You must pass a block to the unless helper", options.fn && options.fn !== Handlebars.VM.noop); @@ -25112,7 +28488,11 @@ EmberHandlebars.registerHelper('unless', function(context, options) { options.fn = inverse; options.inverse = fn; - return helpers.boundIf.call(options.contexts[0], context, options); + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } }); /** @@ -25154,7 +28534,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { `bind-attr` supports a special syntax for handling a number of cases unique to the `class` DOM element attribute. The `class` attribute combines - multiple discreet values into a single attribute as a space-delimited + multiple discrete values into a single attribute as a space-delimited list of strings. Each string can be: * a string return value of an object's property. @@ -25238,7 +28618,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('bind-attr', function(options) { +EmberHandlebars.registerHelper('bind-attr', function bindAttrHelper(options) { var attrs = options.hash; @@ -25284,7 +28664,9 @@ EmberHandlebars.registerHelper('bind-attr', function(options) { observer = function observer() { var result = handlebarsGet(ctx, path, options); - Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), result === null || result === undefined || typeof result === 'number' || typeof result === 'string' || typeof result === 'boolean'); + Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), + result === null || result === undefined || typeof result === 'number' || + typeof result === 'string' || typeof result === 'boolean'); var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']"); @@ -25334,11 +28716,12 @@ EmberHandlebars.registerHelper('bind-attr', function(options) { @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('bindAttr', EmberHandlebars.helpers['bind-attr']); +EmberHandlebars.registerHelper('bindAttr', function bindAttrHelper() { + Ember.warn("The 'bindAttr' view helper is deprecated in favor of 'bind-attr'"); + return EmberHandlebars.helpers['bind-attr'].apply(this, arguments); +}); /** - @private - Helper that, given a space-separated string of property paths and a context, returns an array of class names. Calling this method also has the side effect of setting up observers at those property paths, such that if they @@ -25350,6 +28733,7 @@ EmberHandlebars.registerHelper('bindAttr', EmberHandlebars.helpers['bind-attr']) "fooBar"). If the value is a string, it will add that string as the class. Otherwise, it will not add any new class name. + @private @method bindClasses @for Ember.Handlebars @param {Ember.Object} context The context from which to lookup properties @@ -25471,9 +28855,38 @@ var EmberHandlebars = Ember.Handlebars; var LOWERCASE_A_Z = /^[a-z]/; var VIEW_PREFIX = /^view\./; +function makeBindings(thisContext, options) { + var hash = options.hash, + hashType = options.hashTypes; + + for (var prop in hash) { + if (hashType[prop] === 'ID') { + + var value = hash[prop]; + + if (Ember.IS_BINDING.test(prop)) { + Ember.warn("You're attempting to render a view by passing " + prop + "=" + value + " to a view helper, but this syntax is ambiguous. You should either surround " + value + " in quotes or remove `Binding` from " + prop + "."); + } else { + hash[prop + 'Binding'] = value; + hashType[prop + 'Binding'] = 'STRING'; + delete hash[prop]; + delete hashType[prop]; + } + } + } + + if (hash.hasOwnProperty('idBinding')) { + // id can't be bound, so just perform one-time lookup. + hash.id = EmberHandlebars.get(thisContext, hash.idBinding, options); + hashType.id = 'STRING'; + delete hash.idBinding; + delete hashType.idBinding; + } +} + EmberHandlebars.ViewHelper = Ember.Object.create({ - propertiesFromHTMLOptions: function(options, thisContext) { + propertiesFromHTMLOptions: function(options) { var hash = options.hash, data = options.data; var extensions = {}, classes = hash['class'], @@ -25568,7 +28981,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ return 'templateData.keywords.' + path; } else if (Ember.isGlobalPath(path)) { return null; - } else if (path === 'this') { + } else if (path === 'this' || path === '') { return '_parentView.context'; } else { return '_parentView.context.' + path; @@ -25580,6 +28993,8 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ fn = options.fn, newView; + makeBindings(thisContext, options); + if ('string' === typeof path) { // TODO: this is a lame conditional, this should likely change @@ -25785,7 +29200,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('view', function(path, options) { +EmberHandlebars.registerHelper('view', function viewHelper(path, options) { Ember.assert("The view helper only takes a single argument", arguments.length <= 2); // If no path is provided, treat path param as options. @@ -25803,8 +29218,6 @@ EmberHandlebars.registerHelper('view', function(path, options) { (function() { -/*globals Handlebars */ - // TODO: Don't require all of this module /** @module ember @@ -25855,7 +29268,7 @@ var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fm ``` - ### Blockless Use + ### Blockless use in a collection If you provide an `itemViewClass` option that has its own `template` you can omit the block. @@ -25935,7 +29348,7 @@ var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fm @return {String} HTML string @deprecated Use `{{each}}` helper instead. */ -Ember.Handlebars.registerHelper('collection', function(path, options) { +Ember.Handlebars.registerHelper('collection', function collectionHelper(path, options) { Ember.deprecate("Using the {{collection}} helper without specifying a class has been deprecated as the {{each}} helper now supports the same functionality.", path !== 'collection'); // If no path is provided, treat path param as options. @@ -25952,11 +29365,20 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { var inverse = options.inverse; var view = options.data.view; + + var controller, container; // If passed a path string, convert that into an object. // Otherwise, just default to the standard class. var collectionClass; - collectionClass = path ? handlebarsGet(this, path, options) : Ember.CollectionView; - Ember.assert(fmt("%@ #collection: Could not find collection class %@", [data.view, path]), !!collectionClass); + if (path) { + controller = data.keywords.controller; + container = controller && controller.container; + collectionClass = handlebarsGet(this, path, options) || container.lookupFactory('view:' + path); + Ember.assert(fmt("%@ #collection: Could not find collection class %@", [data.view, path]), !!collectionClass); + } + else { + collectionClass = Ember.CollectionView; + } var hash = options.hash, itemHash = {}, match; @@ -25965,11 +29387,18 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { itemViewClass; if (hash.itemView) { - var controller = data.keywords.controller; - Ember.assert('You specified an itemView, but the current context has no container to look the itemView up in. This probably means that you created a view manually, instead of through the container. Instead, use container.lookup("view:viewName"), which will properly instantiate your view.', controller && controller.container); - var container = controller.container; - itemViewClass = container.resolve('view:' + Ember.String.camelize(hash.itemView)); - Ember.assert('You specified the itemView ' + hash.itemView + ", but it was not found at " + container.describe("view:" + hash.itemView) + " (and it was not registered in the container)", !!itemViewClass); + controller = data.keywords.controller; + Ember.assert('You specified an itemView, but the current context has no ' + + 'container to look the itemView up in. This probably means ' + + 'that you created a view manually, instead of through the ' + + 'container. Instead, use container.lookup("view:viewName"), ' + + 'which will properly instantiate your view.', + controller && controller.container); + container = controller.container; + itemViewClass = container.lookupFactory('view:' + hash.itemView); + Ember.assert('You specified the itemView ' + hash.itemView + ", but it was " + + "not found at " + container.describe("view:" + hash.itemView) + + " (and it was not registered in the container)", !!itemViewClass); } else if (hash.itemViewClass) { itemViewClass = handlebarsGet(collectionPrototype, hash.itemViewClass, options); } else { @@ -26003,7 +29432,7 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { } var emptyViewClass; - if (inverse && inverse !== Handlebars.VM.noop) { + if (inverse && inverse !== Ember.Handlebars.VM.noop) { emptyViewClass = get(collectionPrototype, 'emptyViewClass'); emptyViewClass = emptyViewClass.extend({ template: inverse, @@ -26058,19 +29487,19 @@ var handlebarsGet = Ember.Handlebars.get; @param {String} property @return {String} HTML string */ -Ember.Handlebars.registerHelper('unbound', function(property, fn) { +Ember.Handlebars.registerHelper('unbound', function unboundHelper(property, fn) { var options = arguments[arguments.length - 1], helper, context, out; if (arguments.length > 2) { // Unbound helper call. options.data.isUnbound = true; - helper = Ember.Handlebars.helpers[arguments[0]] || Ember.Handlebars.helperMissing; + helper = Ember.Handlebars.helpers[arguments[0]] || Ember.Handlebars.helpers.helperMissing; out = helper.apply(this, Array.prototype.slice.call(arguments, 1)); delete options.data.isUnbound; return out; } - context = (fn.contexts && fn.contexts[0]) || this; + context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; return handlebarsGet(context, property, fn); }); @@ -26085,7 +29514,7 @@ Ember.Handlebars.registerHelper('unbound', function(property, fn) { @submodule ember-handlebars */ -var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; +var get = Ember.get, handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; /** `log` allows you to output the value of a variable in the current rendering @@ -26099,8 +29528,8 @@ var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.norma @for Ember.Handlebars.helpers @param {String} property */ -Ember.Handlebars.registerHelper('log', function(property, options) { - var context = (options.contexts && options.contexts[0]) || this, +Ember.Handlebars.registerHelper('log', function logHelper(property, options) { + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this, normalized = normalizePath(context, property, options.data), pathRoot = normalized.root, path = normalized.path, @@ -26115,14 +29544,44 @@ Ember.Handlebars.registerHelper('log', function(property, options) { {{debugger}} ``` + Before invoking the `debugger` statement, there + are a few helpful variables defined in the + body of this helper that you can inspect while + debugging that describe how and where this + helper was invoked: + + - templateContext: this is most likely a controller + from which this template looks up / displays properties + - typeOfTemplateContext: a string description of + what the templateContext is + + For example, if you're wondering why a value `{{foo}}` + isn't rendering as expected within a template, you + could place a `{{debugger}}` statement, and when + the `debugger;` breakpoint is hit, you can inspect + `templateContext`, determine if it's the object you + expect, and/or evaluate expressions in the console + to perform property lookups on the `templateContext`: + + ``` + > templateContext.get('foo') // -> "" + ``` + @method debugger @for Ember.Handlebars.helpers @param {String} property */ -Ember.Handlebars.registerHelper('debugger', function(options) { +Ember.Handlebars.registerHelper('debugger', function debuggerHelper(options) { + + // These are helpful values you can inspect while debugging. + var templateContext = this; + var typeOfTemplateContext = Ember.inspect(templateContext); + debugger; }); + + })(); @@ -26134,6 +29593,7 @@ Ember.Handlebars.registerHelper('debugger', function(options) { */ var get = Ember.get, set = Ember.set; +var fmt = Ember.String.fmt; Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { init: function() { @@ -26142,6 +29602,7 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { if (itemController) { var controller = get(this, 'controller.container').lookupFactory('controller:array').create({ + _isVirtual: true, parentController: get(this, 'controller'), itemController: itemController, target: get(this, 'controller'), @@ -26166,8 +29627,13 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { }, _assertArrayLike: function(content) { - Ember.assert("The value that #each loops over must be an Array. You passed " + content.constructor + ", but it should have been an ArrayController", !Ember.ControllerMixin.detect(content) || (content && content.isGenerated) || content instanceof Ember.ArrayController); - Ember.assert("The value that #each loops over must be an Array. You passed " + ((Ember.ControllerMixin.detect(content) && content.get('model') !== undefined) ? ("" + content.get('model') + " (wrapped in " + content + ")") : ("" + content)), Ember.Array.detect(content)); + Ember.assert(fmt("The value that #each loops over must be an Array. You " + + "passed %@, but it should have been an ArrayController", + [content.constructor]), + !Ember.ControllerMixin.detect(content) || + (content && content.isGenerated) || + content instanceof Ember.ArrayController); + Ember.assert(fmt("The value that #each loops over must be an Array. You passed %@", [(Ember.ControllerMixin.detect(content) && content.get('model') !== undefined) ? fmt("'%@' (wrapped in %@)", [content.get('model'), content]) : content]), Ember.Array.detect(content)); }, disableContentObservers: function(callback) { @@ -26501,7 +29967,7 @@ GroupedEach.prototype = { @param [options.itemController] {String} name of a controller to be created for each item @param [options.groupedRows] {boolean} enable normal item-by-item rendering when inside a `#group` helper */ -Ember.Handlebars.registerHelper('each', function(path, options) { +Ember.Handlebars.registerHelper('each', function eachHelper(path, options) { if (arguments.length === 4) { Ember.assert("If you pass more than one argument to the each helper, it must be in the form #each foo in bar", arguments[1] === "in"); @@ -26600,24 +30066,48 @@ Ember.Handlebars.registerHelper('template', function(name, options) { */ /** - `partial` renders a template directly using the current context. - If needed the context can be set using the `{{#with foo}}` helper. + The `partial` helper renders another template without + changing the template context: - ```html - + ```handlebars + {{foo}} + {{partial "nav"}} ``` - The `data-template-name` attribute of a partial template - is prefixed with an underscore. + The above example template will render a template named + "_nav", which has the same context as the parent template + it's rendered into, so if the "_nav" template also referenced + `{{foo}}`, it would print the same thing as the `{{foo}}` + in the above example. - ```html - + If a "_nav" template isn't found, the `partial` helper will + fall back to a template named "nav". + + ## Bound template names + + The parameter supplied to `partial` can also be a path + to a property containing a template name, e.g.: + + ```handlebars + {{partial someTemplateName}} + ``` + + The above example will look up the value of `someTemplateName` + on the template context (e.g. a controller) and use that + value as the name of the template to render. If the resolved + value is falsy, nothing will be rendered. If `someTemplateName` + changes, the partial will be re-rendered using the new template + name. + + ## Setting the partial's context with `with` + + The `partial` helper can be used in conjunction with the `with` + helper to set a context that will be used by the partial: + + ```handlebars + {{#with currentUser}} + {{partial "user_info"}} + {{/with}} ``` @method partial @@ -26625,7 +30115,31 @@ Ember.Handlebars.registerHelper('template', function(name, options) { @param {String} partialName the name of the template to render minus the leading underscore */ -Ember.Handlebars.registerHelper('partial', function(name, options) { +Ember.Handlebars.registerHelper('partial', function partialHelper(name, options) { + + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this; + + if (options.types[0] === "ID") { + // Helper was passed a property path; we need to + // create a binding that will re-render whenever + // this property changes. + options.fn = function(context, fnOptions) { + var partialName = Ember.Handlebars.get(context, name, fnOptions); + renderPartial(context, partialName, fnOptions); + }; + + return Ember.Handlebars.bind.call(context, name, options, true, exists); + } else { + // Render the partial right into parent template. + renderPartial(context, name, options); + } +}); + +function exists(value) { + return !Ember.isNone(value); +} + +function renderPartial(context, name, options) { var nameParts = name.split("/"), lastPart = nameParts[nameParts.length - 1]; @@ -26640,8 +30154,8 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { template = template || deprecatedTemplate; - template(this, { data: options.data }); -}); + template(context, { data: options.data }); +} })(); @@ -26656,7 +30170,6 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { var get = Ember.get, set = Ember.set; /** - `{{yield}}` denotes an area of a template that will be rendered inside of another template. It has two main uses: @@ -26715,11 +30228,11 @@ var get = Ember.get, set = Ember.set; {{#labeled-textfield value=someProperty}} First name: - {{/my-component}} + {{/labeled-textfield}} ``` ```handlebars - + @@ -26738,7 +30251,7 @@ var get = Ember.get, set = Ember.set; @param {Hash} options @return {String} HTML string */ -Ember.Handlebars.registerHelper('yield', function(options) { +Ember.Handlebars.registerHelper('yield', function yieldHelper(options) { var view = options.data.view; while (view && !get(view, 'layout')) { @@ -26782,7 +30295,7 @@ Ember.Handlebars.registerHelper('yield', function(options) { @param {String} str The string to format */ -Ember.Handlebars.registerHelper('loc', function(str) { +Ember.Handlebars.registerHelper('loc', function locHelper(str) { return Ember.String.loc(str); }); @@ -26814,7 +30327,7 @@ var set = Ember.set, get = Ember.get; The internal class used to create text inputs when the `{{input}}` helper is used with `type` of `checkbox`. - See Handlebars.helpers.input for usage details. + See [handlebars.helpers.input](/api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. ## Direct manipulation of `checked` @@ -26883,7 +30396,7 @@ var get = Ember.get, set = Ember.set; Ember.TextSupport = Ember.Mixin.create({ value: "", - attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex'], + attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex', 'readonly'], placeholder: null, disabled: false, maxlength: null, @@ -26917,7 +30430,7 @@ Ember.TextSupport = Ember.Mixin.create({ Options are: * `enter`: the user pressed enter - * `keypress`: the user pressed a key + * `keyPress`: the user pressed a key @property onEvent @type String @@ -26960,7 +30473,7 @@ Ember.TextSupport = Ember.Mixin.create({ Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 13. Uses sendAction to send the `enter` action to the controller. - @method insertNewLine + @method insertNewline @param {Event} event */ insertNewline: function(event) { @@ -26971,8 +30484,8 @@ Ember.TextSupport = Ember.Mixin.create({ /** Called when the user hits escape. - Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 13. - Uses sendAction to send the `enter` action to the controller. + Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 27. + Uses sendAction to send the `escape-press` action to the controller. @method cancel @param {Event} event @@ -27062,7 +30575,7 @@ var get = Ember.get, set = Ember.set; The internal class used to create text inputs when the `{{input}}` helper is used with `type` of `text`. - See Handlebars.helpers.input for usage details. + See [Handlebars.helpers.input](/api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. ## Layout and LayoutName properties @@ -27072,15 +30585,14 @@ var get = Ember.get, set = Ember.set; @class TextField @namespace Ember - @extends Ember.View + @extends Ember.Component @uses Ember.TextSupport */ -Ember.TextField = Ember.Component.extend(Ember.TextSupport, - /** @scope Ember.TextField.prototype */ { +Ember.TextField = Ember.Component.extend(Ember.TextSupport, { classNames: ['ember-text-field'], tagName: "input", - attributeBindings: ['type', 'value', 'size', 'pattern', 'name'], + attributeBindings: ['type', 'value', 'size', 'pattern', 'name', 'min', 'max'], /** The `value` attribute of the input element. As the user inputs text, this @@ -27111,141 +30623,31 @@ Ember.TextField = Ember.Component.extend(Ember.TextSupport, size: null, /** - The `pattern` the pattern attribute of input element. + The `pattern` attribute of input element. @property pattern @type String @default null */ - pattern: null -}); - -})(); - - - -(function() { -/* -@module ember -@submodule ember-handlebars -*/ - -var get = Ember.get, set = Ember.set; - -/* - @class Button - @namespace Ember - @extends Ember.View - @uses Ember.TargetActionSupport - @deprecated -*/ -Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { - classNames: ['ember-button'], - classNameBindings: ['isActive'], + pattern: null, - tagName: 'button', - - propagateEvents: false, - - attributeBindings: ['type', 'disabled', 'href', 'tabindex'], - - /* - @private - - Overrides `TargetActionSupport`'s `targetObject` computed - property to use Handlebars-specific path resolution. + /** + The `min` attribute of input element used with `type="number"` or `type="range"`. - @property targetObject + @property min + @type String + @default null */ - targetObject: Ember.computed(function() { - var target = get(this, 'target'), - root = get(this, 'context'), - data = get(this, 'templateData'); - - if (typeof target !== 'string') { return target; } - - return Ember.Handlebars.get(root, target, { data: data }); - }).property('target'), - - // Defaults to 'button' if tagName is 'input' or 'button' - type: Ember.computed(function(key) { - var tagName = this.tagName; - if (tagName === 'input' || tagName === 'button') { return 'button'; } - }), - - disabled: false, + min: null, - // Allow 'a' tags to act like buttons - href: Ember.computed(function() { - return this.tagName === 'a' ? '#' : null; - }), - - mouseDown: function() { - if (!get(this, 'disabled')) { - set(this, 'isActive', true); - this._mouseDown = true; - this._mouseEntered = true; - } - return get(this, 'propagateEvents'); - }, - - mouseLeave: function() { - if (this._mouseDown) { - set(this, 'isActive', false); - this._mouseEntered = false; - } - }, - - mouseEnter: function() { - if (this._mouseDown) { - set(this, 'isActive', true); - this._mouseEntered = true; - } - }, - - mouseUp: function(event) { - if (get(this, 'isActive')) { - // Actually invoke the button's target and action. - // This method comes from the Ember.TargetActionSupport mixin. - this.triggerAction(); - set(this, 'isActive', false); - } - - this._mouseDown = false; - this._mouseEntered = false; - return get(this, 'propagateEvents'); - }, - - keyDown: function(event) { - // Handle space or enter - if (event.keyCode === 13 || event.keyCode === 32) { - this.mouseDown(); - } - }, - - keyUp: function(event) { - // Handle space or enter - if (event.keyCode === 13 || event.keyCode === 32) { - this.mouseUp(); - } - }, - - // TODO: Handle proper touch behavior. Including should make inactive when - // finger moves more than 20x outside of the edge of the button (vs mouse - // which goes inactive as soon as mouse goes out of edges.) - - touchStart: function(touch) { - return this.mouseDown(touch); - }, - - touchEnd: function(touch) { - return this.mouseUp(touch); - }, + /** + The `max` attribute of input element used with `type="number"` or `type="range"`. - init: function() { - Ember.deprecate("Ember.Button is deprecated and will be removed from future releases. Consider using the `{{action}}` helper."); - this._super(); - } + @property max + @type String + @default null + */ + max: null }); })(); @@ -27264,7 +30666,7 @@ var get = Ember.get, set = Ember.set; The internal class used to create textarea element when the `{{textarea}}` helper is used. - See handlebars.helpers.textarea for usage details. + See [handlebars.helpers.textarea](/api/classes/Ember.Handlebars.helpers.html#method_textarea) for usage details. ## Layout and LayoutName properties @@ -27274,7 +30676,7 @@ var get = Ember.get, set = Ember.set; @class TextArea @namespace Ember - @extends Ember.View + @extends Ember.Component @uses Ember.TextSupport */ Ember.TextArea = Ember.Component.extend(Ember.TextSupport, { @@ -27285,14 +30687,14 @@ Ember.TextArea = Ember.Component.extend(Ember.TextSupport, { rows: null, cols: null, - _updateElementValue: Ember.observer(function() { + _updateElementValue: Ember.observer('value', function() { // We do this check so cursor position doesn't get affected in IE var value = get(this, 'value'), $el = this.$(); if ($el && value !== $el.val()) { $el.val(value); } - }, 'value'), + }), init: function() { this._super(); @@ -27350,7 +30752,7 @@ Ember.SelectOption = Ember.View.extend({ } }).property('content', 'parentView.selection'), - labelPathDidChange: Ember.observer(function() { + labelPathDidChange: Ember.observer('parentView.optionLabelPath', function() { var labelPath = get(this, 'parentView.optionLabelPath'); if (!labelPath) { return; } @@ -27358,9 +30760,9 @@ Ember.SelectOption = Ember.View.extend({ Ember.defineProperty(this, 'label', Ember.computed(function() { return get(this, labelPath); }).property(labelPath)); - }, 'parentView.optionLabelPath'), + }), - valuePathDidChange: Ember.observer(function() { + valuePathDidChange: Ember.observer('parentView.optionValuePath', function() { var valuePath = get(this, 'parentView.optionValuePath'); if (!valuePath) { return; } @@ -27368,7 +30770,7 @@ Ember.SelectOption = Ember.View.extend({ Ember.defineProperty(this, 'value', Ember.computed(function() { return get(this, valuePath); }).property(valuePath)); - }, 'parentView.optionValuePath') + }) }); Ember.SelectOptgroup = Ember.CollectionView.extend({ @@ -27393,7 +30795,7 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ `content` property. The underlying data object of the selected `