From 757bea350f9fca252447c69690e5faa19472e5e2 Mon Sep 17 00:00:00 2001 From: James Croft Date: Thu, 2 Aug 2012 13:01:03 +0100 Subject: [PATCH] Add FilterableMixin Works in a similar way to the SortableMixin. Mix this into an ArrayProxy, define the filterProperties and the array will be filtered to only include the objects whose filterProperties are truthy. You can get more advanced filtering by defining a custom filterCondition. The filterCondition is used to determine if the object is to be included in the filtered list. Any item properties used in the filterCondition will need to be lised in filterProperties. --- .../lib/controllers/array_controller.js | 1 + .../ember-runtime/lib/mixins/filterable.js | 140 ++++++++++++++ .../tests/mixins/filterable_test.js | 179 ++++++++++++++++++ 3 files changed, 320 insertions(+) create mode 100644 packages/ember-runtime/lib/mixins/filterable.js create mode 100644 packages/ember-runtime/tests/mixins/filterable_test.js 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"); +});