backbone-filtered-collection 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 652f34d8646e018db86ec95f4722b8b115a0078f
4
+ data.tar.gz: bf1d2c926f9e1e0135984b78613d3ddc36a055e8
5
+ SHA512:
6
+ metadata.gz: 69c6b95b7d3d05da2521ced7e1cb26fe970efb633869d9459cc52907f8a02105d982f5f500e07c9bd33f48b6ec9cc3ddb6982b3a574c67f16c8c6b353a1de4fc
7
+ data.tar.gz: f1c0fde67a77d7e28c15e4e7808d4d6c4ee4feb22a4d67cf520c5b119472845e89e08f05dafeb9700b7b23139e0d4109a0751f7d70e9bf3d5f08c8d787efdb88
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
+ .idea
6
7
  Gemfile.lock
7
8
  InstalledFiles
8
9
  _yardoc
data/.rvmrc CHANGED
@@ -1,49 +1 @@
1
- #!/usr/bin/env bash
2
-
3
- # This is an RVM Project .rvmrc file, used to automatically load the ruby
4
- # development environment upon cd'ing into the directory
5
-
6
- # First we specify our desired <ruby>[@<gemset>], the @gemset name is optional.
7
- environment_id="1.9.3-head@filtered-collection"
8
-
9
- #
10
- # First we attempt to load the desired environment directly from the environment
11
- # file. This is very fast and efficicent compared to running through the entire
12
- # CLI and selector. If you want feedback on which environment was used then
13
- # insert the word 'use' after --create as this triggers verbose mode.
14
- #
15
- if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
16
- && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]] ; then
17
- \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
18
-
19
- [[ -s ".rvm/hooks/after_use" ]] && . ".rvm/hooks/after_use"
20
- else
21
- # If the environment file has not yet been created, use the RVM CLI to select.
22
- rvm --create "$environment_id"
23
- fi
24
-
25
- #
26
- # If you use an RVM gemset file to install a list of gems (*.gems), you can have
27
- # it be automatically loaded. Uncomment the following and adjust the filename if
28
- # necessary.
29
- #
30
- # filename=".gems"
31
- # if [[ -s "$filename" ]] ; then
32
- # rvm gemset import "$filename" | grep -v already | grep -v listed | grep -v complete | sed '/^$/d'
33
- # fi
34
-
35
- #
36
- # If you use bundler and would like to run bundle each time you enter the
37
- # directory, you can uncomment the following code.
38
- #
39
- # # Ensure that Bundler is installed. Install it if it is not.
40
- # if ! command -v bundle >/dev/null; then
41
- # printf "The rubygem 'bundler' is not installed. Installing it now.\n"
42
- # gem install bundler
43
- # fi
44
- #
45
- # # Bundle while reducing excess noise.
46
- # printf "Bundling your gems. This may take a few minutes on a fresh clone.\n"
47
- # bundle | grep -v '^Using ' | grep -v ' is complete' | sed '/^$/d'
48
- #
49
-
1
+ rvm use ruby-2.0.0-p247@filtered-collection --create
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ before_install:
5
+ - "export DISPLAY=:99.0"
6
+ - "sh -e /etc/init.d/xvfb start"
7
+ install: bundle
8
+ script: rake jasmine:ci
data/LICENSE.txt CHANGED
@@ -1,22 +1,22 @@
1
+ The MIT License (MIT)
2
+
1
3
  Copyright (c) 2013 Dmitriy Likhten
2
4
 
3
- MIT License
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
4
11
 
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
12
14
 
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
15
22
 
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Filtered Collection
2
2
 
3
+ [![Build Status](https://travis-ci.org/dlikhten/filtered-collection.png?branch=master)](https://travis-ci.org/dlikhten/filtered-collection)
4
+
3
5
  This is a simple filtered collection implemented using
4
6
  Backbone.Collection. The goal here is to create a collection which,
5
7
  given a filter function, will just contain elements of the original
@@ -19,21 +21,28 @@ these guys.
19
21
 
20
22
  With bundler
21
23
 
22
- gem 'backbone-filtered-collection', git: "git://github.com/dlikhten/filtered-collection.git"
24
+ gem 'backbone-filtered-collection'
23
25
 
24
26
  Inside your sprockets file:
25
27
 
26
28
  //= require backbone-filtered-collection
27
29
 
30
+ # Installation anywhere else
31
+
32
+ Download the [source][1], minify as you see fit by your minification strategy.
33
+
28
34
  # Usage
29
35
 
30
36
  var YourCollection = Backbone.Collection.extend({model: YourModel});
31
37
  var YourFilteredCollection = Backbone.FilteredCollection.extend({model: YourModel});
38
+
32
39
  var allItems = new YourCollection(...);
40
+
33
41
  // note the null, backbone collections want the pre-populated model here
34
42
  // we can't do that since this collection does not accept mutations, it
35
43
  // only mutates as a proxy for the underlying collection
36
44
  var filteredItems = new YourFilteredCollection(null, {collection: allItems});
45
+
37
46
  var filteredItems.setFilter(function(item) { return item.get('included') == true;});
38
47
 
39
48
  And now filteredItems contains only those items that pass the filter.
@@ -50,6 +59,28 @@ Same goes for remove and reset.
50
59
 
51
60
  To clear the filtering completely, pass the value false to setFilter.
52
61
 
62
+ ## Filters
63
+
64
+ - `setFilter(function() {})`: Set the given function as the filter. Same api as `_.each`.
65
+ - `setFilter(false)`: Turn filtering off. This collection will have all elements of the original collection.
66
+ - `setFilter()`: Re-filter. Don't change the filter function but execute it on all elements. Useful after the original collection was modified via `silent: true`
67
+
68
+ ## Events
69
+
70
+ The collection will create events much like a regular collection. There are a few to note:
71
+
72
+ - `add`: An object was added to the collection (via filter OR via orig collection)
73
+ - `remove`: An object was removed from the collection (via filter OR via orig collection)
74
+ - `reset`: The original collection was reset, filtering happened
75
+ - `sort`: The collection was sorted. No changes in models represented.
76
+ - `change`: An object in the collection was changed. The object was already accepted by the filter, and is still.
77
+ - `filter-complete`: Filtering was completed. If you are not listening to add/remove then just listen to filter-complete and reset your views.
78
+
79
+ ## Change Collection
80
+
81
+ You can change the underlying collection if you really need to by invoking `#resetWith(newCollection)`, only one `reset`
82
+ event will be triggered with the new data.
83
+
53
84
  # Testing
54
85
 
55
86
  bundle install
@@ -59,8 +90,10 @@ I also included a .rvmrc file incase you have rvm installed.
59
90
 
60
91
  # Contributing
61
92
 
62
- Please, do not contribute without a spec. Being tested is critically important
63
- to this project, as it's a framework level component, and so its failure
93
+ Please, do not contribute without a spec. Being tested is critically important
94
+ to this project, as it's a framework level component, and so its failure
64
95
  will be damn hard to detect.
65
96
 
66
97
  Also, no tab characters, 2 spaces only. Minifiers can handle this stuff for you.
98
+
99
+ [1]: https://raw.github.com/dlikhten/filtered-collection/master/vendor/assets/javascripts/backbone-filtered-collection.js
@@ -1,5 +1,5 @@
1
1
  module Backbone
2
2
  module FilteredCollection
3
- VERSION = "1.0.0"
3
+ VERSION = "1.2.0"
4
4
  end
5
5
  end
@@ -7,10 +7,6 @@ describe("Backbone.FilteredCollection", function() {
7
7
  model: TehModel
8
8
  });
9
9
 
10
- var ModelCollection = Backbone.FilteredCollection.extend({
11
- model: TehModel
12
- });
13
-
14
10
  var allModels;
15
11
  var collection;
16
12
 
@@ -20,12 +16,9 @@ describe("Backbone.FilteredCollection", function() {
20
16
  }
21
17
  };
22
18
 
23
- var oddFilter = function(model) {
24
- return model.get("value") % 2 == 1;
25
- }
26
19
  var evenFilter = function(model) {
27
20
  return model.get("value") % 2 == 0;
28
- }
21
+ };
29
22
 
30
23
  beforeEach(function() {
31
24
  allModels = new RegularModelCollection();
@@ -33,20 +26,23 @@ describe("Backbone.FilteredCollection", function() {
33
26
  allModels.add(new TehModel({id: i, value: i}));
34
27
  }
35
28
 
36
- collection = new ModelCollection(null, {collection: allModels});
29
+ collection = new Backbone.FilteredCollection(null, {collection: allModels});
30
+ });
31
+
32
+ afterEach(function() {
33
+ if (collection) collection.off(null, null, null);
37
34
  });
38
35
 
39
36
  describe("#setFilter", function() {
40
- it("should filter the given model", function() {
37
+ it("filters the given model", function() {
41
38
  collection.setFilter(createLessthanFilter(5));
42
39
 
43
40
  expect(collection.length).toEqual(5);
44
41
  expect(collection.at(0).get('value')).toEqual(0);
45
42
  });
46
43
 
47
- it("should change filters", function() {
44
+ it("uses the given filter", function() {
48
45
  collection.setFilter(createLessthanFilter(5));
49
-
50
46
  collection.setFilter(function(model) {
51
47
  return model.get('value') > 7;
52
48
  });
@@ -55,7 +51,7 @@ describe("Backbone.FilteredCollection", function() {
55
51
  expect(collection.at(0).get('value')).toEqual(8);
56
52
  });
57
53
 
58
- it("should take a false filter as a return to no filter", function() {
54
+ it("accepts false to remove all filters", function() {
59
55
  collection.setFilter(createLessthanFilter(5));
60
56
  expect(collection.length).toEqual(5);
61
57
  collection.setFilter(undefined); // no change
@@ -66,46 +62,43 @@ describe("Backbone.FilteredCollection", function() {
66
62
  expect(collection.length).toEqual(10);
67
63
  });
68
64
 
69
- it("should work correctly after filtering is changed constantly", function() {
65
+ it("works correctly after filtering is changed constantly", function() {
70
66
  collection.setFilter(createLessthanFilter(0));
71
67
  expect(collection.models.length).toEqual(0);
72
68
 
73
69
  collection.setFilter(createLessthanFilter(3));
74
70
  expect(collection.models.length).toEqual(3);
75
- expect(collection.models[0].get("value")).toEqual(0)
76
- expect(collection.models[1].get("value")).toEqual(1)
77
- expect(collection.models[2].get("value")).toEqual(2)
71
+ expect(collection.at(0).get("value")).toEqual(0);
72
+ expect(collection.at(1).get("value")).toEqual(1);
73
+ expect(collection.at(2).get("value")).toEqual(2);
78
74
 
79
75
  collection.setFilter(evenFilter);
80
76
  expect(collection.models.length).toEqual(5);
81
- expect(collection.models[0].get("value")).toEqual(0)
82
- expect(collection.models[1].get("value")).toEqual(2)
83
- expect(collection.models[2].get("value")).toEqual(4)
84
- expect(collection.models[3].get("value")).toEqual(6)
85
- expect(collection.models[4].get("value")).toEqual(8)
77
+ expect(collection.at(0).get("value")).toEqual(0);
78
+ expect(collection.at(1).get("value")).toEqual(2);
79
+ expect(collection.at(2).get("value")).toEqual(4);
80
+ expect(collection.at(3).get("value")).toEqual(6);
81
+ expect(collection.at(4).get("value")).toEqual(8);
86
82
  });
87
83
 
88
- it("should not trigger a filter-complete event if options.silent is true", function() {
89
- count = 0;
90
- collection.on("filter-complete", function() {
91
- count += 1;
92
- });
84
+ it("does not trigger a filter-complete event if options.silent is true", function() {
85
+ var eventSpy = jasmine.createSpy('filter-complete listener');
86
+ collection.on("filter-complete", eventSpy);
93
87
 
94
88
  collection.setFilter(createLessthanFilter(0), {silent: true});
95
-
96
- expect(count).toEqual(0);
89
+ expect(eventSpy).not.toHaveBeenCalled();
97
90
  });
98
91
  });
99
92
 
100
93
  describe("event:add", function() {
101
- it("should not add the new object, since it is already filtered out", function() {
94
+ it("does not add an already filtered out new object", function() {
102
95
  collection.setFilter(createLessthanFilter(5));
103
96
  expect(collection.length).toEqual(5);
104
97
  allModels.add(new TehModel({value: 6}));
105
98
  expect(collection.length).toEqual(5);
106
99
  });
107
100
 
108
- it("should add the new object, since it passes the filter", function() {
101
+ it("adds the new object, since it passes the filter", function() {
109
102
  collection.setFilter(createLessthanFilter(5));
110
103
  expect(collection.length).toEqual(5);
111
104
  allModels.add(new TehModel({value: 1}));
@@ -113,7 +106,7 @@ describe("Backbone.FilteredCollection", function() {
113
106
  expect(collection.at(5).get('value')).toEqual(1);
114
107
  });
115
108
 
116
- it("should add the new object to the correct location", function() {
109
+ it("adds the new object to the correct location", function() {
117
110
  collection.setFilter(createLessthanFilter(5));
118
111
  expect(collection.length).toEqual(5);
119
112
  allModels.add(new TehModel({value: 4}), {at: 0});
@@ -121,23 +114,18 @@ describe("Backbone.FilteredCollection", function() {
121
114
  expect(collection.at(0).get('value')).toEqual(4);
122
115
  });
123
116
 
124
- it("should trigger an add event if the object was added", function() {
117
+ it("triggers an add event if the object was added", function() {
125
118
  collection.setFilter(createLessthanFilter(5));
126
- expect(collection.length).toEqual(5);
127
-
128
119
  var newModel = new TehModel({value: 3});
129
- count = 0;
130
- collection.on("add", function(model, collection, options) {
131
- expect(model).toEqual(newModel);
132
- expect(options.index).toEqual(0);
133
- count += 1;
134
- });
120
+ var eventSpy = jasmine.createSpy('reset event listener');
121
+
122
+ collection.on("add", eventSpy);
135
123
  allModels.add(newModel, {at: 0});
136
124
 
137
- expect(count).toEqual(1);
125
+ expect(eventSpy).toHaveBeenCalledWith(newModel, collection, {index: 0})
138
126
  });
139
127
 
140
- it("should re-number elements propperly in the mapping according to what the actualy indices are in the original collection", function() {
128
+ it("re-numbers elements properly in the mapping according to what the atual indices are in the original collection", function() {
141
129
  collection.setFilter(createLessthanFilter(10));
142
130
  expect(collection.length).toEqual(10);
143
131
 
@@ -148,14 +136,14 @@ describe("Backbone.FilteredCollection", function() {
148
136
  });
149
137
 
150
138
  describe("event:remove", function() {
151
- it("should be a noop since the object is filtered", function() {
139
+ it("is a no-op if the item removed is already not filtered", function() {
152
140
  collection.setFilter(createLessthanFilter(5));
153
141
  expect(collection.length).toEqual(5);
154
142
  allModels.remove(allModels.at(6));
155
143
  expect(collection.length).toEqual(5);
156
144
  });
157
145
 
158
- it("should be a remove the removed object", function() {
146
+ it("removes the model that was removed from the underlying collection", function() {
159
147
  collection.setFilter(createLessthanFilter(5));
160
148
  expect(collection.length).toEqual(5);
161
149
  allModels.remove(allModels.at(4));
@@ -163,7 +151,7 @@ describe("Backbone.FilteredCollection", function() {
163
151
  expect(collection.at(collection.length - 1).get('value')).toEqual(3);
164
152
  });
165
153
 
166
- it("should re-number elements propperly in the mapping according to what the actualy indices are in the original collection", function() {
154
+ it("should re-number elements properly in the mapping according to what the actual indices are in the original collection", function() {
167
155
  collection.setFilter(createLessthanFilter(10));
168
156
  expect(collection.length).toEqual(10);
169
157
 
@@ -174,7 +162,7 @@ describe("Backbone.FilteredCollection", function() {
174
162
  });
175
163
 
176
164
  describe("event:reset", function() {
177
- it("should be a noop since the object is filtered", function() {
165
+ it("resets all models to those in the underlying collection", function() {
178
166
  collection.setFilter(createLessthanFilter(15));
179
167
  var newAll = [];
180
168
  for (var i = 10; i < 20; i++) {
@@ -184,10 +172,20 @@ describe("Backbone.FilteredCollection", function() {
184
172
  expect(collection.length).toEqual(5);
185
173
  expect(collection.at(4).get('value')).toEqual(14);
186
174
  });
175
+
176
+ it("triggers exactly one reset event", function() {
177
+ var eventSpy = jasmine.createSpy('reset event listener');
178
+ collection.on('reset', eventSpy);
179
+
180
+ allModels.reset([{id: 11, value: 11}]);
181
+
182
+ expect(eventSpy).toHaveBeenCalledWith(collection);
183
+ expect(eventSpy.callCount).toEqual(1);
184
+ });
187
185
  });
188
186
 
189
187
  describe("event:sort", function() {
190
- it("should continue filtering the collection, except with a new order", function() {
188
+ it("continues to filter the collection, except with a new order", function() {
191
189
  collection.setFilter(createLessthanFilter(5));
192
190
  allModels.comparator = function(v1, v2) {
193
191
  return v2.get("value") - v1.get("value");
@@ -204,25 +202,26 @@ describe("Backbone.FilteredCollection", function() {
204
202
  });
205
203
 
206
204
  describe("event:filter-complete", function() {
207
- it("should fire when the underlying collection fires it (thus we're done filtering too)", function() {
208
- var filterFired = 0;
209
- collection.on("filter-complete", function() {
210
- filterFired += 1;
211
- });
205
+ it("triggers when the underlying collection triggers it (thus we're done filtering too)", function() {
206
+ var eventSpy = jasmine.createSpy('filter-complete event listener');
207
+ collection.on("filter-complete", eventSpy);
208
+
212
209
  allModels.trigger("filter-complete");
213
- expect(filterFired).toEqual(1);
210
+
211
+ expect(eventSpy).toHaveBeenCalled();
214
212
  });
215
213
 
216
- it("should fire once only at the end of a filter", function() {
217
- var filterFired = 0;
218
- collection.on("filter-complete", function() {
219
- filterFired += 1;
220
- });
214
+ it("triggers once only at the end of a filter", function() {
215
+ var eventSpy = jasmine.createSpy('filter-complete event listener');
216
+ collection.on("filter-complete", eventSpy);
217
+
221
218
  collection.setFilter(createLessthanFilter(3));
222
- expect(filterFired).toEqual(1);
219
+
220
+ expect(eventSpy).toHaveBeenCalled();
221
+ expect(eventSpy.callCount).toEqual(1);
223
222
  });
224
223
 
225
- it("should fire once when a change is propagated from an underlying model", function() {
224
+ it("triggers once when a change is propagated from an underlying model", function() {
226
225
  var filterFired = 0;
227
226
  collection.on("filter-complete", function() {
228
227
  filterFired += 1;
@@ -230,51 +229,51 @@ describe("Backbone.FilteredCollection", function() {
230
229
  collection.setFilter(createLessthanFilter(3));
231
230
  filterFired = 0;
232
231
 
233
- collection.models[0].trigger("change", collection.models[0], allModels)
232
+ collection.at(0).trigger("change", collection.at(0), allModels)
234
233
  expect(filterFired).toEqual(1);
235
234
  });
236
235
  });
237
236
 
238
237
  describe("model - event:destroy", function() {
239
- it("should just remove the model from the base collection like normal, and raise no problems with the filter", function() {
238
+ it("just removes the model from the base collection like normal, and raise no problems with the filter", function() {
240
239
  collection.setFilter(createLessthanFilter(5));
241
- origModelZero = collection.models[0];
240
+ origModelZero = collection.at(0);
242
241
  // simulate an ajax destroy
243
- origModelZero.trigger("destroy", origModelZero, origModelZero.collection)
242
+ origModelZero.trigger("destroy", origModelZero, origModelZero.collection);
244
243
 
245
- expect(collection.models[0].get("value")).toEqual(1)
244
+ expect(collection.at(0).get("value")).toEqual(1);
246
245
  });
247
246
 
248
- it("should remove elements from the model as events occur", function() {
247
+ it("removes elements from the model as events occur", function() {
249
248
  collection.setFilter(createLessthanFilter(10));
250
249
 
251
250
  // start removing in weird orders, make sure vents are done properly
252
- model = collection.models[0];
253
- model.trigger("destroy", model, model.collection)
254
- expect(collection.models[0].get("value")).toEqual(1)
251
+ model = collection.at(0);
252
+ model.trigger("destroy", model, model.collection);
253
+ expect(collection.at(0).get("value")).toEqual(1);
255
254
 
256
- model = collection.models[3];
257
- model.trigger("destroy", model, model.collection)
258
- expect(collection.models[3].get("value")).toEqual(5)
255
+ model = collection.at(3);
256
+ model.trigger("destroy", model, model.collection);
257
+ expect(collection.at(3).get("value")).toEqual(5);
259
258
 
260
- model = collection.models[3];
261
- model.trigger("destroy", model, model.collection)
262
- expect(collection.models[3].get("value")).toEqual(6)
259
+ model = collection.at(3);
260
+ model.trigger("destroy", model, model.collection);
261
+ expect(collection.at(3).get("value")).toEqual(6);
263
262
 
264
- model = collection.models[3];
265
- model.trigger("destroy", model, model.collection)
266
- expect(collection.models[3].get("value")).toEqual(7)
263
+ model = collection.at(3);
264
+ model.trigger("destroy", model, model.collection);
265
+ expect(collection.at(3).get("value")).toEqual(7);
267
266
 
268
- model = collection.models[2];
269
- model.trigger("destroy", model, model.collection)
270
- expect(collection.models[2].get("value")).toEqual(7)
267
+ model = collection.at(2);
268
+ model.trigger("destroy", model, model.collection);
269
+ expect(collection.at(2).get("value")).toEqual(7);
271
270
 
272
- model = collection.models[1];
273
- model.trigger("destroy", model, model.collection)
274
- expect(collection.models[1].get("value")).toEqual(7)
271
+ model = collection.at(1);
272
+ model.trigger("destroy", model, model.collection);
273
+ expect(collection.at(1).get("value")).toEqual(7);
275
274
  });
276
275
 
277
- it("should create remove events for every deleted model", function() {
276
+ it("triggers remove events for every deleted model", function() {
278
277
  collection.setFilter(createLessthanFilter(10));
279
278
  var lastModelRemoved = null;
280
279
  var count = 0;
@@ -285,37 +284,37 @@ describe("Backbone.FilteredCollection", function() {
285
284
 
286
285
  // start removing in weird orders, make sure vents are done properly
287
286
  count = 0;
288
- model = collection.models[0];
287
+ model = collection.at(0);
289
288
  model.trigger("destroy", model, model.collection)
290
289
  expect(lastModelRemoved).toEqual(model);
291
290
  expect(count).toEqual(1);
292
291
 
293
292
  count = 0;
294
- model = collection.models[3];
293
+ model = collection.at(3);
295
294
  model.trigger("destroy", model, model.collection)
296
295
  expect(lastModelRemoved).toEqual(model);
297
296
  expect(count).toEqual(1);
298
297
 
299
298
  count = 0;
300
- model = collection.models[3];
299
+ model = collection.at(3);
301
300
  model.trigger("destroy", model, model.collection)
302
301
  expect(lastModelRemoved).toEqual(model);
303
302
  expect(count).toEqual(1);
304
303
 
305
304
  count = 0;
306
- model = collection.models[3];
305
+ model = collection.at(3);
307
306
  model.trigger("destroy", model, model.collection)
308
307
  expect(lastModelRemoved).toEqual(model);
309
308
  expect(count).toEqual(1);
310
309
 
311
310
  count = 0;
312
- model = collection.models[2];
311
+ model = collection.at(2);
313
312
  model.trigger("destroy", model, model.collection)
314
313
  expect(lastModelRemoved).toEqual(model);
315
314
  expect(count).toEqual(1);
316
315
 
317
316
  count = 0;
318
- model = collection.models[1];
317
+ model = collection.at(1);
319
318
  model.trigger("destroy", model, model.collection)
320
319
  expect(lastModelRemoved).toEqual(model);
321
320
  expect(count).toEqual(1);
@@ -323,31 +322,145 @@ describe("Backbone.FilteredCollection", function() {
323
322
  });
324
323
 
325
324
  describe("model - event:change", function() {
326
- it("should remove the model because it failed the filter post change", function() {
325
+ var changeSpy, addSpy, removeSpy;
326
+
327
+ beforeEach(function() {
328
+ changeSpy = jasmine.createSpy("change listener");
329
+ addSpy = jasmine.createSpy("add listener");
330
+ removeSpy = jasmine.createSpy("remove listener");
331
+ });
332
+
333
+ it("removes the model because it failed the filter post change, triggers remove event", function() {
327
334
  collection.setFilter(createLessthanFilter(5));
328
- origModelZero = collection.models[0];
335
+ collection.on("change", changeSpy);
336
+ collection.on("add", addSpy);
337
+ collection.on("remove", removeSpy);
338
+
339
+ origModelZero = collection.at(0);
329
340
  origModelZero.set("value", 10)
330
341
 
331
342
  expect(collection.models.length).toEqual(4)
332
- expect(collection.models[0].get("value")).toEqual(1)
343
+ expect(collection.at(0).get("value")).toEqual(1)
344
+ expect(changeSpy).not.toHaveBeenCalled();
345
+ expect(addSpy).not.toHaveBeenCalled();
346
+ expect(removeSpy).toHaveBeenCalledWith(origModelZero, collection, jasmine.any(Object));
333
347
  });
334
348
 
335
- it("should do nothing if the model is still passing the filter", function() {
349
+ it("does not alter the collection if the model is still passing, triggers change event", function() {
336
350
  collection.setFilter(createLessthanFilter(5));
337
- origModelZero = collection.models[0];
351
+ collection.on("change", changeSpy);
352
+ collection.on("add", addSpy);
353
+ collection.on("remove", removeSpy);
354
+
355
+ origModelZero = collection.at(0);
338
356
  origModelZero.set("value", 3)
339
357
 
340
358
  expect(collection.models.length).toEqual(5)
341
- expect(collection.models[0].get("value")).toEqual(3)
359
+ expect(collection.at(0).get("value")).toEqual(3)
360
+ expect(changeSpy).toHaveBeenCalledWith(origModelZero, collection);
361
+ expect(addSpy).not.toHaveBeenCalled();
362
+ expect(removeSpy).not.toHaveBeenCalled();
342
363
  });
343
364
 
344
- it("should add the model that is now passing the filter", function() {
365
+ it("adds the model that is now passing the filter, triggers add event", function() {
345
366
  collection.setFilter(createLessthanFilter(5));
346
- origModelZero = allModels.models[9];
367
+ collection.on("change", changeSpy);
368
+ collection.on("add", addSpy);
369
+ collection.on("remove", removeSpy);
370
+
371
+ origModelZero = allModels.at(9);
347
372
  origModelZero.set("value", 2)
348
373
 
349
374
  expect(collection.models.length).toEqual(6)
350
- expect(collection.models[5].get("value")).toEqual(2)
375
+ expect(collection.at(5).get("value")).toEqual(2)
376
+ expect(changeSpy).not.toHaveBeenCalled();
377
+ expect(addSpy).toHaveBeenCalledWith(origModelZero, collection, jasmine.any(Object));
378
+ expect(removeSpy).not.toHaveBeenCalled();
379
+ });
380
+ });
381
+
382
+ describe("#resetWith", function() {
383
+ var moreModels;
384
+
385
+ beforeEach(function(){
386
+ moreModels = new RegularModelCollection();
387
+ for(var i = 10; i < 16; i++) {
388
+ moreModels.add(new TehModel({id: i, value: i}));
389
+ }
390
+ collection.setFilter(evenFilter);
391
+ });
392
+
393
+ it("updates the collection's length to match that of the new collection", function() {
394
+ var lengthBefore = collection.length;
395
+ collection.resetWith(moreModels);
396
+ var lengthAfter = collection.length;
397
+
398
+ expect(lengthBefore).toEqual(5); // even models
399
+ expect(lengthAfter).toEqual(3); // even models
400
+ });
401
+
402
+ it("handles add events on the new collection", function() {
403
+ collection.resetWith(moreModels);
404
+
405
+ var lengthBefore = collection.length;
406
+ newModel = new TehModel({id: 16, value: 16});
407
+ moreModels.add(newModel);
408
+ var lengthAfter = collection.length;
409
+
410
+ expect(lengthAfter).toEqual(lengthBefore + 1);
411
+ expect(collection.indexOf(newModel)).toBeGreaterThan(-1);
412
+ });
413
+
414
+ it("handles remove events on the new collection", function() {
415
+ collection.resetWith(moreModels);
416
+
417
+ var lengthBefore = collection.length;
418
+ var toRemove = moreModels.get(12);
419
+ moreModels.remove(toRemove);
420
+ var lengthAfter = collection.length;
421
+
422
+ expect(lengthAfter).toEqual(lengthBefore - 1);
423
+ expect(collection.indexOf(toRemove)).toEqual(-1)
424
+ });
425
+
426
+ it("handles change events on the new collection", function() {
427
+ collection.resetWith(moreModels);
428
+
429
+ var lengthBefore = collection.length;
430
+ var changeModel = moreModels.get(10);
431
+ changeModel.set('value', 11);
432
+ var lengthAfter = collection.length;
433
+
434
+ expect(lengthAfter).toEqual(lengthBefore - 1);
435
+ expect(collection.indexOf(changeModel)).toEqual(-1);
436
+ });
437
+
438
+ it("ignores the old collection events", function() {
439
+ var eventSpy = jasmine.createSpy('all event listener');
440
+
441
+ collection.resetWith(moreModels);
442
+
443
+ collection.on('all', eventSpy);
444
+
445
+ allModels.add(new TehModel({id: 16, value: 16}));
446
+ allModels.remove(allModels.at(0));
447
+ allModels.get(2).set("value", "3");
448
+ allModels.comparator = function(a, b) { return 0 };
449
+ allModels.sort();
450
+ allModels.reset([{value: 5}]);
451
+
452
+ expect(eventSpy).not.toHaveBeenCalled();
453
+ });
454
+
455
+ it("fire a reset with the new filtered data, exactly once", function() {
456
+ var eventSpy = jasmine.createSpy('reset event listener');
457
+
458
+ collection.on('reset', eventSpy);
459
+
460
+ collection.resetWith(moreModels);
461
+
462
+ expect(eventSpy).toHaveBeenCalledWith(collection);
463
+ expect(eventSpy.callCount).toEqual(1);
351
464
  });
352
465
  });
353
466
  });
@@ -1,25 +1,27 @@
1
1
  /*
2
- Copyright (C) 2012 Dmitriy Likhten
2
+ The MIT License (MIT)
3
3
 
4
- Permission is hereby granted, free of charge, to any person obtaining a copy of
5
- this software and associated documentation files (the "Software"), to deal in
6
- the Software without restriction, including without limitation the rights to
7
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8
- of the Software, and to permit persons to whom the Software is furnished to do
9
- so, subject to the following conditions:
4
+ Copyright (c) 2013 Dmitriy Likhten
10
5
 
11
- The above copyright notice and this permission notice shall be included in all
12
- copies or substantial portions of the Software.
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
13
15
 
14
16
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
17
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
18
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
19
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
20
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
- SOFTWARE.
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
21
23
  */
22
- /* version 1.0.0 */
24
+ /* version 1.2.0 */
23
25
  (function(_, Backbone) {
24
26
  var defaultFilter = function() {return true;};
25
27
  /**
@@ -44,17 +46,44 @@ SOFTWARE.
44
46
  collectionFilter: null
45
47
  ,defaultFilter: defaultFilter
46
48
 
49
+ /**
50
+ Default initializer.
51
+
52
+ @param {Boolean} [models=false] - By default we must to use a falsy value here.
53
+ @param {Object} data - Options.
54
+ @param {Backbone.Collection} data.collection - Collection to be filtered
55
+ @param {Function} data.collectionFilter - Filter to be applied to the collection.
56
+
57
+ @constructor
58
+ **/
47
59
  ,initialize: function(models, data) {
48
60
  if (models) throw "models cannot be set directly, unfortunately first argument is the models.";
61
+
49
62
  this.collection = data.collection;
50
63
  this.setFilter(data.collectionFilter);
64
+ this._bindEvents();
65
+ }
66
+
67
+ /**
68
+ * Replace the current underlying collection with a new one.
69
+ * note: only a reset event will be fired.
70
+ */
71
+ ,resetWith: function(newCollection) {
72
+ this.stopListening(this.collection);
73
+ this.collection = newCollection;
74
+ this.resetCollection();
75
+ this._bindEvents();
76
+
77
+ return this;
78
+ }
51
79
 
52
- this.collection.on("add", this.addModel, this);
53
- this.collection.on("remove", this.removeModel, this);
54
- this.collection.on("reset", this.resetCollection, this);
55
- this.collection.on("sort", this.resortCollection, this);
56
- this.collection.on("change", this._modelChanged, this);
57
- this.collection.on("filter-complete", this._filterComplete, this);
80
+ ,_bindEvents: function () {
81
+ this.listenTo(this.collection, "add", this.addModel);
82
+ this.listenTo(this.collection, "remove", this.removeModel);
83
+ this.listenTo(this.collection, "reset", this.resetCollection);
84
+ this.listenTo(this.collection, "sort", this.resortCollection);
85
+ this.listenTo(this.collection, "change", this._modelChanged);
86
+ this.listenTo(this.collection, "filter-complete", this._filterComplete);
58
87
  }
59
88
 
60
89
  ,_reset: function(options) {
@@ -85,6 +114,11 @@ SOFTWARE.
85
114
  var index = this.collection.indexOf(model);
86
115
  this._forceAddModel(model, {index: index});
87
116
  }
117
+ // the model passes the filter and is already in the collection
118
+ // therefore we want to indicate that the model has changed
119
+ else {
120
+ this.trigger("change", model, this);
121
+ }
88
122
  } else {
89
123
  // Model did not pass filter
90
124
  if (ownIndexOfModel > -1){
@@ -97,11 +131,22 @@ SOFTWARE.
97
131
  }
98
132
 
99
133
  ,resortCollection: function() {
100
- this._mapping = [];
101
- this._reset();
102
- this.setFilter(undefined, {silent: true});
134
+ // note: we don't need to do any filter work since sort
135
+ // implies nothing changed, only order
136
+ var newModels = [];
137
+ var newMapping = [];
138
+ var models = this.models;
139
+ _.each(this.collection.models, function(model, index) {
140
+ if (models.indexOf(model) >= 0) {
141
+ newModels.push(model);
142
+ newMapping.push(index);
143
+ }
144
+ });
145
+ this.models = newModels;
146
+ this._mapping = newMapping;
103
147
  this.trigger("sort", this);
104
148
  }
149
+
105
150
  ,resetCollection: function() {
106
151
  this._mapping = [];
107
152
  this._reset();
metadata CHANGED
@@ -1,22 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: backbone-filtered-collection
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
5
- prerelease:
4
+ version: 1.2.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Dmitriy Likhten
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-07-10 00:00:00.000000000 Z
11
+ date: 2013-10-10 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: railties
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ! '>='
17
+ - - '>='
20
18
  - !ruby/object:Gem::Version
21
19
  version: '3.0'
22
20
  - - <
@@ -25,9 +23,8 @@ dependencies:
25
23
  type: :runtime
26
24
  prerelease: false
27
25
  version_requirements: !ruby/object:Gem::Requirement
28
- none: false
29
26
  requirements:
30
- - - ! '>='
27
+ - - '>='
31
28
  - !ruby/object:Gem::Version
32
29
  version: '3.0'
33
30
  - - <
@@ -36,33 +33,29 @@ dependencies:
36
33
  - !ruby/object:Gem::Dependency
37
34
  name: rake
38
35
  requirement: !ruby/object:Gem::Requirement
39
- none: false
40
36
  requirements:
41
- - - ! '>='
37
+ - - '>='
42
38
  - !ruby/object:Gem::Version
43
39
  version: '0'
44
40
  type: :development
45
41
  prerelease: false
46
42
  version_requirements: !ruby/object:Gem::Requirement
47
- none: false
48
43
  requirements:
49
- - - ! '>='
44
+ - - '>='
50
45
  - !ruby/object:Gem::Version
51
46
  version: '0'
52
47
  - !ruby/object:Gem::Dependency
53
48
  name: jasmine
54
49
  requirement: !ruby/object:Gem::Requirement
55
- none: false
56
50
  requirements:
57
- - - ! '>='
51
+ - - '>='
58
52
  - !ruby/object:Gem::Version
59
53
  version: '0'
60
54
  type: :development
61
55
  prerelease: false
62
56
  version_requirements: !ruby/object:Gem::Requirement
63
- none: false
64
57
  requirements:
65
- - - ! '>='
58
+ - - '>='
66
59
  - !ruby/object:Gem::Version
67
60
  version: '0'
68
61
  description: A filtered collection for backbone.js
@@ -74,6 +67,7 @@ extra_rdoc_files: []
74
67
  files:
75
68
  - .gitignore
76
69
  - .rvmrc
70
+ - .travis.yml
77
71
  - Gemfile
78
72
  - LICENSE
79
73
  - LICENSE.txt
@@ -93,27 +87,26 @@ files:
93
87
  homepage: http://github.com/dlikhten/filtered-collection
94
88
  licenses:
95
89
  - MIT
90
+ metadata: {}
96
91
  post_install_message:
97
92
  rdoc_options: []
98
93
  require_paths:
99
94
  - lib
100
95
  required_ruby_version: !ruby/object:Gem::Requirement
101
- none: false
102
96
  requirements:
103
- - - ! '>='
97
+ - - '>='
104
98
  - !ruby/object:Gem::Version
105
99
  version: '0'
106
100
  required_rubygems_version: !ruby/object:Gem::Requirement
107
- none: false
108
101
  requirements:
109
- - - ! '>='
102
+ - - '>='
110
103
  - !ruby/object:Gem::Version
111
104
  version: '0'
112
105
  requirements: []
113
106
  rubyforge_project:
114
- rubygems_version: 1.8.24
107
+ rubygems_version: 2.0.6
115
108
  signing_key:
116
- specification_version: 3
109
+ specification_version: 4
117
110
  summary: Allowing implementation of a chain-of-responsibility pattern in backbone's
118
111
  collection filtering
119
112
  test_files: