├── test
├── unit
│ ├── sliding-view.js
│ ├── collection-class.js
│ ├── options.js
│ ├── initial-boundaries.js
│ ├── scrolling.js
│ ├── is-small-change.js
│ ├── boundaries.js
│ └── boundary-comparator.js
├── setup
│ ├── browserify.js
│ ├── node.js
│ └── setup.js
├── runner.html
└── .jshintrc
├── CHANGELOG.md
├── .jscsrc
├── .gitignore
├── bower.json
├── .travis.yml
├── LICENSE
├── .jshintrc
├── dist
├── marionette.sliding-view.min.js
├── marionette.sliding-view.min.js.map
├── marionette.sliding-view.js
└── marionette.sliding-view.js.map
├── package.json
├── gulpfile.js
├── src
└── marionette.sliding-view.js
└── README.md
/test/unit/sliding-view.js:
--------------------------------------------------------------------------------
1 | describe('SlidingView', () => {
2 | it('should exist on Marionette', () => {
3 | expect(Mn.SlidingView).to.exist;
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### [v0.1.0](https://github.com/jmeas/marionette.sliding-view/releases/tag/v0.1.0)
2 |
3 | - Add `collectionClass` option
4 |
5 | ### [v0.0.1](https://github.com/jmeas/marionette.sliding-view/releases/tag/v0.0.1)
6 |
7 | - First release
8 |
--------------------------------------------------------------------------------
/test/setup/browserify.js:
--------------------------------------------------------------------------------
1 | var config = require('../../package.json').babelBoilerplateOptions;
2 |
3 | global.mocha.setup('bdd');
4 | global.onload = function() {
5 | global.mocha.checkLeaks();
6 | global.mocha.globals(config.mochaGlobals);
7 | global.mocha.run();
8 | require('./setup')();
9 | };
10 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "esnext": true,
3 | "validateQuoteMarks": "'",
4 | "requireDotNotation": true,
5 | "requireCurlyBraces": true,
6 | "requireCommaBeforeLineBreak": true,
7 | "requireOperatorBeforeLineBreak": true,
8 | "disallowTrailingWhitespace": true,
9 | "disallowMultipleLineStrings": true,
10 | "disallowMixedSpacesAndTabs": true,
11 | "disallowEmptyBlocks": true
12 | }
13 |
--------------------------------------------------------------------------------
/test/setup/node.js:
--------------------------------------------------------------------------------
1 | global.chai = require('chai');
2 | global.sinon = require('sinon');
3 | global.chai.use(require('sinon-chai'));
4 |
5 | // Set up JSDom
6 | global.jsdom = require('jsdom').jsdom;
7 | global.document = global.jsdom('
');
8 | global.window = global.document.parentWindow;
9 |
10 | require('babel/register');
11 | require('./setup')();
12 |
--------------------------------------------------------------------------------
/test/unit/collection-class.js:
--------------------------------------------------------------------------------
1 | var SlidingView, MyCollection, slidingView;
2 |
3 | describe('A SlidingView with a custom collectionClass', () => {
4 | beforeEach(() => {
5 | MyCollection = Bb.Collection.extend();
6 | SlidingView = Mn.SlidingView.extend({
7 | collectionClass: MyCollection
8 | });
9 | slidingView = new SlidingView({
10 | referenceCollection: new Bb.Collection()
11 | });
12 | });
13 |
14 | it('should create a collection of that Class', () => {
15 | expect(slidingView.collection).to.be.instanceof(MyCollection);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/test/setup/setup.js:
--------------------------------------------------------------------------------
1 | import _ from 'underscore';
2 | import $ from 'jquery';
3 | import Bb from 'backbone';
4 | Bb.$ = $;
5 | import Mn from 'backbone.marionette';
6 | import '../../src/marionette.sliding-view';
7 |
8 | global._ = _;
9 | global.$ = $;
10 | global.Bb = Bb;
11 | global.Mn = Mn;
12 |
13 | export default function() {
14 | global.expect = global.chai.expect;
15 |
16 | beforeEach(function() {
17 | this.sandbox = global.sinon.sandbox.create();
18 | global.stub = this.sandbox.stub.bind(this.sandbox);
19 | global.spy = this.sandbox.spy.bind(this.sandbox);
20 | });
21 |
22 | afterEach(function() {
23 | delete global.stub;
24 | delete global.spy;
25 | this.sandbox.restore();
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Commenting this out is preferred by some people, see
24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
25 | node_modules
26 | bower_components
27 | coverage
28 | tmp
29 |
30 | # Users Environment Variables
31 | .lock-wscript
32 |
--------------------------------------------------------------------------------
/test/runner.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "marionette.sliding-view",
3 | "version": "0.1.0",
4 | "homepage": "https://github.com/jmeas/marionette.sliding-view",
5 | "authors": [
6 | "Jmeas "
7 | ],
8 | "description": "A sliding Collection View in Marionette.",
9 | "main": "dist/marionette.sliding-view.js",
10 | "keywords": [
11 | "backbone",
12 | "marionette",
13 | "backbone.marionette",
14 | "slickgrid",
15 | "slick",
16 | "grid",
17 | "sliding",
18 | "infinite",
19 | "scroll",
20 | "view",
21 | "collection",
22 | "efficient",
23 | "render",
24 | "list"
25 | ],
26 | "license": "MIT",
27 | "ignore": [
28 | "**/.*",
29 | "node_modules",
30 | "bower_components",
31 | "test",
32 | "tests"
33 | ],
34 | "dependencies": {
35 | "underscore": "1.4.4 - 1.7.0",
36 | "backbone": "1.1.0 - 1.1.2",
37 | "backbone.marionette": "^2.0.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10"
4 | script: "gulp coverage"
5 | matrix:
6 | include:
7 | - node_js: "0.10"
8 | env: UNDERSCORE=1.4.4 BACKBONE=1.0 MAINRUN=false
9 | - node_js: "0.10"
10 | env: UNDERSCORE=1.5 BACKBONE=1.0 MAINRUN=false
11 | - node_js: "0.10"
12 | env: UNDERSCORE=1.6 BACKBONE=1.0 MAINRUN=false
13 | - node_js: "0.10"
14 | env: UNDERSCORE=1.4.4 BACKBONE=1.1.0 MAINRUN=false
15 | - node_js: "0.10"
16 | env: UNDERSCORE=1.5 BACKBONE=1.1 MAINRUN=false
17 | env: MAINRUN=true
18 | before_install:
19 | - npm config set ca ""
20 | install:
21 | - npm install -g grunt-cli
22 | - npm install
23 | # Which matrix settings -- otherwise default
24 | - if [[ $MAINRUN == false ]]; then npm install backbone@$BACKBONE; fi
25 | - if [[ $MAINRUN == false ]]; then npm install underscore@$UNDERSCORE; fi
26 | after_success:
27 | - if [[ $MAINRUN == true ]]; then npm install -g codeclimate-test-reporter; fi
28 | - if [[ $MAINRUN == true ]]; then codeclimate < coverage/lcov.info; fi
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 James Smith
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/test/unit/options.js:
--------------------------------------------------------------------------------
1 | var SlidingView, slidingView, collection;
2 |
3 | describe('SlidingView options', () => {
4 | describe('when passing a `collection` option', () => {
5 | it('should throw an error', () => {
6 | expect(() => {
7 | slidingView = new Mn.SlidingView({
8 | collection: new Bb.Collection()
9 | });
10 | }).to.throw('Do not pass a `collection` option to SlidingView.');
11 | });
12 | });
13 |
14 | describe('when passing in a `referenceCollection`', () => {
15 | beforeEach(() => {
16 | collection = new Bb.Collection();
17 | slidingView = new Mn.SlidingView({
18 | referenceCollection: collection
19 | });
20 | });
21 |
22 | it('should accept the option', () => {
23 | expect(slidingView.referenceCollection).to.deep.equal(collection);
24 | });
25 | });
26 |
27 | describe('when not passing a `referenceCollection`, but defining it on the prototype', () => {
28 | beforeEach(() => {
29 | collection = new Bb.Collection();
30 | SlidingView = Mn.SlidingView.extend({
31 | referenceCollection: collection
32 | });
33 | slidingView = new SlidingView();
34 | });
35 |
36 | it('should use the one on the prototype', () => {
37 | expect(slidingView.referenceCollection).to.deep.equal(collection);
38 | });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "bitwise" : true,
3 | "camelcase" : true,
4 | "eqeqeq" : true,
5 | "forin" : false,
6 | "immed" : true,
7 | "indent" : 2,
8 | "latedef" : true,
9 | "newcap" : true,
10 | "noarg" : true,
11 | "nonbsp" : true,
12 | "nonew" : true,
13 | "plusplus" : false,
14 | "undef" : true,
15 | "unused" : true,
16 | "strict" : false,
17 | "maxparams" : 4,
18 | "maxdepth" : 2,
19 | "maxstatements" : 15,
20 | "maxcomplexity" : 10,
21 | "maxlen" : 140,
22 |
23 | "asi" : false,
24 | "boss" : false,
25 | "debug" : false,
26 | "eqnull" : false,
27 | "esnext" : true,
28 | "evil" : false,
29 | "expr" : false,
30 | "funcscope" : false,
31 | "globalstrict" : false,
32 | "iterator" : false,
33 | "lastsemic" : false,
34 | "loopfunc" : false,
35 | "maxerr" : 50,
36 | "notypeof" : false,
37 | "proto" : false,
38 | "scripturl" : false,
39 | "shadow" : false,
40 | "supernew" : false,
41 | "validthis" : false,
42 | "noyield" : false,
43 |
44 | "browser" : true,
45 | "couch" : false,
46 | "devel" : false,
47 | "dojo" : false,
48 | "jquery" : false,
49 | "mootools" : false,
50 | "node" : false,
51 | "nonstandard" : false,
52 | "prototypejs" : false,
53 | "rhino" : false,
54 | "worker" : false,
55 | "wsh" : false,
56 | "yui" : false
57 | }
58 |
--------------------------------------------------------------------------------
/dist/marionette.sliding-view.min.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("underscore"),require("backbone"),require("backbone.marionette")):"function"==typeof define&&define.amd?define(["underscore","backbone","backbone.marionette"],t):t(e._,e.Backbone,e.Mn)}(this,function(e,t,o){"use strict";o.SlidingView=o.CollectionView.extend({constructor:function(){var t=void 0===arguments[0]?{}:arguments[0];if(t.collection)throw new Error("Do not pass a `collection` option to SlidingView.");t.referenceCollection&&(this.referenceCollection=t.referenceCollection),this.collection=new this.collectionClass,o.CollectionView.prototype.constructor.apply(this,arguments),this._lowerBound=e.result(this,"initialLowerBound"),this._upperBound=e.result(this,"initialUpperBound"),this._updateCollection(),this.onUpdateEvent||(this.onUpdateEvent=this.throttle(this.throttledUpdateHandler)),this.registerUpdateEvent()},collectionClass:t.Collection,registerUpdateEvent:function(){var e=this;this.$el.on("scroll",function(){e.onUpdateEvent()})},throttle:function(t){return e.throttle(t,1e3/60)},throttledUpdateHandler:function(){var e=this,t=this.getLowerBound(),o=this.getUpperBound(t);(t!==this._lowerBound||o!==this._upperBound)&&this._deferredUpdateId&&clearTimeout(this._deferredUpdateId),(t!==this._lowerBound||o!==this._upperBound)&&(this._lowerBound=t,this._upperBound=o,this._deferredUpdateId=setTimeout(function(){e._updateCollection()},50))},getLowerBound:function(){},getUpperBound:function(){},pruneCollection:function(){return this.referenceCollection.models},_updateCollection:function(){this.collection.set(this.pruneCollection(this._lowerBound,this._upperBound))}})});
2 | //# sourceMappingURL=marionette.sliding-view.min.js.map
--------------------------------------------------------------------------------
/dist/marionette.sliding-view.min.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"marionette.sliding-view.min.js","sources":["/source/marionette.sliding-view.js"],"names":["global","factory","exports","module","require","define","amd","_","Backbone","Mn","this","SlidingView","CollectionView","extend","constructor","options","undefined","arguments","collection","Error","referenceCollection","collectionClass","prototype","apply","_lowerBound","result","_upperBound","_updateCollection","onUpdateEvent","throttle","throttledUpdateHandler","registerUpdateEvent","Collection","$el","on","_this","cb","lowerBound","getLowerBound","upperBound","getUpperBound","_deferredUpdateId","clearTimeout","setTimeout","pruneCollection","models","set"],"mappings":"CAAA,SAAWA,EAAQC,GACE,gBAAZC,UAA0C,mBAAXC,QAAyBF,EAAQG,QAAQ,cAAeA,QAAQ,YAAaA,QAAQ,wBACzG,kBAAXC,SAAyBA,OAAOC,IAAMD,QAAQ,aAAc,WAAY,uBAAwBJ,GACvGA,EAAQD,EAAOO,EAAGP,EAAOQ,SAAUR,EAAOS,KAC1CC,KAAM,SAAUH,EAAGC,EAAUC,GAAM,YAEnCA,GAAGE,YAAcF,EAAGG,eAAeC,QACjCC,YAAW,cAACC,GAAOC,SAAAC,UAAA,MAAKA,UAAA,EAKtB,IAAIF,EAAQG,WACV,KAAM,IAAIC,OAAM,oDAIdJ,GAAQK,sBACVV,KAAKU,oBAAsBL,EAAQK,qBAIrCV,KAAKQ,WAAa,GAAIR,MAAKW,gBAE3BZ,EAAGG,eAAeU,UAAUR,YAAYS,MAAMb,KAAMO,WAGpDP,KAAKc,YAAcjB,EAAEkB,OAAOf,KAAM,qBAClCA,KAAKgB,YAAcnB,EAAEkB,OAAOf,KAAM,qBAClCA,KAAKiB,oBAIAjB,KAAKkB,gBACRlB,KAAKkB,cAAgBlB,KAAKmB,SAASnB,KAAKoB,yBAI1CpB,KAAKqB,uBAGPV,gBAAiBb,EAASwB,WAK1BD,oBAAmB,qBAGjBrB,MAAKuB,IAAIC,GAAG,SAAU,WACpBC,EAAKP,mBAOTC,SAAQ,SAACO,GACP,MAAO7B,GAAEsB,SAASO,EAAI,IAAK,KAI7BN,uBAAsB,sBAGhBO,EAAa3B,KAAK4B,gBAClBC,EAAa7B,KAAK8B,cAAcH,IAOhCA,IAAe3B,KAAKc,aAAee,IAAe7B,KAAKgB,cACrDhB,KAAK+B,mBACPC,aAAahC,KAAK+B,oBAKlBJ,IAAe3B,KAAKc,aAAee,IAAe7B,KAAKgB,eAK3DhB,KAAKc,YAAca,EACnB3B,KAAKgB,YAAca,EAGnB7B,KAAK+B,kBAAoBE,WAAW,WAClCR,EAAKR,qBACJ,MAKLW,cAAa,aACbE,cAAa,aAMbI,gBAAe,WACb,MAAOlC,MAAKU,oBAAoByB,QAQlClB,kBAAiB,WACfjB,KAAKQ,WAAW4B,IAAIpC,KAAKkC,gBAAgBlC,KAAKc,YAAad,KAAKgB","sourceRoot":"/Users/Jmeas/Webdev/marionette.sliding-view"}
--------------------------------------------------------------------------------
/test/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "bitwise" : true,
3 | "camelcase" : true,
4 | "eqeqeq" : true,
5 | "forin" : false,
6 | "immed" : true,
7 | "indent" : 2,
8 | "latedef" : true,
9 | "newcap" : true,
10 | "noarg" : true,
11 | "nonbsp" : true,
12 | "nonew" : true,
13 | "plusplus" : false,
14 | "undef" : true,
15 | "unused" : true,
16 | "strict" : false,
17 | "maxparams" : 4,
18 | "maxdepth" : 2,
19 | "maxstatements" : 15,
20 | "maxcomplexity" : 10,
21 | "maxlen" : 200,
22 |
23 | "asi" : false,
24 | "boss" : false,
25 | "debug" : false,
26 | "eqnull" : false,
27 | "esnext" : true,
28 | "evil" : false,
29 | "expr" : true,
30 | "funcscope" : false,
31 | "globalstrict" : false,
32 | "iterator" : false,
33 | "lastsemic" : false,
34 | "loopfunc" : false,
35 | "maxerr" : 50,
36 | "notypeof" : false,
37 | "proto" : false,
38 | "scripturl" : false,
39 | "shadow" : false,
40 | "supernew" : false,
41 | "validthis" : false,
42 | "noyield" : false,
43 |
44 | "browser" : true,
45 | "couch" : false,
46 | "devel" : false,
47 | "dojo" : false,
48 | "jquery" : false,
49 | "mootools" : false,
50 | "node" : true,
51 | "nonstandard" : false,
52 | "prototypejs" : false,
53 | "rhino" : false,
54 | "worker" : false,
55 | "wsh" : false,
56 | "yui" : false,
57 | "globals": {
58 | "Mn": true,
59 | "Bb": true,
60 | "_": true,
61 | "$": true,
62 | "console": true,
63 | "sinon": true,
64 | "spy": true,
65 | "stub": true,
66 | "describe": true,
67 | "before": true,
68 | "after": true,
69 | "beforeEach": true,
70 | "afterEach": true,
71 | "it": true,
72 | "expect": true
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "marionette.sliding-view",
3 | "version": "0.1.0",
4 | "description": "A sliding Collection View in Marionette.",
5 | "main": "dist/marionette.sliding-view.js",
6 | "scripts": {
7 | "test": "gulp",
8 | "test-browser": "gulp test-browser",
9 | "build": "gulp build",
10 | "coverage": "gulp coverage"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/jmeas/marionette.sliding-view.git"
15 | },
16 | "keywords": [
17 | "backbone",
18 | "marionette",
19 | "backbone.marionette",
20 | "slickgrid",
21 | "slick",
22 | "grid",
23 | "sliding",
24 | "infinite",
25 | "scroll",
26 | "view",
27 | "collection",
28 | "efficient",
29 | "render",
30 | "list"
31 | ],
32 | "author": "Jmeas",
33 | "license": "MIT",
34 | "bugs": {
35 | "url": "https://github.com/jmeas/marionette.sliding-view/issues"
36 | },
37 | "homepage": "https://github.com/jmeas/marionette.sliding-view",
38 | "dependencies": {
39 | "underscore": "1.4.4 - 1.7.0",
40 | "backbone": "1.0.0 - 1.1.2",
41 | "backbone.marionette": "^2.0.0"
42 | },
43 | "devDependencies": {
44 | "babel": "^4.3.0",
45 | "babelify": "^5.0.3",
46 | "browserify": "^10.2.4",
47 | "chai": "^3.0.0",
48 | "del": "^1.2.0",
49 | "esperanto": "^0.7.2",
50 | "glob": "^5.0.10",
51 | "gulp": "^3.9.0",
52 | "gulp-babel": "^5.1.0",
53 | "gulp-file": "^0.2.0",
54 | "jsdom": "^3.1.1",
55 | "gulp-filter": "^2.0.2",
56 | "gulp-istanbul": "^0.10.0",
57 | "gulp-jscs": "^1.6.0",
58 | "gulp-jshint": "^1.11.0",
59 | "gulp-livereload": "^3.8.0",
60 | "gulp-load-plugins": "^0.10.0",
61 | "gulp-mocha": "^2.1.1",
62 | "gulp-notify": "^2.2.0",
63 | "gulp-plumber": "^1.0.1",
64 | "gulp-rename": "^1.2.2",
65 | "gulp-sourcemaps": "^1.5.2",
66 | "gulp-uglifyjs": "^0.6.2",
67 | "isparta": "^3.0.3",
68 | "jquery": "^2.1.4",
69 | "jshint-stylish": "^2.0.0",
70 | "mkdirp": "^0.5.1",
71 | "mocha": "^2.2.5",
72 | "run-sequence": "^1.1.0",
73 | "sinon": "^1.14.1",
74 | "sinon-chai": "^2.8.0",
75 | "vinyl-source-stream": "^1.1.0"
76 | },
77 | "babelBoilerplateOptions": {
78 | "entryFileName": "marionette.sliding-view.js",
79 | "exportVarName": "none",
80 | "mochaGlobals": [
81 | "stub",
82 | "spy",
83 | "expect",
84 | "_",
85 | "$",
86 | "Bb",
87 | "Mn"
88 | ]
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/test/unit/initial-boundaries.js:
--------------------------------------------------------------------------------
1 | var SlidingView, slidingView, collection, initialUpperBound;
2 |
3 | describe('Initial boundaries', () => {
4 | describe('when creating a SlidingView with initialBoundary methods defined as flat values', () => {
5 | beforeEach(() => {
6 | collection = new Bb.Collection([{},{},{},{},{}]);
7 | SlidingView = Mn.SlidingView.extend({
8 |
9 | // Start with the first option from the referenceCollection
10 | initialLowerBound: 3,
11 | initialUpperBound: 4,
12 |
13 | pruneCollection(lowerBound, upperBound) {
14 | return this.referenceCollection.slice(lowerBound, upperBound);
15 | }
16 | });
17 |
18 | slidingView = new SlidingView({
19 | referenceCollection: collection
20 | });
21 | });
22 |
23 | it('should set the collection of the slidingView to be of length 1', () => {
24 | expect(slidingView.collection.length).to.equal(1);
25 | });
26 |
27 | it('should equal the correct item in the referenceCollection', () => {
28 | expect(slidingView.collection.at(0)).to.deep.equal(collection.at(3));
29 | });
30 | });
31 |
32 | describe('when creating a SlidingView with initialBoundary methods defined as methods', () => {
33 | beforeEach(() => {
34 | collection = new Bb.Collection([{},{},{},{},{}]);
35 | initialUpperBound = () => {
36 | return 4;
37 | };
38 |
39 | initialUpperBound = spy(initialUpperBound);
40 |
41 | SlidingView = Mn.SlidingView.extend({
42 |
43 | // Start with the first option from the referenceCollection
44 | initialLowerBound() {
45 | return 3;
46 | },
47 |
48 | initialUpperBound: initialUpperBound,
49 |
50 | pruneCollection(lowerBound, upperBound) {
51 | return this.referenceCollection.slice(lowerBound, upperBound);
52 | }
53 | });
54 |
55 | slidingView = new SlidingView({
56 | referenceCollection: collection
57 | });
58 | });
59 |
60 | it('should set the collection of the slidingView to be of length 1', () => {
61 | expect(slidingView.collection.length).to.equal(1);
62 | });
63 |
64 | it('should equal the correct item in the referenceCollection', () => {
65 | expect(slidingView.collection.at(0)).to.deep.equal(collection.at(3));
66 | });
67 |
68 | it('should pass the result of initialLowerBound into the initialUpperBound callback', () => {
69 | expect(initialUpperBound)
70 | .to.have.been.calledOnce
71 | .and.calledWith(3);
72 | });
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/test/unit/scrolling.js:
--------------------------------------------------------------------------------
1 | var slidingView, collection, clock;
2 |
3 | describe('When scrolling', () => {
4 | beforeEach(() => {
5 | clock = sinon.useFakeTimers();
6 | collection = new Bb.Collection([{},{},{},{},{}]);
7 | stub(Mn.SlidingView.prototype, 'throttledUpdateHandler');
8 |
9 | slidingView = new Mn.SlidingView({
10 | referenceCollection: collection
11 | });
12 |
13 | spy(slidingView, 'onUpdateEvent');
14 | });
15 |
16 | afterEach(() => {
17 | clock.restore();
18 | });
19 |
20 | describe('once', () => {
21 | beforeEach(() => {
22 | slidingView.$el.scroll();
23 | });
24 |
25 | it('should call the throttled callback once', () => {
26 | expect(slidingView.onUpdateEvent).to.have.been.calledOnce;
27 | });
28 |
29 | it('should call the scroll callback once', () => {
30 | expect(slidingView.throttledUpdateHandler).to.have.been.calledOnce;
31 | });
32 | });
33 |
34 | describe('very fast', () => {
35 | beforeEach(() => {
36 | slidingView.$el.scroll().scroll().scroll().scroll().scroll().scroll();
37 | });
38 |
39 | it('should call the throttled callback 6 times', () => {
40 | expect(slidingView.onUpdateEvent).to.have.callCount(6);
41 | });
42 |
43 | it('should call the scroll callback once', () => {
44 | expect(slidingView.throttledUpdateHandler).to.have.been.calledOnce;
45 | });
46 | });
47 |
48 | describe('at a rate of 60fps', () => {
49 | beforeEach(() => {
50 | slidingView.$el.scroll();
51 | clock.tick(17);
52 | slidingView.$el.scroll();
53 | clock.tick(17);
54 | slidingView.$el.scroll();
55 | clock.tick(17);
56 | });
57 |
58 | it('should call the throttled callback 3 times', () => {
59 | expect(slidingView.onUpdateEvent).to.have.been.calledThrice;
60 | });
61 |
62 | it('should call the scroll callback thrice', () => {
63 | expect(slidingView.throttledUpdateHandler).to.have.been.calledThrice;
64 | });
65 | });
66 |
67 | describe('batches of scrolls; each batch at 60fps', () => {
68 | beforeEach(() => {
69 | slidingView.$el.scroll().scroll().scroll();
70 | clock.tick(17);
71 | slidingView.$el.scroll().scroll();
72 | clock.tick(17);
73 | slidingView.$el.scroll().scroll().scroll().scroll();
74 | clock.tick(17);
75 | });
76 |
77 | it('should call the throttled callback 9 times', () => {
78 | expect(slidingView.onUpdateEvent).to.have.callCount(9);
79 | });
80 |
81 | // The fourth time is because Underscore queues up an extra
82 | // call during the third batch of scrolls
83 | it('should call the scroll callback four time', () => {
84 | expect(slidingView.throttledUpdateHandler).to.have.callCount(4);
85 | });
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/test/unit/is-small-change.js:
--------------------------------------------------------------------------------
1 | var SlidingView, slidingView, collection, clock;
2 |
3 | describe('isSmallChange', () => {
4 | describe('When isSmallChange is set to true, and the throttled method is called', () => {
5 | beforeEach(() => {
6 | clock = sinon.useFakeTimers();
7 |
8 | collection = new Bb.Collection([{},{},{},{},{}]);
9 |
10 | SlidingView = Mn.SlidingView.extend({
11 | initialLowerBound() {
12 | return 0;
13 | },
14 | initialUpperBound() {
15 | return 1;
16 | },
17 | getLowerBound() {
18 | return 1;
19 | },
20 | getUpperBound() {
21 | return 2;
22 | },
23 | isSmallChange: true
24 | });
25 |
26 | slidingView = new SlidingView({
27 | referenceCollection: collection
28 | });
29 |
30 | spy(slidingView, '_updateCollection');
31 | });
32 |
33 | afterEach(() => {
34 | clock.restore();
35 | });
36 |
37 | describe('and no time has passed', () => {
38 | beforeEach(() => {
39 | slidingView.throttledUpdateHandler();
40 | });
41 |
42 | it('should update the collection', () => {
43 | expect(slidingView._updateCollection).to.have.been.calledOnce;
44 | });
45 | });
46 | });
47 |
48 | describe('When isSmallChange is set to a method, and the throttled method is called', () => {
49 | beforeEach(() => {
50 | clock = sinon.useFakeTimers();
51 |
52 | collection = new Bb.Collection([{},{},{},{},{}]);
53 |
54 | SlidingView = Mn.SlidingView.extend({
55 | initialLowerBound() {
56 | return 0;
57 | },
58 | initialUpperBound() {
59 | return 1;
60 | },
61 | getLowerBound() {
62 | return 1;
63 | },
64 | getUpperBound() {
65 | return 2;
66 | },
67 | isSmallChange() {
68 | return true;
69 | }
70 | });
71 |
72 | slidingView = new SlidingView({
73 | referenceCollection: collection
74 | });
75 |
76 | spy(slidingView, '_updateCollection');
77 | spy(slidingView, 'isSmallChange');
78 | });
79 |
80 | afterEach(() => {
81 | clock.restore();
82 | });
83 |
84 | describe('and no time has passed', () => {
85 | beforeEach(() => {
86 | slidingView.throttledUpdateHandler();
87 | });
88 |
89 | it('should update the collection', () => {
90 | expect(slidingView._updateCollection).to.have.been.calledOnce;
91 | });
92 |
93 | it('should pass the boundaries to `isSmallChange`', () => {
94 | expect(slidingView.isSmallChange)
95 | .to.have.been.calledOnce
96 | .and.to.have.been.calledWith({
97 | oldLowerBound: 0,
98 | oldUpperBound: 1,
99 | lowerBound: 1,
100 | upperBound: 2
101 | });
102 | });
103 | });
104 | });
105 | });
106 |
107 |
108 |
--------------------------------------------------------------------------------
/test/unit/boundaries.js:
--------------------------------------------------------------------------------
1 | var SlidingView, slidingView, collection, clock;
2 |
3 | describe('When boundary methods are specified, and the throttled method is called', () => {
4 | beforeEach(() => {
5 | clock = sinon.useFakeTimers();
6 |
7 | collection = new Bb.Collection([{},{},{},{},{}]);
8 |
9 | SlidingView = Mn.SlidingView.extend({
10 | initialLowerBound() {
11 | return 0;
12 | },
13 | initialUpperBound() {
14 | return 1;
15 | },
16 | getLowerBound() {
17 | return 1;
18 | },
19 | getUpperBound() {
20 | return 2;
21 | }
22 | });
23 |
24 | slidingView = new SlidingView({
25 | referenceCollection: collection
26 | });
27 |
28 | stub(slidingView, 'getUpperBound');
29 | spy(slidingView, '_updateCollection');
30 | });
31 |
32 | afterEach(() => {
33 | clock.restore();
34 | });
35 |
36 | describe('and no time has passed', () => {
37 | beforeEach(() => {
38 | slidingView.throttledUpdateHandler();
39 | });
40 |
41 | it('should not update the collection', () => {
42 | expect(slidingView._updateCollection).to.not.have.been.called;
43 | });
44 |
45 | it('should pass the lower bound to the upperBound call', () => {
46 | expect(slidingView.getUpperBound)
47 | .to.have.been.calledOnce
48 | .and.calledWith(1);
49 | });
50 | });
51 |
52 | describe('and 20ms has passed', () => {
53 | beforeEach(() => {
54 | slidingView.throttledUpdateHandler();
55 | clock.tick(20);
56 | });
57 |
58 | it('should not update the collection', () => {
59 | expect(slidingView._updateCollection).to.not.have.been.called;
60 | });
61 | });
62 |
63 | describe('and 50ms has passed', () => {
64 | beforeEach(() => {
65 | slidingView.throttledUpdateHandler();
66 | clock.tick(50);
67 | });
68 |
69 | it('should call the update collection method', () => {
70 | expect(slidingView._updateCollection).to.have.been.calledOnce;
71 | });
72 |
73 | it('should set the collection correctly', () => {
74 | expect(slidingView.collection.at(0)).to.deep.equal(collection.at(0));
75 | });
76 | });
77 |
78 | describe('and the callback is called quickly, but the boundaries do not change', () => {
79 | beforeEach(() => {
80 | slidingView.throttledUpdateHandler();
81 | slidingView.throttledUpdateHandler();
82 | slidingView.throttledUpdateHandler();
83 | slidingView.throttledUpdateHandler();
84 | clock.tick(50);
85 | });
86 |
87 | it('should only update the collection once', () => {
88 | expect(slidingView._updateCollection).to.have.been.calledOnce;
89 | });
90 | });
91 |
92 | describe('and the boundaries change, but the callback is called too quickly', () => {
93 | beforeEach(() => {
94 | var count = 0;
95 | slidingView.getLowerBound = function() {
96 | return ++count;
97 | };
98 | slidingView.getUpperBound = function() {
99 | return ++count;
100 | };
101 |
102 | spy(global, 'clearTimeout');
103 | slidingView.throttledUpdateHandler();
104 | slidingView.throttledUpdateHandler();
105 | slidingView.throttledUpdateHandler();
106 | slidingView.throttledUpdateHandler();
107 | slidingView.throttledUpdateHandler();
108 | clock.tick(50);
109 | });
110 |
111 | it('should clear the timeout on all but one of them (n-1)', () => {
112 | expect(global.clearTimeout).to.have.callCount(4);
113 | });
114 |
115 | it('should only update the collection once', () => {
116 | expect(slidingView._updateCollection).to.have.been.calledOnce;
117 | });
118 | });
119 | });
120 |
--------------------------------------------------------------------------------
/test/unit/boundary-comparator.js:
--------------------------------------------------------------------------------
1 | var SlidingView, slidingView, collection, clock;
2 |
3 | describe('When boundary methods are specified with a custom comparator, and the throttled method is called', () => {
4 | beforeEach(() => {
5 | clock = sinon.useFakeTimers();
6 |
7 | collection = new Bb.Collection([{},{},{},{},{}]);
8 |
9 | SlidingView = Mn.SlidingView.extend({
10 | initialLowerBound() {
11 | return {
12 | index: 0,
13 | timestamp: 100000
14 | };
15 | },
16 | initialUpperBound() {
17 | return {
18 | index: 1,
19 | timestamp: 120000
20 | };
21 | },
22 | getLowerBound() {
23 | return {
24 | index: 1,
25 | timestamp: 120000
26 | };
27 | },
28 | getUpperBound() {
29 | return {
30 | index: 2,
31 | timestamp: 140000
32 | };
33 | },
34 |
35 | compareBoundaries(a, b) {
36 | return a.index === b.index;
37 | }
38 | });
39 |
40 | slidingView = new SlidingView({
41 | referenceCollection: collection
42 | });
43 |
44 | spy(slidingView, 'getUpperBound');
45 | spy(slidingView, '_updateCollection');
46 | });
47 |
48 | afterEach(() => {
49 | clock.restore();
50 | });
51 |
52 | describe('and no time has passing', () => {
53 | beforeEach(() => {
54 | slidingView.throttledUpdateHandler();
55 | });
56 |
57 | it('should not update the collection', () => {
58 | expect(slidingView._updateCollection).to.not.have.been.called;
59 | });
60 |
61 | it('should pass the lower bound to the upperBound call', () => {
62 | expect(slidingView.getUpperBound)
63 | .to.have.been.calledOnce
64 | .and.calledWith({
65 | index: 1,
66 | timestamp: 120000
67 | });
68 | });
69 | });
70 |
71 | describe('and 20ms has passed', () => {
72 | beforeEach(() => {
73 | slidingView.throttledUpdateHandler();
74 | clock.tick(20);
75 | });
76 |
77 | it('should not update the collection', () => {
78 | expect(slidingView._updateCollection).to.not.have.been.called;
79 | });
80 | });
81 |
82 | describe('and 50ms has passed', () => {
83 | beforeEach(() => {
84 | slidingView.throttledUpdateHandler();
85 | clock.tick(50);
86 | });
87 |
88 | it('should call the update collection method', () => {
89 | expect(slidingView._updateCollection).to.have.been.calledOnce;
90 | });
91 |
92 | it('should set the collection correctly', () => {
93 | expect(slidingView.collection.at(0)).to.deep.equal(collection.at(0));
94 | });
95 | });
96 |
97 | describe('and the callback is called quickly, but the boundaries do not change', () => {
98 | beforeEach(() => {
99 | slidingView.throttledUpdateHandler();
100 | slidingView.throttledUpdateHandler();
101 | slidingView.throttledUpdateHandler();
102 | slidingView.throttledUpdateHandler();
103 | clock.tick(50);
104 | });
105 |
106 | it('should only update the collection once', () => {
107 | expect(slidingView._updateCollection).to.have.been.calledOnce;
108 | });
109 | });
110 |
111 | describe('and the boundaries change, but the callback is called too quickly', () => {
112 | beforeEach(() => {
113 | var count = 0;
114 | slidingView.getLowerBound = function() {
115 | return {
116 | index: ++count,
117 | timestamp: 120000
118 | };
119 | };
120 | slidingView.getUpperBound = function() {
121 | return {
122 | index: ++count,
123 | timestamp: 120000
124 | };
125 | };
126 |
127 | spy(global, 'clearTimeout');
128 | slidingView.throttledUpdateHandler();
129 | slidingView.throttledUpdateHandler();
130 | slidingView.throttledUpdateHandler();
131 | slidingView.throttledUpdateHandler();
132 | slidingView.throttledUpdateHandler();
133 | clock.tick(50);
134 | });
135 |
136 | it('should clear the timeout on all but one of them (n-1)', () => {
137 | expect(global.clearTimeout).to.have.callCount(4);
138 | });
139 |
140 | it('should only update the collection once', () => {
141 | expect(slidingView._updateCollection).to.have.been.calledOnce;
142 | });
143 | });
144 | });
145 |
--------------------------------------------------------------------------------
/dist/marionette.sliding-view.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === "object" && typeof module !== "undefined" ? factory(require("underscore"), require("backbone"), require("backbone.marionette")) : typeof define === "function" && define.amd ? define(["underscore", "backbone", "backbone.marionette"], factory) : factory(global._, global.Backbone, global.Mn);
3 | })(this, function (_, Backbone, Mn) {
4 | "use strict";
5 |
6 | Mn.SlidingView = Mn.CollectionView.extend({
7 | constructor: function constructor() {
8 | var options = arguments[0] === undefined ? {} : arguments[0];
9 |
10 |
11 | // Throw an error if passed a `collection` option. SlidingView
12 | // does not follow the same API as CollectionView, so we want
13 | // to catch errors early on
14 | if (options.collection) {
15 | throw new Error("Do not pass a `collection` option to SlidingView.");
16 | }
17 |
18 | // Store the referenceCollection on the SlidingView
19 | if (options.referenceCollection) {
20 | this.referenceCollection = options.referenceCollection;
21 | }
22 |
23 | // Set the collection to be a new empty collection
24 | this.collection = new this.collectionClass();
25 |
26 | Mn.CollectionView.prototype.constructor.apply(this, arguments);
27 |
28 | // Get our initial boundaries, and then update the collection
29 | this._lowerBound = _.result(this, "initialLowerBound");
30 | this._upperBound = _.result(this, "initialUpperBound");
31 | this._updateCollection();
32 |
33 | // If no onUpdateEvent was defined, then we set one
34 | // using the `throttle` method.
35 | if (!this.onUpdateEvent) {
36 | this.onUpdateEvent = this.throttle(this.throttledUpdateHandler);
37 | }
38 |
39 | // Listen to scroll events to continuously update the collection
40 | this.registerUpdateEvent();
41 | },
42 |
43 | collectionClass: Backbone.Collection,
44 |
45 | // Register the event that calls the onUpdateEvent method. The default
46 | // is to listen to the view's own scroll event, but it could just
47 | // as easily listen to any event.
48 | registerUpdateEvent: function registerUpdateEvent() {
49 | var _this = this;
50 |
51 |
52 | // Execute the throttled callback on scroll
53 | this.$el.on("scroll", function () {
54 | _this.onUpdateEvent();
55 | });
56 | },
57 |
58 | // What we use to throttle the update event callback. Use
59 | // requestAnimationFrame in your `onUpdateEvent` callback
60 | // for better performance
61 | throttle: function throttle(cb) {
62 | return _.throttle(cb, 1000 / 60);
63 | },
64 |
65 | // Called at 60fps whenever the update event occurs
66 | throttledUpdateHandler: function throttledUpdateHandler() {
67 | var _this = this;
68 |
69 |
70 | // Pass along our arguments to the methods that calculate our boundaries
71 | var lowerBound = this.getLowerBound();
72 | var upperBound = this.getUpperBound(lowerBound);
73 |
74 | // We need to render if either of the boundaries have changed. If this is
75 | // the case, and there's already a render in the queue, then we cancel out
76 | // that queued render. This prevents users who are scrolling very fast
77 | // from getting too many renders at once. It won't render until they've
78 | // slowed down a bit.
79 | if (lowerBound !== this._lowerBound || upperBound !== this._upperBound) {
80 | if (this._deferredUpdateId) {
81 | clearTimeout(this._deferredUpdateId);
82 | }
83 | }
84 |
85 | // If the boundaries are unchanged, then we bail out early
86 | if (lowerBound === this._lowerBound && upperBound === this._upperBound) {
87 | return;
88 | }
89 |
90 | // Update our indices
91 | this._lowerBound = lowerBound;
92 | this._upperBound = upperBound;
93 |
94 | // Defer an update for 50ms. This prevents many renders when scrolling fast.
95 | this._deferredUpdateId = setTimeout(function () {
96 | _this._updateCollection();
97 | }, 50);
98 | },
99 |
100 | // The methods that determine our boundaries with each
101 | // 'update' (typically the scroll event)
102 | getLowerBound: function getLowerBound() {},
103 | getUpperBound: function getUpperBound() {},
104 |
105 | // Use the boundaries calculated in `onUpdateEvent` to prune
106 | // your collection to only the models that you wish to show. Return
107 | // an array of models to be set on the collection. The default is
108 | // to just return all of the models
109 | pruneCollection: function pruneCollection() {
110 | return this.referenceCollection.models;
111 | },
112 |
113 | // Update the collection with the results of `pruneCollection`
114 | // This leverages two important facts:
115 | // 1. Collection#set performs a 'smart' update at the data layer
116 | // 2. CollectionView performs a 'smart' update of the view layer
117 | // whenever the data layer changes
118 | _updateCollection: function _updateCollection() {
119 | this.collection.set(this.pruneCollection(this._lowerBound, this._upperBound));
120 | }
121 | });
122 | });
123 | //# sourceMappingURL=./marionette.sliding-view.js.map
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var $ = require('gulp-load-plugins')();
3 | const fs = require('fs');
4 | const del = require('del');
5 | const glob = require('glob');
6 | const path = require('path');
7 | const mkdirp = require('mkdirp');
8 | const babelify = require('babelify');
9 | const isparta = require('isparta');
10 | const esperanto = require('esperanto');
11 | const browserify = require('browserify');
12 | const runSequence = require('run-sequence');
13 | const source = require('vinyl-source-stream');
14 |
15 | const manifest = require('./package.json');
16 | const config = manifest.babelBoilerplateOptions;
17 | const mainFile = manifest.main;
18 | const destinationFolder = path.dirname(mainFile);
19 | const exportFileName = path.basename(mainFile, path.extname(mainFile));
20 |
21 | // Remove the built files
22 | gulp.task('clean', function(cb) {
23 | del([destinationFolder], cb);
24 | });
25 |
26 | // Remove our temporary files
27 | gulp.task('clean-tmp', function(cb) {
28 | del(['tmp'], cb);
29 | });
30 |
31 | // Send a notification when JSHint fails,
32 | // so that you know your changes didn't build
33 | function jshintNotify(file) {
34 | if (!file.jshint) { return; }
35 | return file.jshint.success ? false : 'JSHint failed';
36 | }
37 |
38 | function jscsNotify(file) {
39 | if (!file.jscs) { return; }
40 | return file.jscs.success ? false : 'JSRC failed';
41 | }
42 |
43 | // Lint our source code
44 | gulp.task('lint-src', function() {
45 | return gulp.src(['src/**/*.js'])
46 | .pipe($.plumber())
47 | .pipe($.jshint())
48 | .pipe($.jshint.reporter('jshint-stylish'))
49 | .pipe($.notify(jshintNotify))
50 | .pipe($.jscs())
51 | .pipe($.notify(jscsNotify))
52 | .pipe($.jshint.reporter('fail'));
53 | });
54 |
55 | // Lint our test code
56 | gulp.task('lint-test', function() {
57 | return gulp.src(['test/unit/**/*.js'])
58 | .pipe($.plumber())
59 | .pipe($.jshint())
60 | .pipe($.jshint.reporter('jshint-stylish'))
61 | .pipe($.notify(jshintNotify))
62 | .pipe($.jscs())
63 | .pipe($.notify(jscsNotify))
64 | .pipe($.jshint.reporter('fail'));
65 | });
66 |
67 | // Build two versions of the library
68 | gulp.task('build', ['lint-src', 'clean'], function(done) {
69 | esperanto.bundle({
70 | base: 'src',
71 | entry: config.entryFileName,
72 | }).then(function(bundle) {
73 | var res = bundle.toUmd({
74 | sourceMap: true,
75 | sourceMapSource: config.entryFileName + '.js',
76 | sourceMapFile: exportFileName + '.js',
77 | name: config.exportVarName
78 | });
79 |
80 | // Write the generated sourcemap
81 | mkdirp.sync(destinationFolder);
82 | fs.writeFileSync(path.join(destinationFolder, exportFileName + '.js'), res.map.toString());
83 |
84 | $.file(exportFileName + '.js', res.code, { src: true })
85 | .pipe($.plumber())
86 | .pipe($.sourcemaps.init({ loadMaps: true }))
87 | .pipe($.babel({ blacklist: ['useStrict'] }))
88 | .pipe($.sourcemaps.write('./', {addComment: false}))
89 | .pipe(gulp.dest(destinationFolder))
90 | .pipe($.filter(['*', '!**/*.js.map']))
91 | .pipe($.rename(exportFileName + '.min.js'))
92 | .pipe($.uglifyjs({
93 | outSourceMap: true,
94 | inSourceMap: destinationFolder + '/' + exportFileName + '.js.map',
95 | }))
96 | .pipe(gulp.dest(destinationFolder))
97 | .on('end', done);
98 | });
99 | });
100 |
101 | // Bundle our app for our unit tests
102 | gulp.task('browserify', function() {
103 | var testFiles = glob.sync('./test/unit/**/*');
104 | var allFiles = ['./test/setup/browserify.js'].concat(testFiles);
105 | var bundler = browserify(allFiles);
106 | bundler.transform(babelify.configure({
107 | sourceMapRelative: __dirname + '/src',
108 | blacklist: ['useStrict']
109 | }));
110 | var bundleStream = bundler.bundle();
111 | return bundleStream
112 | .on('error', function(err){
113 | console.log(err.message);
114 | this.emit('end');
115 | })
116 | .pipe($.plumber())
117 | .pipe(source('./tmp/__spec-build.js'))
118 | .pipe(gulp.dest(''))
119 | .pipe($.livereload());
120 | });
121 |
122 | gulp.task('coverage', function(done) {
123 | require('babel/register')({ modules: 'common' });
124 | gulp.src(['src/*.js'])
125 | .pipe($.plumber())
126 | .pipe($.istanbul({ instrumenter: isparta.Instrumenter }))
127 | .pipe($.istanbul.hookRequire())
128 | .on('finish', function() {
129 | return test()
130 | .pipe($.istanbul.writeReports())
131 | .on('end', done);
132 | });
133 | });
134 |
135 | function test() {
136 | return gulp.src(['test/setup/node.js', 'test/unit/**/*.js'], {read: false})
137 | .pipe($.plumber())
138 | .pipe($.mocha({reporter: 'dot', globals: config.mochaGlobals}));
139 | };
140 |
141 | // Lint and run our tests
142 | gulp.task('test', ['lint-src', 'lint-test'], function() {
143 | require('babel/register')({ modules: 'common' });
144 | return test();
145 | });
146 |
147 | // Ensure that linting occurs before browserify runs. This prevents
148 | // the build from breaking due to poorly formatted code.
149 | gulp.task('build-in-sequence', function(callback) {
150 | runSequence(['lint-src', 'lint-test'], 'browserify', callback);
151 | });
152 |
153 | // Run the headless unit tests as you make changes.
154 | gulp.task('watch', function() {
155 | gulp.watch(['src/**/*', 'test/**/*', '.jshintrc', 'test/.jshintrc'], ['test']);
156 | });
157 |
158 | // Set up a livereload environment for our spec runner
159 | gulp.task('test-browser', ['build-in-sequence'], function() {
160 | $.livereload.listen({port: 35729, host: 'localhost', start: true});
161 | return gulp.watch(['src/**/*.js', 'test/**/*', '.jshintrc', 'test/.jshintrc'], ['build-in-sequence']);
162 | });
163 |
164 | // An alias of test
165 | gulp.task('default', ['test']);
166 |
--------------------------------------------------------------------------------
/src/marionette.sliding-view.js:
--------------------------------------------------------------------------------
1 | import _ from 'underscore';
2 | import Backbone from 'backbone';
3 | import Mn from 'backbone.marionette';
4 |
5 | // Similar to _.result, but it passes additional arguments when the property is a method
6 | function execute(target, prop, ...args) {
7 | return _.isFunction(target[prop]) ? target[prop](...args) : target[prop];
8 | }
9 |
10 | Mn.SlidingView = Mn.CollectionView.extend({
11 | constructor(options = {}) {
12 |
13 | // Throw an error if passed a `collection` option. SlidingView
14 | // does not follow the same API as CollectionView, so we want
15 | // to catch errors early on
16 | if (options.collection) {
17 | throw new Error('Do not pass a `collection` option to SlidingView.');
18 | }
19 |
20 | // Store the referenceCollection on the SlidingView
21 | if (options.referenceCollection) {
22 | this.referenceCollection = options.referenceCollection;
23 | }
24 |
25 | // Set the collection to be a new empty collection
26 | this.collection = new this.collectionClass();
27 |
28 | Mn.CollectionView.prototype.constructor.apply(this, arguments);
29 |
30 | // Get our initial boundaries, and then update the collection
31 | this._lowerBound = _.result(this, 'initialLowerBound');
32 | this._upperBound = execute(this, 'initialUpperBound', this._lowerBound);
33 |
34 | this._updateCollection();
35 |
36 | // If no onUpdateEvent was defined, then we set one
37 | // using the `throttle` method.
38 | if (!this.onUpdateEvent) {
39 | this.onUpdateEvent = this.throttle(this.throttledUpdateHandler);
40 | }
41 |
42 | // Listen to scroll events to continuously update the collection
43 | this.registerUpdateEvent();
44 | },
45 |
46 | collectionClass: Backbone.Collection,
47 |
48 | // Register the event that calls the onUpdateEvent method. The default
49 | // is to listen to the view's own scroll event, but it could just
50 | // as easily listen to any event.
51 | registerUpdateEvent() {
52 |
53 | // Execute the throttled callback on scroll
54 | this.$el.on('scroll', () => {
55 | this.onUpdateEvent();
56 | });
57 | },
58 |
59 | // Whether or not the latest scroll was small enough to be rendered
60 | // immediately. Accepts a function argument that receives the old and
61 | // new boundaries as an object. Refer to the docs for more.
62 | isSmallChange: false,
63 |
64 | // This method is used to determine whether or not the boundaries have changed since the
65 | // last scroll event.
66 | // When dealing more complex charts or visualizations you may use something other than a flat value
67 | // for your boundaries. Consider if your boundaries are JavaScript objects. This method lets you determine
68 | // how equality should be calculated.
69 | compareBoundaries(a, b) {
70 | return a === b;
71 | },
72 |
73 | // What we use to throttle the update event callback. Use
74 | // requestAnimationFrame in your `onUpdateEvent` callback
75 | // for better performance. Refer to this blog post:
76 | // http://www.html5rocks.com/en/tutorials/speed/animations/
77 | // for proper use of rAF.
78 | throttle(cb) {
79 | return _.throttle(cb, 1000/60);
80 | },
81 |
82 | // Called at 60fps whenever the update event occurs
83 | throttledUpdateHandler() {
84 |
85 | // Pass along our arguments to the methods that calculate our boundaries
86 | var lowerBound = this.getLowerBound();
87 | var upperBound = this.getUpperBound(lowerBound);
88 |
89 | // We need to render if either of the boundaries have changed. If this is
90 | // the case, and there's already a render in the queue, then we cancel out
91 | // that queued render. This prevents users who are scrolling very fast
92 | // from getting too many renders at once. It won't render until they've
93 | // slowed down a bit.
94 | if (!this.compareBoundaries(lowerBound, this._lowerBound) || !this.compareBoundaries(upperBound, this._upperBound)) {
95 | if (this._deferredUpdateId) {
96 | clearTimeout(this._deferredUpdateId);
97 | }
98 | }
99 |
100 | // If the boundaries are unchanged, then we bail out early
101 | if (this.compareBoundaries(lowerBound, this._lowerBound) && this.compareBoundaries(upperBound, this._upperBound)) {
102 | return;
103 | }
104 |
105 | var oldLowerBound = this._lowerBound;
106 | var oldUpperBound = this._upperBound;
107 | this._lowerBound = lowerBound;
108 | this._upperBound = upperBound;
109 |
110 | // Determine if the change is small enough to be rendered immediately
111 | var isSmallChange = execute(this, 'isSmallChange', {
112 | oldLowerBound, oldUpperBound,
113 | lowerBound, upperBound
114 | });
115 |
116 | if (isSmallChange) {
117 | this._updateCollection();
118 | }
119 |
120 | // Defer an update for 50ms. This prevents many renders when scrolling fast.
121 | else {
122 | this._deferredUpdateId = setTimeout(() => {
123 | this._updateCollection();
124 | }, 50);
125 | }
126 | },
127 |
128 | // The methods that determine our boundaries with each
129 | // 'update' (typically the scroll event)
130 | getLowerBound() {},
131 | getUpperBound() {},
132 |
133 | // Use the boundaries calculated in `onUpdateEvent` to prune
134 | // your collection to only the models that you wish to show. Return
135 | // an array of models to be set on the collection. The default is
136 | // to just return all of the models
137 | pruneCollection() {
138 | return this.referenceCollection.models;
139 | },
140 |
141 | // Update the collection with the results of `pruneCollection`
142 | // This leverages two important facts:
143 | // 1. Collection#set performs a 'smart' update at the data layer
144 | // 2. CollectionView performs a 'smart' update of the view layer
145 | // whenever the data layer changes
146 | _updateCollection() {
147 | this.collection.set(this.pruneCollection(this._lowerBound, this._upperBound));
148 | }
149 | });
150 |
--------------------------------------------------------------------------------
/dist/marionette.sliding-view.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["marionette.sliding-view.js"],"names":[],"mappings":"AAAA,AAAC,CAAA,UAAU,MAAM,EAAE,OAAO,EAAE;AAC1B,SAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,MAAM,KAAK,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC,GAClJ,OAAO,MAAM,KAAK,UAAU,IAAI,MAAM,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC,YAAY,EAAE,UAAU,EAAE,qBAAqB,CAAC,EAAE,OAAO,CAAC,GAC/G,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA;CAC9C,CAAA,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE;AAAE,cAAY,CAAC;;AAEhD,IAAE,CAAC,WAAW,GAAG,EAAE,CAAC,cAAc,CAAC,MAAM,CAAC;AACxC,eAAW,EAAA,uBAAe;UAAd,OAAO,gCAAG,EAAE;;;;;;AAKtB,UAAI,OAAO,CAAC,UAAU,EAAE;AACtB,cAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;OACtE;;;AAGD,UAAI,OAAO,CAAC,mBAAmB,EAAE;AAC/B,YAAI,CAAC,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC;OACxD;;;AAGD,UAAI,CAAC,UAAU,GAAG,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;;AAE7C,QAAE,CAAC,cAAc,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;;;AAG/D,UAAI,CAAC,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;AACvD,UAAI,CAAC,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;AACvD,UAAI,CAAC,iBAAiB,EAAE,CAAC;;;;AAIzB,UAAI,CAAC,IAAI,CAAC,aAAa,EAAE;AACvB,YAAI,CAAC,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;OACjE;;;AAGD,UAAI,CAAC,mBAAmB,EAAE,CAAC;KAC5B;;AAED,mBAAe,EAAE,QAAQ,CAAC,UAAU;;;;;AAKpC,uBAAmB,EAAA,+BAAG;;;;;AAGpB,UAAI,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAM;AAC1B,cAAK,aAAa,EAAE,CAAC;OACtB,CAAC,CAAC;KACJ;;;;;AAKD,YAAQ,EAAA,kBAAC,EAAE,EAAE;AACX,aAAO,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,GAAC,EAAE,CAAC,CAAC;KAChC;;;AAGD,0BAAsB,EAAA,kCAAG;;;;;AAGvB,UAAI,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;AACtC,UAAI,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;;;;;;;AAOhD,UAAI,UAAU,KAAK,IAAI,CAAC,WAAW,IAAI,UAAU,KAAK,IAAI,CAAC,WAAW,EAAE;AACtE,YAAI,IAAI,CAAC,iBAAiB,EAAE;AAC1B,sBAAY,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;SACtC;OACF;;;AAGD,UAAI,UAAU,KAAK,IAAI,CAAC,WAAW,IAAI,UAAU,KAAK,IAAI,CAAC,WAAW,EAAE;AACtE,eAAO;OACR;;;AAGD,UAAI,CAAC,WAAW,GAAG,UAAU,CAAC;AAC9B,UAAI,CAAC,WAAW,GAAG,UAAU,CAAC;;;AAG9B,UAAI,CAAC,iBAAiB,GAAG,UAAU,CAAC,YAAM;AACxC,cAAK,iBAAiB,EAAE,CAAC;OAC1B,EAAE,EAAE,CAAC,CAAC;KACR;;;;AAID,iBAAa,EAAA,yBAAG,EAAE;AAClB,iBAAa,EAAA,yBAAG,EAAE;;;;;;AAMlB,mBAAe,EAAA,2BAAG;AAChB,aAAO,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC;KACxC;;;;;;;AAOD,qBAAiB,EAAA,6BAAG;AAClB,UAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;KAC/E;GACF,CAAC,CAAC;CAEJ,CAAC,CAAE","file":"marionette.sliding-view.js","sourcesContent":["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('underscore'), require('backbone'), require('backbone.marionette')) :\n typeof define === 'function' && define.amd ? define(['underscore', 'backbone', 'backbone.marionette'], factory) :\n factory(global._, global.Backbone, global.Mn)\n}(this, function (_, Backbone, Mn) { 'use strict';\n\n Mn.SlidingView = Mn.CollectionView.extend({\n constructor(options = {}) {\n\n // Throw an error if passed a `collection` option. SlidingView\n // does not follow the same API as CollectionView, so we want\n // to catch errors early on\n if (options.collection) {\n throw new Error('Do not pass a `collection` option to SlidingView.');\n }\n\n // Store the referenceCollection on the SlidingView\n if (options.referenceCollection) {\n this.referenceCollection = options.referenceCollection;\n }\n\n // Set the collection to be a new empty collection\n this.collection = new this.collectionClass();\n\n Mn.CollectionView.prototype.constructor.apply(this, arguments);\n\n // Get our initial boundaries, and then update the collection\n this._lowerBound = _.result(this, 'initialLowerBound');\n this._upperBound = _.result(this, 'initialUpperBound');\n this._updateCollection();\n\n // If no onUpdateEvent was defined, then we set one\n // using the `throttle` method.\n if (!this.onUpdateEvent) {\n this.onUpdateEvent = this.throttle(this.throttledUpdateHandler);\n }\n\n // Listen to scroll events to continuously update the collection\n this.registerUpdateEvent();\n },\n\n collectionClass: Backbone.Collection,\n\n // Register the event that calls the onUpdateEvent method. The default\n // is to listen to the view's own scroll event, but it could just\n // as easily listen to any event.\n registerUpdateEvent() {\n\n // Execute the throttled callback on scroll\n this.$el.on('scroll', () => {\n this.onUpdateEvent();\n });\n },\n\n // What we use to throttle the update event callback. Use\n // requestAnimationFrame in your `onUpdateEvent` callback\n // for better performance\n throttle(cb) {\n return _.throttle(cb, 1000/60);\n },\n\n // Called at 60fps whenever the update event occurs\n throttledUpdateHandler() {\n\n // Pass along our arguments to the methods that calculate our boundaries\n var lowerBound = this.getLowerBound();\n var upperBound = this.getUpperBound(lowerBound);\n\n // We need to render if either of the boundaries have changed. If this is\n // the case, and there's already a render in the queue, then we cancel out\n // that queued render. This prevents users who are scrolling very fast\n // from getting too many renders at once. It won't render until they've\n // slowed down a bit.\n if (lowerBound !== this._lowerBound || upperBound !== this._upperBound) {\n if (this._deferredUpdateId) {\n clearTimeout(this._deferredUpdateId);\n }\n }\n\n // If the boundaries are unchanged, then we bail out early\n if (lowerBound === this._lowerBound && upperBound === this._upperBound) {\n return;\n }\n\n // Update our indices\n this._lowerBound = lowerBound;\n this._upperBound = upperBound;\n\n // Defer an update for 50ms. This prevents many renders when scrolling fast.\n this._deferredUpdateId = setTimeout(() => {\n this._updateCollection();\n }, 50);\n },\n\n // The methods that determine our boundaries with each\n // 'update' (typically the scroll event)\n getLowerBound() {},\n getUpperBound() {},\n\n // Use the boundaries calculated in `onUpdateEvent` to prune\n // your collection to only the models that you wish to show. Return\n // an array of models to be set on the collection. The default is\n // to just return all of the models\n pruneCollection() {\n return this.referenceCollection.models;\n },\n\n // Update the collection with the results of `pruneCollection`\n // This leverages two important facts:\n // 1. Collection#set performs a 'smart' update at the data layer\n // 2. CollectionView performs a 'smart' update of the view layer\n // whenever the data layer changes\n _updateCollection() {\n this.collection.set(this.pruneCollection(this._lowerBound, this._upperBound));\n }\n });\n\n}));\n"],"sourceRoot":"/source/"}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # marionette.sliding-view
2 | [](https://travis-ci.org/jmeas/marionette.sliding-view)
3 | [](https://codeclimate.com/github/jmeas/marionette.sliding-view)
4 | [](https://codeclimate.com/github/jmeas/marionette.sliding-view)
5 | [](https://david-dm.org/jmeas/marionette.sliding-view)
6 | [](https://david-dm.org/jmeas/marionette.sliding-view#info=devDependencies)
7 |
8 | A sliding Collection View in Marionette.
9 |
10 | [**View example.**](http://jmeas.github.io/marionette.sliding-view/examples/simple-list.html)
11 |
12 | ### Motivation
13 |
14 | Some Collections contain many, many items, and rendering them all at once with a CollectionView
15 | can take a very long time. A 'sliding' CollectionView only displays some of the models at once (typically, only those
16 | visible), giving you fast load times even as the number of items goes into the tens of thousands.
17 |
18 | ### Getting Started
19 |
20 | This is a more complex view class. Accordingly, it may take some time to fully understand the API it provides.
21 | Once you've got it down, though, you should find that it's a really powerful tool.
22 |
23 | #### Concepts
24 |
25 | Understanding a few core concepts will help you to use the SlidingView.
26 |
27 | ##### Reference Collection
28 |
29 | A SlidingView has two collections: `collection` and `referenceCollection`. The `collection`
30 | represents only the models that are *currently* being displayed. The `referenceCollection` is the
31 | full list of models that the SlidingView represents.
32 |
33 | ##### Update Event
34 |
35 | The SlidingView determines if it needs to change the models that are displayed whenever the "update event"
36 | occurs. By default, the "update event" is the scroll event on the SlidingView's element.
37 |
38 | Although in most cases the update event is typically a scroll event, it could be anything.
39 |
40 | ##### Lower and Upper Boundaries
41 |
42 | The SlidingView has two internal properties, called the `lowerBound` and `upperBound`. These
43 | are two properties that can be used to determine which models from the reference collection
44 | should be displayed at any given time.
45 |
46 | There are two hooks that are used to set the boundaries, and they are called everytime that
47 | the update event occurs.
48 |
49 | In the simplest case, the boundaries will be indices that represent which indices to `slice`
50 | the `referenceCollection` at.
51 |
52 | ### API
53 |
54 | ##### `constructor( [options] )`
55 |
56 | A `CollectionView` typically receives a `collection` as an option. SlidingView is different in that you
57 | **do not** pass in a `collection`. Instead, pass it in as the option `referenceCollection`. While the
58 | `referenceCollection` represents the full list of models, the `collection` attribute will be created for
59 | you, and will be kept up-to-date with the current models that are displayed in the View.
60 |
61 | You can either pass the `referenceCollection` as an option, or specify it on the prototype.
62 |
63 | ##### `collectionClass`
64 |
65 | The Class of Collection that the SlidingView will instantiate to serve as its Collection. The default
66 | value is just `Backbone.Collection`, and, generally, you won't need to override this. Keep in mind that the
67 | models in this collection instance are the same models that exist in the `referenceCollection`.
68 |
69 | ##### `registerUpdateEvent()`
70 |
71 | A hook that lets you register when to call the `onUpdateEvent` method. By default,
72 | the SlidingView listens to `scroll` events on its own element. By overriding this,
73 | you could make it update its collection when another element (like a parent) is scrolled,
74 | or any time any event occurs.
75 |
76 | When overriding this method, use the `onUpdateEvent` method as your callback for the event.
77 |
78 | ```js
79 | var MySlidingView = Mn.SlidingView.extend({
80 |
81 | // Update whenever a model changes
82 | registerUpdateEvent: function() {
83 | var self = this;
84 | this.listenTo(someModel, 'change', function() {
85 | self.onUpdateEvent();
86 | });
87 | }
88 | });
89 | ```
90 |
91 | ##### `onUpdateEvent()`
92 |
93 | A callback that is executed every time the registered update event happens. The purpose
94 | of this callback is to throttle the *true* callback to the event, which is
95 | `throttledUpdateHandler`.
96 |
97 | The default behavior is to throttle the `throttledUpdateHandler` method using the `throttle`
98 | method on the SlidingView.
99 |
100 | For a big performance boost, you are highly encouraged to override this method to use
101 | `requestAnimationFrame`.
102 |
103 | ```js
104 | var MySlidingView = Mn.SlidingView.extend({
105 |
106 | // Use requestAnimationFrame for a big performance boost!
107 | onUpdateEvent: function() {
108 | requestAnimationFrame(this.throttledUpdateHandler);
109 | }
110 | });
111 | ```
112 |
113 | ##### `throttledUpdateHandler()`
114 |
115 | This is the method that contains all of the logic for the intelligent SlidingView updates. It is
116 | not recommended that you override this method. You only need to do anything with it when defining
117 | a custom `onUpdateEvent` method.
118 |
119 | ##### `throttle( fn )`
120 |
121 | If you're not using `requestAnimationFrame` (you should be!), then you can specify how
122 | to throttle `fn` here. The default implementation is to use `_.throttle` at 60 fps.
123 |
124 | Note that if you **are** using `requestAnimationFrame`, then you can ignore this method
125 | entirely.
126 |
127 | ##### `pruneCollection(lowerBound, upperBound)`
128 |
129 | Use the values of `lowerBound` and `upperBound` to calculate a list of models to be
130 | `set` on the SlidingView's `collection`. By default, all of the models from
131 | `referenceCollection` are returned.
132 |
133 | If your upper and lower boundaries reference indices, then you could `slice` your collection
134 | to return just the models within those indices.
135 |
136 | ```js
137 | var MySlidingView = Mn.SlidingView.extend({
138 | pruneCollection: function(lowerBound, upperBound) {
139 | return this.referenceCollection.slice(lowerBound, upperBound)
140 | }
141 | });
142 | ```
143 |
144 | ##### `initialLowerBound`
145 |
146 | The initial lower boundary for the SlidingView. It can be a flat value or a function.
147 |
148 | ```js
149 | var MySlidingView = Mn.SlidingView.extend({
150 | initialLowerBound: 3
151 | });
152 | ```
153 |
154 | ##### `initialUpperBound( initialLowerBound )`
155 |
156 | The initial upper boundary for the SlidingView. It can be a flat value or a function. When a
157 | function is provided, it will be passed the initial lower boundary.
158 |
159 | ```js
160 | var MySlidingView = Mn.SlidingView.extend({
161 | initialLowerBound: function(initialLowerBound) {
162 | return initialLowerBound + 5;
163 | }
164 | });
165 | ```
166 |
167 | ##### `getLowerBound()`
168 |
169 | A function that is called each time the update event occurs. Within this method
170 | you should calculate the new value of the `lowerBound` and return it.
171 |
172 | ##### `getUpperBound( lowerBound )`
173 |
174 | Similar to the above, but for the upper boundary. It is passed the `lowerBound` that
175 | was just computed, if you need to use that as a reference.
176 |
177 | ##### `compareBoundaries( a, b )`
178 |
179 | This method is used to determine whether or not two boundaries are equal. The default implementation is
180 | simply `a === b`, which works if you're using simple boundaries, like numbers or strings. Sometimes, though,
181 | more complex charts or grids require returning JavaScript Objects as boundaries. This hook allows you to
182 | define how those Objects should be compared.
183 |
184 | ##### `isSmallChange( boundaries )`
185 |
186 | `isSmallChange` determines whether the render will occur instantly or if it will be delayed. The delay prevents too many
187 | items from being rendered at a single time, which greatly improves performance.
188 |
189 | `isSmallChange` can be provided as a flat value or as a function. When a function is provided, it is passed an object with
190 | four properties:
191 |
192 | - `oldLowerBound` - The previous lower boundary
193 | - `oldUpperBound` - The previous upper boundary
194 | - `lowerBound` - The new lower boundary
195 | - `upperBound` - The new upper boundary
196 |
197 | If your boundaries are indices, you might implement an `isSmallChange` method like so:
198 |
199 | ```js
200 | isSmallChange: function(bounds) {
201 | var lowerBoundDiff = Math.abs(bounds.oldLowerBound - bounds.lowerBound);
202 | var upperBoundDiff = Math.abs(bounds.oldUpperBound - bounds.upperBound);
203 |
204 | // This means that anytime less than 6 items are added/removed we will render
205 | // immediately rather than waiting 50ms
206 | return lowerBoundDiff + upperBoundDiff < 6;
207 | }
208 | ```
209 |
--------------------------------------------------------------------------------