backbone-filtered-collection 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1,49 @@
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
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in backbone_filtered_collection.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Dmitriy Likhten
4
+
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:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
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.
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Dmitriy Likhten
2
+
3
+ MIT License
4
+
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
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
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 ADDED
@@ -0,0 +1,66 @@
1
+ # Filtered Collection
2
+
3
+ This is a simple filtered collection implemented using
4
+ Backbone.Collection. The goal here is to create a collection which,
5
+ given a filter function, will just contain elements of the original
6
+ which pass the filter. Supports add/remove/reset events of the original
7
+ to modify the filtered version.
8
+
9
+ # Why not just extend backbone?
10
+
11
+ The main reason I did not just extend backbone is because by extending
12
+ it, you shove all behaviors into one model, making it a
13
+ jack-of-all-trades and potentially conflicting with behaviors of other
14
+ extentions, not to mention making normal operaitons potentially slower.
15
+ So the intention is to compose a filter chain pattern using
16
+ these guys.
17
+
18
+ # Installation to rails
19
+
20
+ With bundler
21
+
22
+ gem 'backbone-filtered-collection', git: "git://github.com/dlikhten/filtered-collection.git"
23
+
24
+ Inside your sprockets file:
25
+
26
+ //= require backbone-filtered-collection
27
+
28
+ # Usage
29
+
30
+ var YourCollection = Backbone.Collection.extend({model: YourModel});
31
+ var YourFilteredCollection = Backbone.FilteredCollection.extend({model: YourModel});
32
+ var allItems = new YourCollection(...);
33
+ // note the null, backbone collections want the pre-populated model here
34
+ // we can't do that since this collection does not accept mutations, it
35
+ // only mutates as a proxy for the underlying collection
36
+ var filteredItems = new YourFilteredCollection(null, {collection: allItems});
37
+ var filteredItems.setFilter(function(item) { return item.get('included') == true;});
38
+
39
+ And now filteredItems contains only those items that pass the filter.
40
+ You can still manipulate the original:
41
+
42
+ allItems.add(..., {at: 5}); // at is supported too...
43
+
44
+ However, if you invoke {silent: true} on the original model, then you
45
+ must reset the filter by invoking:
46
+
47
+ filteredItems.setFilter(); // no args = just re-filter
48
+
49
+ Same goes for remove and reset.
50
+
51
+ To clear the filtering completely, pass the value false to setFilter.
52
+
53
+ # Testing
54
+
55
+ bundle install
56
+ rake jasmine
57
+
58
+ I also included a .rvmrc file incase you have rvm installed.
59
+
60
+ # Contributing
61
+
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
64
+ will be damn hard to detect.
65
+
66
+ Also, no tab characters, 2 spaces only. Minifiers can handle this stuff for you.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'jasmine'
4
+ load 'jasmine/tasks/jasmine.rake'
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/backbone/filtered_collection/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "backbone-filtered-collection"
6
+ gem.version = Backbone::FilteredCollection::VERSION
7
+ gem.platform = Gem::Platform::RUBY
8
+ gem.authors = ["Dmitriy Likhten"]
9
+ gem.email = ["dlikhten@gmail.com"]
10
+ gem.description = %q{A filtered collection for backbone.js}
11
+ gem.summary = %q{Allowing implementation of a chain-of-responsibility pattern in backbone's collection filtering}
12
+ gem.homepage = "http://github.com/dlikhten/filtered-collection"
13
+ gem.license = "MIT"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "railties", ">= 3.0", "< 5.0"
21
+
22
+ gem.add_development_dependency 'rake'
23
+ gem.add_development_dependency 'jasmine'
24
+ end
@@ -0,0 +1,6 @@
1
+ module Backbone
2
+ module FilteredCollection
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Backbone
2
+ module FilteredCollection
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ require 'backbone/filtered_collection/engine' if ::Rails.version >= '3.1'
2
+ require 'backbone/filtered_collection/version'
3
+
4
+ module Backbone
5
+ module FilteredCollection
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ require "backbone/filtered_collection"
@@ -0,0 +1,353 @@
1
+ describe("Backbone.FilteredCollection", function() {
2
+ var TehModel = Backbone.Model.extend({
3
+ defaults: {value: -1}
4
+ });
5
+
6
+ var RegularModelCollection = Backbone.Collection.extend({
7
+ model: TehModel
8
+ });
9
+
10
+ var ModelCollection = Backbone.FilteredCollection.extend({
11
+ model: TehModel
12
+ });
13
+
14
+ var allModels;
15
+ var collection;
16
+
17
+ var createLessthanFilter = function(lessThan) {
18
+ return function(model) {
19
+ return model.get('value') < lessThan;
20
+ }
21
+ };
22
+
23
+ var oddFilter = function(model) {
24
+ return model.get("value") % 2 == 1;
25
+ }
26
+ var evenFilter = function(model) {
27
+ return model.get("value") % 2 == 0;
28
+ }
29
+
30
+ beforeEach(function() {
31
+ allModels = new RegularModelCollection();
32
+ for(var i = 0; i < 10; i++) {
33
+ allModels.add(new TehModel({id: i, value: i}));
34
+ }
35
+
36
+ collection = new ModelCollection(null, {collection: allModels});
37
+ });
38
+
39
+ describe("#setFilter", function() {
40
+ it("should filter the given model", function() {
41
+ collection.setFilter(createLessthanFilter(5));
42
+
43
+ expect(collection.length).toEqual(5);
44
+ expect(collection.at(0).get('value')).toEqual(0);
45
+ });
46
+
47
+ it("should change filters", function() {
48
+ collection.setFilter(createLessthanFilter(5));
49
+
50
+ collection.setFilter(function(model) {
51
+ return model.get('value') > 7;
52
+ });
53
+
54
+ expect(collection.length).toEqual(2);
55
+ expect(collection.at(0).get('value')).toEqual(8);
56
+ });
57
+
58
+ it("should take a false filter as a return to no filter", function() {
59
+ collection.setFilter(createLessthanFilter(5));
60
+ expect(collection.length).toEqual(5);
61
+ collection.setFilter(undefined); // no change
62
+ expect(collection.length).toEqual(5);
63
+ collection.setFilter(null); // no change
64
+ expect(collection.length).toEqual(5);
65
+ collection.setFilter(false); // filter reset
66
+ expect(collection.length).toEqual(10);
67
+ });
68
+
69
+ it("should work correctly after filtering is changed constantly", function() {
70
+ collection.setFilter(createLessthanFilter(0));
71
+ expect(collection.models.length).toEqual(0);
72
+
73
+ collection.setFilter(createLessthanFilter(3));
74
+ 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)
78
+
79
+ collection.setFilter(evenFilter);
80
+ 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)
86
+ });
87
+
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
+ });
93
+
94
+ collection.setFilter(createLessthanFilter(0), {silent: true});
95
+
96
+ expect(count).toEqual(0);
97
+ });
98
+ });
99
+
100
+ describe("event:add", function() {
101
+ it("should not add the new object, since it is already filtered out", function() {
102
+ collection.setFilter(createLessthanFilter(5));
103
+ expect(collection.length).toEqual(5);
104
+ allModels.add(new TehModel({value: 6}));
105
+ expect(collection.length).toEqual(5);
106
+ });
107
+
108
+ it("should add the new object, since it passes the filter", function() {
109
+ collection.setFilter(createLessthanFilter(5));
110
+ expect(collection.length).toEqual(5);
111
+ allModels.add(new TehModel({value: 1}));
112
+ expect(collection.length).toEqual(6);
113
+ expect(collection.at(5).get('value')).toEqual(1);
114
+ });
115
+
116
+ it("should add the new object to the correct location", function() {
117
+ collection.setFilter(createLessthanFilter(5));
118
+ expect(collection.length).toEqual(5);
119
+ allModels.add(new TehModel({value: 4}), {at: 0});
120
+ expect(collection.length).toEqual(6);
121
+ expect(collection.at(0).get('value')).toEqual(4);
122
+ });
123
+
124
+ it("should trigger an add event if the object was added", function() {
125
+ collection.setFilter(createLessthanFilter(5));
126
+ expect(collection.length).toEqual(5);
127
+
128
+ 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
+ });
135
+ allModels.add(newModel, {at: 0});
136
+
137
+ expect(count).toEqual(1);
138
+ });
139
+
140
+ it("should re-number elements propperly in the mapping according to what the actualy indices are in the original collection", function() {
141
+ collection.setFilter(createLessthanFilter(10));
142
+ expect(collection.length).toEqual(10);
143
+
144
+ allModels.add(new TehModel({value: 4}), {at: 6});
145
+
146
+ expect(collection._mapping).toEqual([0,1,2,3,4,5,6,7,8,9,10])
147
+ });
148
+ });
149
+
150
+ describe("event:remove", function() {
151
+ it("should be a noop since the object is filtered", function() {
152
+ collection.setFilter(createLessthanFilter(5));
153
+ expect(collection.length).toEqual(5);
154
+ allModels.remove(allModels.at(6));
155
+ expect(collection.length).toEqual(5);
156
+ });
157
+
158
+ it("should be a remove the removed object", function() {
159
+ collection.setFilter(createLessthanFilter(5));
160
+ expect(collection.length).toEqual(5);
161
+ allModels.remove(allModels.at(4));
162
+ expect(collection.length).toEqual(4);
163
+ expect(collection.at(collection.length - 1).get('value')).toEqual(3);
164
+ });
165
+
166
+ it("should re-number elements propperly in the mapping according to what the actualy indices are in the original collection", function() {
167
+ collection.setFilter(createLessthanFilter(10));
168
+ expect(collection.length).toEqual(10);
169
+
170
+ allModels.remove(allModels.at(4));
171
+
172
+ expect(collection._mapping).toEqual([0,1,2,3,4,5,6,7,8])
173
+ });
174
+ });
175
+
176
+ describe("event:reset", function() {
177
+ it("should be a noop since the object is filtered", function() {
178
+ collection.setFilter(createLessthanFilter(15));
179
+ var newAll = [];
180
+ for (var i = 10; i < 20; i++) {
181
+ newAll.push(new TehModel({value: i}));
182
+ }
183
+ allModels.reset(newAll);
184
+ expect(collection.length).toEqual(5);
185
+ expect(collection.at(4).get('value')).toEqual(14);
186
+ });
187
+ });
188
+
189
+ describe("event:sort", function() {
190
+ it("should continue filtering the collection, except with a new order", function() {
191
+ collection.setFilter(createLessthanFilter(5));
192
+ allModels.comparator = function(v1, v2) {
193
+ return v2.get("value") - v1.get("value");
194
+ };
195
+ allModels.sort();
196
+
197
+ expect(collection.length).toEqual(5);
198
+ expect(collection.at(0).get('value')).toEqual(4);
199
+ expect(collection.at(1).get('value')).toEqual(3);
200
+ expect(collection.at(2).get('value')).toEqual(2);
201
+ expect(collection.at(3).get('value')).toEqual(1);
202
+ expect(collection.at(4).get('value')).toEqual(0);
203
+ });
204
+ });
205
+
206
+ 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
+ });
212
+ allModels.trigger("filter-complete");
213
+ expect(filterFired).toEqual(1);
214
+ });
215
+
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
+ });
221
+ collection.setFilter(createLessthanFilter(3));
222
+ expect(filterFired).toEqual(1);
223
+ });
224
+
225
+ it("should fire once when a change is propagated from an underlying model", function() {
226
+ var filterFired = 0;
227
+ collection.on("filter-complete", function() {
228
+ filterFired += 1;
229
+ });
230
+ collection.setFilter(createLessthanFilter(3));
231
+ filterFired = 0;
232
+
233
+ collection.models[0].trigger("change", collection.models[0], allModels)
234
+ expect(filterFired).toEqual(1);
235
+ });
236
+ });
237
+
238
+ 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() {
240
+ collection.setFilter(createLessthanFilter(5));
241
+ origModelZero = collection.models[0];
242
+ // simulate an ajax destroy
243
+ origModelZero.trigger("destroy", origModelZero, origModelZero.collection)
244
+
245
+ expect(collection.models[0].get("value")).toEqual(1)
246
+ });
247
+
248
+ it("should remove elements from the model as events occur", function() {
249
+ collection.setFilter(createLessthanFilter(10));
250
+
251
+ // 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)
255
+
256
+ model = collection.models[3];
257
+ model.trigger("destroy", model, model.collection)
258
+ expect(collection.models[3].get("value")).toEqual(5)
259
+
260
+ model = collection.models[3];
261
+ model.trigger("destroy", model, model.collection)
262
+ expect(collection.models[3].get("value")).toEqual(6)
263
+
264
+ model = collection.models[3];
265
+ model.trigger("destroy", model, model.collection)
266
+ expect(collection.models[3].get("value")).toEqual(7)
267
+
268
+ model = collection.models[2];
269
+ model.trigger("destroy", model, model.collection)
270
+ expect(collection.models[2].get("value")).toEqual(7)
271
+
272
+ model = collection.models[1];
273
+ model.trigger("destroy", model, model.collection)
274
+ expect(collection.models[1].get("value")).toEqual(7)
275
+ });
276
+
277
+ it("should create remove events for every deleted model", function() {
278
+ collection.setFilter(createLessthanFilter(10));
279
+ var lastModelRemoved = null;
280
+ var count = 0;
281
+ collection.on("remove", function(removedModel) {
282
+ lastModelRemoved = removedModel;
283
+ count += 1;
284
+ });
285
+
286
+ // start removing in weird orders, make sure vents are done properly
287
+ count = 0;
288
+ model = collection.models[0];
289
+ model.trigger("destroy", model, model.collection)
290
+ expect(lastModelRemoved).toEqual(model);
291
+ expect(count).toEqual(1);
292
+
293
+ count = 0;
294
+ model = collection.models[3];
295
+ model.trigger("destroy", model, model.collection)
296
+ expect(lastModelRemoved).toEqual(model);
297
+ expect(count).toEqual(1);
298
+
299
+ count = 0;
300
+ model = collection.models[3];
301
+ model.trigger("destroy", model, model.collection)
302
+ expect(lastModelRemoved).toEqual(model);
303
+ expect(count).toEqual(1);
304
+
305
+ count = 0;
306
+ model = collection.models[3];
307
+ model.trigger("destroy", model, model.collection)
308
+ expect(lastModelRemoved).toEqual(model);
309
+ expect(count).toEqual(1);
310
+
311
+ count = 0;
312
+ model = collection.models[2];
313
+ model.trigger("destroy", model, model.collection)
314
+ expect(lastModelRemoved).toEqual(model);
315
+ expect(count).toEqual(1);
316
+
317
+ count = 0;
318
+ model = collection.models[1];
319
+ model.trigger("destroy", model, model.collection)
320
+ expect(lastModelRemoved).toEqual(model);
321
+ expect(count).toEqual(1);
322
+ });
323
+ });
324
+
325
+ describe("model - event:change", function() {
326
+ it("should remove the model because it failed the filter post change", function() {
327
+ collection.setFilter(createLessthanFilter(5));
328
+ origModelZero = collection.models[0];
329
+ origModelZero.set("value", 10)
330
+
331
+ expect(collection.models.length).toEqual(4)
332
+ expect(collection.models[0].get("value")).toEqual(1)
333
+ });
334
+
335
+ it("should do nothing if the model is still passing the filter", function() {
336
+ collection.setFilter(createLessthanFilter(5));
337
+ origModelZero = collection.models[0];
338
+ origModelZero.set("value", 3)
339
+
340
+ expect(collection.models.length).toEqual(5)
341
+ expect(collection.models[0].get("value")).toEqual(3)
342
+ });
343
+
344
+ it("should add the model that is now passing the filter", function() {
345
+ collection.setFilter(createLessthanFilter(5));
346
+ origModelZero = allModels.models[9];
347
+ origModelZero.set("value", 2)
348
+
349
+ expect(collection.models.length).toEqual(6)
350
+ expect(collection.models[5].get("value")).toEqual(2)
351
+ });
352
+ });
353
+ });