diff --git a/FEATURES.md b/FEATURES.md index d888fd8a87b..f221dbedd26 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -57,3 +57,8 @@ for a detailed explanation. - `interaction.` for events handled by a component. - `interaction.ember-action` for closure actions. - `interaction.link-to` for link-to execution. + +* `ember-runtime-enumerable-includes` + +Deprecates `Enumerable#contains` and `Array#contains` in favor of `Enumerable#includes` and `Array#includes` +to stay in line with ES standards (see [RFC](https://github.com/emberjs/rfcs/blob/master/text/0136-contains-to-includes.md)). diff --git a/features.json b/features.json index 6597ead596c..9c4c656d9f1 100644 --- a/features.json +++ b/features.json @@ -10,6 +10,7 @@ "ember-route-serializers": null, "ember-glimmer": null, "ember-runtime-computed-uniq-by": null, - "ember-improved-instrumentation": null + "ember-improved-instrumentation": null, + "ember-runtime-enumerable-includes": null } } diff --git a/packages/ember-runtime/lib/mixins/array.js b/packages/ember-runtime/lib/mixins/array.js index 40631a110ca..43fbbf40189 100644 --- a/packages/ember-runtime/lib/mixins/array.js +++ b/packages/ember-runtime/lib/mixins/array.js @@ -30,6 +30,8 @@ import { import { meta as metaFor } from 'ember-metal/meta'; import { markObjectAsDirty } from 'ember-metal/tags'; import EachProxy from 'ember-runtime/system/each_proxy'; +import { deprecate } from 'ember-metal/debug'; +import isEnabled from 'ember-metal/features'; function arrayObserversHelper(obj, target, opts, operation, notify) { var willChange = (opts && opts.willChange) || 'arrayWillChange'; @@ -112,7 +114,7 @@ export function isEmberArray(obj) { @since Ember 0.9.0 @public */ -export default Mixin.create(Enumerable, { +var ArrayMixin = Mixin.create(Enumerable, { [EMBER_ARRAY]: true, @@ -214,6 +216,14 @@ export default Mixin.create(Enumerable, { // optimized version from Enumerable contains(obj) { + if (isEnabled('ember-runtime-enumerable-includes')) { + deprecate( + '`Enumerable#contains` is deprecated, use `Enumerable#includes` instead.', + false, + { id: 'ember-runtime.enumerable-contains', until: '3.0.0', url: 'http://emberjs.com/deprecations/v2.x#toc_enumerable-contains' } + ); + } + return this.indexOf(obj) >= 0; }, @@ -573,3 +583,55 @@ export default Mixin.create(Enumerable, { return this.__each; }).volatile() }); + +if (isEnabled('ember-runtime-enumerable-includes')) { + ArrayMixin.reopen({ + /** + Returns `true` if the passed object can be found in the array. + This method is a Polyfill for ES 2016 Array.includes. + If no `startAt` argument is given, the starting location to + search is 0. If it's negative, searches from the index of + `this.length + startAt` by asc. + ```javascript + [1, 2, 3].includes(2); // true + [1, 2, 3].includes(4); // false + [1, 2, 3].includes(3, 2); // true + [1, 2, 3].includes(3, 3); // false + [1, 2, 3].includes(3, -1); // true + [1, 2, 3].includes(1, -1); // false + [1, 2, 3].includes(1, -4); // true + [1, 2, NaN].includes(NaN); // true + ``` + @method includes + @param {Object} obj The object to search for. + @param {Number} startAt optional starting location to search, default 0 + @return {Boolean} `true` if object is found in the array. + @public + */ + includes(obj, startAt) { + var len = get(this, 'length'); + var idx, currentObj; + + if (startAt === undefined) { + startAt = 0; + } + + if (startAt < 0) { + startAt += len; + } + + for (idx = startAt; idx < len; idx++) { + currentObj = objectAt(this, idx); + + // SameValueZero comparison (NaN !== NaN) + if (obj === currentObj || (obj !== obj && currentObj !== currentObj)) { + return true; + } + } + + return false; + } + }); +} + +export default ArrayMixin; diff --git a/packages/ember-runtime/lib/mixins/enumerable.js b/packages/ember-runtime/lib/mixins/enumerable.js index 0b349ad148e..cc62215e7c0 100644 --- a/packages/ember-runtime/lib/mixins/enumerable.js +++ b/packages/ember-runtime/lib/mixins/enumerable.js @@ -29,6 +29,7 @@ import { } from 'ember-metal/events'; import compare from 'ember-runtime/compare'; import require from 'require'; +import { assert, deprecate } from 'ember-metal/debug'; let _emberA; @@ -231,6 +232,14 @@ var Enumerable = Mixin.create({ @public */ contains(obj) { + if (isEnabled('ember-runtime-enumerable-includes')) { + deprecate( + '`Enumerable#contains` is deprecated, use `Enumerable#includes` instead.', + false, + { id: 'ember-runtime.enumerable-contains', until: '3.0.0', url: 'http://emberjs.com/deprecations/v2.x#toc_enumerable-contains' } + ); + } + var found = this.find(function(item) { return item === obj; }); @@ -797,8 +806,8 @@ var Enumerable = Mixin.create({ /** 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. + implementation returns an array regardless of the receiver type. + If the receiver does not contain the value it returns the original enumerable. ```javascript var arr = ['a', 'b', 'a', 'c']; @@ -1118,4 +1127,63 @@ if (isEnabled('ember-runtime-computed-uniq-by')) { }); } +if (isEnabled('ember-runtime-enumerable-includes')) { + Enumerable.reopen({ + /** + Returns `true` if the passed object can be found in the enumerable. + ```javascript + [1, 2, 3].includes(2); // true + [1, 2, 3].includes(4); // false + [1, 2, undefined].includes(undefined); // true + [1, 2, null].includes(null); // true + [1, 2, NaN].includes(NaN); // true + ``` + @method includes + @param {Object} obj The object to search for. + @return {Boolean} `true` if object is found in the enumerable. + @public + */ + includes(obj) { + assert('Enumerable#includes cannot accept a second argument "startAt" as enumerable items are unordered.', arguments.length === 1); + + var len = get(this, 'length'); + var idx, next; + var last = null; + var found = false; + + var context = popCtx(); + + for (idx = 0; idx < len && !found; idx++) { + next = this.nextObject(idx, last, context); + + found = obj === next || (obj !== obj && next !== next); + + last = next; + } + + next = last = null; + context = pushCtx(context); + + return found; + }, + + without(value) { + if (!this.includes(value)) { + return this; // nothing to do + } + + var ret = emberA(); + + this.forEach(function(k) { + // SameValueZero comparison (NaN !== NaN) + if (!(k === value || k !== k && value !== value)) { + ret[ret.length] = k; + } + }); + + return ret; + } + }); +} + export default Enumerable; diff --git a/packages/ember-runtime/lib/mixins/mutable_array.js b/packages/ember-runtime/lib/mixins/mutable_array.js index d6deb9c3a60..873f7c3ed32 100644 --- a/packages/ember-runtime/lib/mixins/mutable_array.js +++ b/packages/ember-runtime/lib/mixins/mutable_array.js @@ -24,6 +24,7 @@ import { Mixin } from 'ember-metal/mixin'; import EmberArray, { objectAt } from 'ember-runtime/mixins/array'; import MutableEnumerable from 'ember-runtime/mixins/mutable_enumerable'; import Enumerable from 'ember-runtime/mixins/enumerable'; +import isEnabled from 'ember-metal/features'; /** This mixin defines the API for modifying array-like objects. These methods @@ -388,7 +389,15 @@ export default Mixin.create(EmberArray, MutableEnumerable, { @public */ addObject(obj) { - if (!this.contains(obj)) { + var included; + + if (isEnabled('ember-runtime-enumerable-includes')) { + included = this.includes(obj); + } else { + included = this.contains(obj); + } + + if (!included) { this.pushObject(obj); } diff --git a/packages/ember-runtime/tests/mixins/enumerable_test.js b/packages/ember-runtime/tests/mixins/enumerable_test.js index 4801b37848c..37618f9a356 100644 --- a/packages/ember-runtime/tests/mixins/enumerable_test.js +++ b/packages/ember-runtime/tests/mixins/enumerable_test.js @@ -6,6 +6,7 @@ import { A as emberA } from 'ember-runtime/system/native_array'; import { get } from 'ember-metal/property_get'; import { computed } from 'ember-metal/computed'; import { observer as emberObserver } from 'ember-metal/mixin'; +import isEnabled from 'ember-metal/features'; function K() { return this; } @@ -93,11 +94,21 @@ QUnit.test('should apply Ember.Array to return value of toArray', function() { }); QUnit.test('should apply Ember.Array to return value of without', function() { - var x = EmberObject.extend(Enumerable, { + var X = EmberObject.extend(Enumerable, { contains() { return true; } - }).create(); + }); + + if (isEnabled('ember-runtime-enumerable-includes')) { + X.reopen({ + includes() { + return true; + } + }); + } + + var x = X.create(); var y = x.without(K); equal(EmberArray.detect(y), true, 'should have mixin applied'); }); @@ -164,6 +175,17 @@ QUnit.test('every', function() { equal(allWhite, true); }); +if (isEnabled('ember-runtime-enumerable-includes')) { + QUnit.test('should throw an error passing a second argument to includes', function() { + var x = EmberObject.extend(Enumerable).create(); + + equal(x.includes('any'), false); + expectAssertion(() => { + x.includes('any', 1); + }, /Enumerable#includes cannot accept a second argument "startAt" as enumerable items are unordered./); + }); +} + // .......................................................... // CONTENT DID CHANGE // diff --git a/packages/ember-runtime/tests/suites/array.js b/packages/ember-runtime/tests/suites/array.js index dab019c247b..55478eaaaf6 100644 --- a/packages/ember-runtime/tests/suites/array.js +++ b/packages/ember-runtime/tests/suites/array.js @@ -5,10 +5,12 @@ import { import indexOfTests from 'ember-runtime/tests/suites/array/indexOf'; import lastIndexOfTests from 'ember-runtime/tests/suites/array/lastIndexOf'; import objectAtTests from 'ember-runtime/tests/suites/array/objectAt'; +import includesTests from 'ember-runtime/tests/suites/array/includes'; import { addArrayObserver, removeArrayObserver } from 'ember-runtime/mixins/array'; +import isEnabled from 'ember-metal/features'; var ObserverClass = EnumerableTestsObserverClass.extend({ @@ -46,4 +48,8 @@ ArrayTests.importModuleTests(indexOfTests); ArrayTests.importModuleTests(lastIndexOfTests); ArrayTests.importModuleTests(objectAtTests); +if (isEnabled('ember-runtime-enumerable-includes')) { + ArrayTests.importModuleTests(includesTests); +} + export {ArrayTests, ObserverClass}; diff --git a/packages/ember-runtime/tests/suites/array/includes.js b/packages/ember-runtime/tests/suites/array/includes.js new file mode 100644 index 00000000000..f47eb1c4fad --- /dev/null +++ b/packages/ember-runtime/tests/suites/array/includes.js @@ -0,0 +1,39 @@ +import {SuiteModuleBuilder} from 'ember-runtime/tests/suites/suite'; + +var suite = SuiteModuleBuilder.create(); + +suite.module('includes'); + +suite.test('includes returns correct value if startAt is positive', function() { + var data = this.newFixture(3); + var obj = this.newObject(data); + + equal(obj.includes(data[1], 1), true, 'should return true if included'); + equal(obj.includes(data[0], 1), false, 'should return false if not included'); +}); + +suite.test('includes returns correct value if startAt is negative', function() { + var data = this.newFixture(3); + var obj = this.newObject(data); + + equal(obj.includes(data[1], -2), true, 'should return true if included'); + equal(obj.includes(data[0], -2), false, 'should return false if not included'); +}); + +suite.test('includes returns true if startAt + length is still negative', function() { + var data = this.newFixture(1); + var obj = this.newObject(data); + + equal(obj.includes(data[0], -2), true, 'should return true if included'); + equal(obj.includes(this.newFixture(1), -2), false, 'should return false if not included'); +}); + +suite.test('includes returns false if startAt out of bounds', function() { + var data = this.newFixture(1); + var obj = this.newObject(data); + + equal(obj.includes(data[0], 2), false, 'should return false if startAt >= length'); + equal(obj.includes(this.newFixture(1), 2), false, 'should return false if startAt >= length'); +}); + +export default suite; diff --git a/packages/ember-runtime/tests/suites/enumerable.js b/packages/ember-runtime/tests/suites/enumerable.js index b5d646dd813..c44b1fafc8b 100644 --- a/packages/ember-runtime/tests/suites/enumerable.js +++ b/packages/ember-runtime/tests/suites/enumerable.js @@ -285,6 +285,7 @@ import anyTests from 'ember-runtime/tests/suites/enumerable/any'; import isAnyTests from 'ember-runtime/tests/suites/enumerable/is_any'; import compactTests from 'ember-runtime/tests/suites/enumerable/compact'; import containsTests from 'ember-runtime/tests/suites/enumerable/contains'; +import includesTests from 'ember-runtime/tests/suites/enumerable/includes'; import everyTests from 'ember-runtime/tests/suites/enumerable/every'; import filterTests from 'ember-runtime/tests/suites/enumerable/filter'; import findTests from 'ember-runtime/tests/suites/enumerable/find'; @@ -325,6 +326,10 @@ if (isEnabled('ember-runtime-computed-uniq-by')) { EnumerableTests.importModuleTests(uniqByTests); } +if (isEnabled('ember-runtime-enumerable-includes')) { + EnumerableTests.importModuleTests(includesTests); +} + EnumerableTests.importModuleTests(withoutTests); export default EnumerableTests; diff --git a/packages/ember-runtime/tests/suites/enumerable/contains.js b/packages/ember-runtime/tests/suites/enumerable/contains.js index 79f61891540..295c4cfd1b0 100644 --- a/packages/ember-runtime/tests/suites/enumerable/contains.js +++ b/packages/ember-runtime/tests/suites/enumerable/contains.js @@ -1,18 +1,27 @@ import {SuiteModuleBuilder} from 'ember-runtime/tests/suites/suite'; +import isEnabled from 'ember-metal/features'; var suite = SuiteModuleBuilder.create(); suite.module('contains'); -suite.test('contains returns true if items is in enumerable', function() { +suite.test('contains returns true if item is in enumerable', function() { var data = this.newFixture(3); var obj = this.newObject(data); + + if (isEnabled('ember-runtime-enumerable-includes')) { + expectDeprecation('`Enumerable#contains` is deprecated, use `Enumerable#includes` instead.'); + } equal(obj.contains(data[1]), true, 'should return true if contained'); }); suite.test('contains returns false if item is not in enumerable', function() { var data = this.newFixture(1); var obj = this.newObject(this.newFixture(3)); + + if (isEnabled('ember-runtime-enumerable-includes')) { + expectDeprecation('`Enumerable#contains` is deprecated, use `Enumerable#includes` instead.'); + } equal(obj.contains(data[0]), false, 'should return false if not contained'); }); diff --git a/packages/ember-runtime/tests/suites/enumerable/includes.js b/packages/ember-runtime/tests/suites/enumerable/includes.js new file mode 100644 index 00000000000..ef0e4ac0dc7 --- /dev/null +++ b/packages/ember-runtime/tests/suites/enumerable/includes.js @@ -0,0 +1,25 @@ +import {SuiteModuleBuilder} from 'ember-runtime/tests/suites/suite'; + +var suite = SuiteModuleBuilder.create(); + +suite.module('includes'); + +suite.test('includes returns true if item is in enumerable', function() { + var data = this.newFixture(1); + var obj = this.newObject([...data, NaN, undefined, null]); + + equal(obj.includes(data[0]), true, 'should return true if included'); + equal(obj.includes(NaN), true, 'should return true if NaN included'); + equal(obj.includes(undefined), true, 'should return true if undefined included'); + equal(obj.includes(null), true, 'should return true if null included'); +}); + +suite.test('includes returns false if item is not in enumerable', function() { + var data = this.newFixture(1); + var obj = this.newObject([...this.newFixture(3), null]); + + equal(obj.includes(data[0]), false, 'should return false if not included'); + equal(obj.includes(undefined), false, 'should return false if undefined not included but null is included'); +}); + +export default suite; diff --git a/packages/ember-runtime/tests/suites/enumerable/without.js b/packages/ember-runtime/tests/suites/enumerable/without.js index 117866fdbe7..2277c0f1efc 100644 --- a/packages/ember-runtime/tests/suites/enumerable/without.js +++ b/packages/ember-runtime/tests/suites/enumerable/without.js @@ -1,4 +1,5 @@ import {SuiteModuleBuilder} from 'ember-runtime/tests/suites/suite'; +import isEnabled from 'ember-metal/features'; var suite = SuiteModuleBuilder.create(); @@ -16,6 +17,19 @@ suite.test('should return new instance with item removed', function() { deepEqual(this.toArray(obj), before, 'should not have changed original'); }); +if (isEnabled('ember-runtime-enumerable-includes')) { + suite.test('should remove NaN value', function() { + var before, after, obj, ret; + + before = [...this.newFixture(2), NaN]; + after = [before[0], before[1]]; + obj = this.newObject(before); + + ret = obj.without(NaN); + deepEqual(this.toArray(ret), after, 'should have removed item'); + }); +} + suite.test('should return same instance if object not found', function() { var item, obj, ret; diff --git a/packages/ember/tests/component_registration_test.js b/packages/ember/tests/component_registration_test.js index e8f7f84a55f..f18df7fed71 100644 --- a/packages/ember/tests/component_registration_test.js +++ b/packages/ember/tests/component_registration_test.js @@ -11,6 +11,7 @@ import jQuery from 'ember-views/system/jquery'; import { A as emberA } from 'ember-runtime/system/native_array'; import { setTemplates, set as setTemplate } from 'ember-templates/template_registry'; import { test } from 'ember-glimmer/tests/utils/skip-if-glimmer'; +import isEnabled from 'ember-metal/features'; var App, appInstance; var originalHelpers; @@ -39,9 +40,17 @@ function cleanup() { } function cleanupHelpers() { + var included; + keys(helpers). forEach((name) => { - if (!originalHelpers.contains(name)) { + if (isEnabled('ember-runtime-enumerable-includes')) { + included = originalHelpers.includes(name); + } else { + included = originalHelpers.contains(name); + } + + if (!included) { delete helpers[name]; } });