├── 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 | [![Travis build status](http://img.shields.io/travis/jmeas/marionette.sliding-view.svg?style=flat)](https://travis-ci.org/jmeas/marionette.sliding-view) 3 | [![Code Climate](https://codeclimate.com/github/jmeas/marionette.sliding-view/badges/gpa.svg)](https://codeclimate.com/github/jmeas/marionette.sliding-view) 4 | [![Test Coverage](https://codeclimate.com/github/jmeas/marionette.sliding-view/badges/coverage.svg)](https://codeclimate.com/github/jmeas/marionette.sliding-view) 5 | [![Dependency Status](https://david-dm.org/jmeas/marionette.sliding-view.svg)](https://david-dm.org/jmeas/marionette.sliding-view) 6 | [![devDependency Status](https://david-dm.org/jmeas/marionette.sliding-view/dev-status.svg)](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 | --------------------------------------------------------------------------------