diff --git a/packages/ember-runtime/lib/controllers/array_controller.js b/packages/ember-runtime/lib/controllers/array_controller.js index 8c564b37d3e..5d0c3b318c0 100644 --- a/packages/ember-runtime/lib/controllers/array_controller.js +++ b/packages/ember-runtime/lib/controllers/array_controller.js @@ -7,6 +7,7 @@ require('ember-runtime/system/array_proxy'); require('ember-runtime/controllers/controller'); require('ember-runtime/mixins/sortable'); +require('ember-runtime/mixins/filterable'); var get = Ember.get, set = Ember.set; diff --git a/packages/ember-runtime/lib/mixins/filterable.js b/packages/ember-runtime/lib/mixins/filterable.js new file mode 100644 index 00000000000..5a14abab97c --- /dev/null +++ b/packages/ember-runtime/lib/mixins/filterable.js @@ -0,0 +1,140 @@ +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; + +/** + @class + + @extends Ember.Mixin + @extends Ember.MutableEnumerable +*/ +Ember.FilterableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { + filterProperties: null, + + filterCondition: function(item){ + var filterProperties = get(this, 'filterProperties'); + return Ember.A(filterProperties).every(function(property){ + return !!get(item, property); + }); + }, + + addObject: function(obj) { + var content = get(this, 'content'); + content.pushObject(obj); + }, + + removeObject: function(obj) { + var content = get(this, 'content'); + content.removeObject(obj); + }, + + destroy: function() { + var content = get(this, 'content'), + filterProperties = get(this, 'filterProperties'); + + if (content && filterProperties) { + forEach(content, function(item) { + forEach(filterProperties, function(filterProperty) { + Ember.removeObserver(item, filterProperty, this, 'contentItemFilterPropertyDidChange'); + }, this); + }, this); + } + + return this._super(); + }, + + isFiltered: Ember.computed('filterProperties', function() { + return !!get(this, 'filterProperties'); + }), + + arrangedContent: Ember.computed('content', 'filterProperties.@each', function(key, value) { + var content = get(this, 'content'), + isFiltered = get(this, 'isFiltered'), + filterProperties = get(this, 'filterProperties'), + filterValue = get(this, 'filterValue'), + self = this; + + if (content && isFiltered) { + forEach(content, function(item) { + forEach(filterProperties, function(filterProperty) { + Ember.addObserver(item, filterProperty, this, 'contentItemFilterPropertyDidChange'); + }, this); + }, this); + content = content.slice(); + content = content.filter(this.filterCondition, this); + + return Ember.A(content); + } + + return content; + }).cacheable(), + + _contentWillChange: Ember.beforeObserver(function() { + var content = get(this, 'content'), + filterProperties = get(this, 'filterProperties'); + + if (content && filterProperties) { + forEach(content, function(item) { + forEach(filterProperties, function(filterProperty) { + Ember.removeObserver(item, filterProperty, this, 'contentItemFilterPropertyDidChange'); + }, this); + }, this); + } + + this._super(); + }, 'content'), + + contentArrayWillChange: function(array, idx, removedCount, addedCount) { + var isFiltered = get(this, 'isFiltered'); + + if (isFiltered) { + var arrangedContent = get(this, 'arrangedContent'); + var removedObjects = array.slice(idx, idx+removedCount); + var filterProperties = get(this, 'filterProperties'); + + forEach(removedObjects, function(item) { + arrangedContent.removeObject(item); + forEach(filterProperties, function(filterProperty) { + Ember.removeObserver(item, filterProperty, this, 'contentItemFilterPropertyDidChange'); + }, this); + }); + } + + return this._super(array, idx, removedCount, addedCount); + }, + + contentArrayDidChange: function(array, idx, removedCount, addedCount) { + var isFiltered = get(this, 'isFiltered'), + filterProperties = get(this, 'filterProperties'); + + if (isFiltered) { + var addedObjects = array.slice(idx, idx+addedCount); + var arrangedContent = get(this, 'arrangedContent'); + + forEach(addedObjects, function(item) { + this.insertItemFiltered(item); + + forEach(filterProperties, function(filterProperty) { + Ember.addObserver(item, filterProperty, this, 'contentItemFilterPropertyDidChange'); + }, this); + }, this); + } + + return this._super(array, idx, removedCount, addedCount); + }, + + contentItemFilterPropertyDidChange: function(item) { + var arrangedContent = get(this, 'arrangedContent'), + index = arrangedContent.indexOf(item); + + arrangedContent.removeObject(item); + this.insertItemFiltered(item); + }, + + insertItemFiltered: function(item){ + var arrangedContent = get(this, 'arrangedContent'); + + if( this.filterCondition(item) ){ + arrangedContent.pushObject(item); + } + } + +}); diff --git a/packages/ember-runtime/tests/mixins/filterable_test.js b/packages/ember-runtime/tests/mixins/filterable_test.js new file mode 100644 index 00000000000..ae061715e4f --- /dev/null +++ b/packages/ember-runtime/tests/mixins/filterable_test.js @@ -0,0 +1,179 @@ +var get = Ember.get, set = Ember.set; + + +var array, unfilteredArray, filteredArrayController; + +module("Ember.Filterable"); + +module("Ember.Filterable with content", { + setup: function() { + Ember.run(function() { + array = [{ id: 1, name: "Scumbag Dale" }, { id: 2, name: "Scumbag Katz" }, { id: 3, name: "Scumbag Bryn" }]; + unfilteredArray = Ember.A(array); + + filteredArrayController = Ember.ArrayProxy.create(Ember.FilterableMixin, { + content: unfilteredArray + }); + }); + }, + + teardown: function() { + Ember.run(function() { + filteredArrayController.set('content', null); + filteredArrayController.destroy(); + }); + } +}); + +test("if you do not specify `filterProperties` filterable has no effect", function() { + equal(filteredArrayController.get('length'), 3, 'array has 3 items'); + + unfilteredArray.pushObject({id: 4, name: 'Scumbag Chavard'}); + + equal(filteredArrayController.get('length'), 4, 'array has 4 items'); +}); + +test("you can change the filterProperties and filterCondition", function() { + equal(filteredArrayController.get('length'), 3, 'precond - array has 3 items'); + + filteredArrayController.filterCondition = function(item){ return get(item, 'id') === 1; }; + filteredArrayController.set('filterProperties', ['id']); + + equal(filteredArrayController.get('length'), 1, 'array has 1 item'); + equal(filteredArrayController.objectAt(0).name, 'Scumbag Dale', 'array is filtered by id'); +}); + + +module("Ember.Filterable with content, filterProperties and filterCondition", { + setup: function() { + Ember.run(function() { + array = [{ id: 1, name: "Scumbag Dale" }, { id: 2, name: "Scumbag Katz" }, { id: 3, name: "Scumbag Bryn" }]; + unfilteredArray = Ember.A(array); + + filteredArrayController = Ember.ArrayProxy.create(Ember.FilterableMixin, { + content: unfilteredArray, + filterProperties: ['id'], + filterCondition: function(item){ return get(item, 'id') === 1; } + }); + }); + }, + + teardown: function() { + Ember.run(function() { + filteredArrayController.destroy(); + }); + } +}); + +test("filtered object will expose filtered content", function() { + equal(filteredArrayController.get('length'), 1, 'array is filtered by id'); + equal(filteredArrayController.objectAt(0).name, 'Scumbag Dale', 'the object is the correct one'); +}); + +test("you can add objects in the filtered array", function() { + equal(filteredArrayController.get('length'), 1, 'array has 1 item'); + + unfilteredArray.pushObject({id: 1, name: 'Scumbag Chavard'}); + + equal(filteredArrayController.get('length'), 2, 'array has 2 items'); + equal(filteredArrayController.objectAt(1).name, 'Scumbag Chavard', 'a new object added to content was inserted according to given constraint'); + + unfilteredArray.addObject({id: 1, name: 'Scumbag Fucs'}); + + equal(filteredArrayController.get('length'), 3, 'array has 3 items'); + equal(filteredArrayController.objectAt(2).name, 'Scumbag Fucs', 'a new object added to controller was inserted according to given constraint'); +}); + +test("new objects don't get added if they don't meet the filter condition", function() { + equal(filteredArrayController.get('length'), 1, 'array has 1 item'); + + unfilteredArray.pushObject({id: 5, name: 'Scumbag Chavard'}); + + equal(filteredArrayController.get('length'), 1, 'array has 1 item'); +}); + +test("you can change a filter property and the content will be removed", function() { + equal(filteredArrayController.get('length'), 1, 'array has 1 item'); + equal(filteredArrayController.objectAt(0).name, 'Scumbag Dale', 'dale is the only one'); + + set(filteredArrayController.objectAt(0), 'id', 2); + + equal(filteredArrayController.get('length'), 0, 'array has no items'); +}); + +test("you can change a filter property and the content will be added", function() { + equal(filteredArrayController.get('length'), 1, 'array has 1 item'); + equal(filteredArrayController.objectAt(0).name, 'Scumbag Dale', 'dale is the only one'); + + set(unfilteredArray.objectAt(1), 'id', 1); + + equal(filteredArrayController.get('length'), 2, 'array has two items'); + equal(filteredArrayController.objectAt(0).name, 'Scumbag Dale', 'dale is there'); + equal(filteredArrayController.objectAt(1).name, 'Scumbag Katz', 'katz is there'); +}); + +module("Ember.Filterable with filterProperties and filterCondition", { + setup: function() { + Ember.run(function() { + array = [{ id: 1, name: "Scumbag Dale" }, { id: 2, name: "Scumbag Katz" }, { id: 3, name: "Scumbag Bryn" }]; + unfilteredArray = Ember.A(array); + + filteredArrayController = Ember.ArrayProxy.create(Ember.FilterableMixin, { + filterProperties: ['id'], + filterCondition: function(item){ + return get(item,'id') === 1; + } + }); + }); + }, + + teardown: function() { + Ember.run(function() { + filteredArrayController.destroy(); + }); + } +}); + +test("you can set content later and it will be filtered", function() { + equal(filteredArrayController.get('length'), 0, 'array has 0 items'); + + Ember.run(function() { + filteredArrayController.set('content', unfilteredArray); + }); + + equal(filteredArrayController.get('length'), 1, 'array has 1 item'); + equal(filteredArrayController.objectAt(0).name, 'Scumbag Dale', 'dale is in the filtered array'); +}); + +module("Ember.Filterable with content and filterProperties", { + setup: function() { + Ember.run(function() { + array = [{ id: 1, name: "Scumbag Dale" }, { id: 2, name: "Scumbag Katz" }, { id: 3, name: null }]; + unfilteredArray = Ember.A(array); + + filteredArrayController = Ember.ArrayProxy.create(Ember.FilterableMixin, { + content: unfilteredArray, + filterProperties: ['id', 'name'] + }); + }); + }, + + teardown: function() { + Ember.run(function() { + filteredArrayController.destroy(); + }); + } +}); + +test("by default it tests if all filterProperties are truthy", function() { + equal(filteredArrayController.get('length'), 2, 'array has 2 items'); + + unfilteredArray.pushObject({id: 4, name: 'Scumbag Chavard'}); + + equal(filteredArrayController.get('length'), 3, 'adds valid items to the filtered array'); + + unfilteredArray.pushObject({id: undefined, name: 'Scumbag Chavard'}); + unfilteredArray.pushObject({id: 6, name: ''}); + + equal(filteredArrayController.get('length'), 3, "it doesn't add invalid items to the filtered array"); +});