├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── backbone.viewcache.js ├── backbone.viewcache.min.js ├── bower.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{js,json}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "amd": true, 5 | "browser": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "rules": { 9 | "camelcase": 2, 10 | "curly": [2, "multi-line", "consistent"], 11 | "indent": [2, 2], 12 | "max-len": [2, 80], 13 | "no-inline-comments": 2, 14 | "no-multi-spaces": 2, 15 | "no-multiple-empty-lines": [2, { "max": 1 }], 16 | "no-nested-ternary": 2, 17 | "no-trailing-spaces": 2, 18 | "no-unneeded-ternary": 2, 19 | "quotes": [2, "single"], 20 | "semi": 2, 21 | "semi-spacing": 2, 22 | "spaced-comment": [2, "always", { 23 | "block": { 24 | "markers": ["!"] 25 | } 26 | }], 27 | "wrap-iife": 2 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014, 2015 Ingmar Hergst 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backbone.ViewCache 2 | 3 | Maintains a cache of [Backbone][backbone] views based on the view’s route fragment. Retains the view’s scroll position by default (useful when re-inserting view elements into the DOM). 4 | 5 | Cache expiry can be set globally and per view instance. 6 | 7 | ## Installation 8 | 9 | In a browser, include the plugin after jQuery, Underscore (or an equivalent library such as lodash), and Backbone have been included. 10 | 11 | ``` html 12 | 13 | ``` 14 | 15 | *Backbone.ViewCache* can also be loaded as an [AMD module][amd] or required in CommonJS-like environments (like Node) – e.g. for use with [RequireJS][requirejs] or [Browserify][browserify]. It can be installed using the [Bower package manager][bower]. 16 | 17 | ``` bash 18 | bower install backbone.viewcache --save 19 | ``` 20 | 21 | ``` javascript 22 | // AMD 23 | require(['backbone.viewcache'], function(ViewCache){ /* ... */ }); 24 | // Node.js 25 | var ViewCache = require('backbone.viewcache'); 26 | ``` 27 | 28 | ## Usage 29 | 30 | Use *Backbone.ViewCache* in your route handlers. 31 | 32 | ```javascript 33 | home: function() { 34 | // Get the cached view for the current URL fragment. 35 | var homeView = Backbone.ViewCache.get(); 36 | 37 | if (homeView) { 38 | // Re-activate the cached view. 39 | homeView.delegateEvents(); 40 | } else { 41 | // Not in cache, instantiate a new view and cache it. 42 | homeView = Backbone.ViewCache.set(new HomeView()); 43 | 44 | homeView.render(); 45 | } 46 | 47 | // (Re-)insert the view element into the DOM. 48 | } 49 | ``` 50 | 51 | ```javascript 52 | // Remove the view for the current URL fragment from the cache. 53 | Backbone.ViewCache.remove(); 54 | 55 | // Clear the cache, or clear all expired views from the cache. 56 | Backbone.ViewCache.clear(); 57 | Backbone.ViewCache.clearExpireds(); 58 | 59 | // "get", "set", and "remove" can also be called with an optional URL 60 | // fragment argument. Defaults to `Backbone.history.fragment`. 61 | Backbone.ViewCache.get('search'); 62 | Backbone.ViewCache.set(new SearchView(), 'search'); 63 | Backbone.ViewCache.remove('search'); 64 | ``` 65 | 66 | For retaining the scroll position and auto-clear expireds functionality, *Backbone.ViewCache* `beforeRoute` and `afterRoute` methods have to be called as pre- and post-route hooks. 67 | This can be done in your router’s `execute` method (added in Backbone v1.0.0). 68 | 69 | ```javascript 70 | execute: function(callback, args) { 71 | Backbone.ViewCache.beforeRoute(); 72 | if (callback) callback.apply(this, args); 73 | Backbone.ViewCache.afterRoute(); 74 | } 75 | ``` 76 | 77 | ## Configuration 78 | 79 | Configure *Backbone.ViewCache* globally. Example with default configuration: 80 | 81 | ```javascript 82 | Backbone.ViewCache.config({ 83 | 84 | // Automatically save and restore the cached view’s scroll position. 85 | // Useful when re-inserting view elements into the DOM. 86 | retainScrollPosition: true, 87 | 88 | // Element that will be used for retaining the view’s scroll position. 89 | // Can be a selector string or DOM element. 90 | scrollElement: window, 91 | 92 | // Cached view’s expiry time in seconds, or falsy for no expiry. 93 | // Can be overridden per view with the view’s `setCacheExpiry` method. 94 | cacheExpiry: undefined, 95 | 96 | // Time in seconds to have Backbone.ViewCache automatically clear 97 | // expired views from the cache with `Backbone.ViewCache.beforeRoute`. 98 | // Defaults to the value of the "cacheExpiry" configuration setting. 99 | checkExpireds: undefined, 100 | 101 | // When restoring the cached view’s scroll position, scroll to the top of 102 | // `scrollElement` if the view currently has no saved scroll position. 103 | scrollToTopByDefault: true 104 | 105 | }); 106 | ``` 107 | 108 | ## Methods added to Backbone.View.prototype 109 | 110 | Backbone views are extended with three additional methods which are called internally and can also be called on demand: `saveScrollPosition`, `restoreScrollPosition`, and `setCacheExpiry`. 111 | 112 | ```javascript 113 | // Expire the view in 5 minutes (takes precedence over global config). 114 | homeView.setCacheExpiry(300); 115 | 116 | // While the view is in the DOM, save its scroll position. 117 | homeView.saveScrollPosition(); 118 | 119 | // While the view is in the DOM, restore its scroll position. 120 | // (Scrolls to top if the "scrollToTopByDefault" setting is on and 121 | // the view currently has no saved scroll position.) 122 | homeView.restoreScrollPosition(); 123 | ``` 124 | 125 | ## Limitations 126 | 127 | Due to a [known Android bug][android], restoring the view’s scroll position doesn’t work in the stock browser for Android 4.0.x (Ice Cream Sandwich) and lower. 128 | 129 | [backbone]: http://backbonejs.org/ 130 | [amd]: https://github.com/amdjs/amdjs-api/wiki/AMD 131 | [requirejs]: http://requirejs.org/ 132 | [browserify]: http://browserify.org/ 133 | [bower]: https://bower.io/ 134 | [android]: https://code.google.com/p/android/issues/detail?id=19625 135 | -------------------------------------------------------------------------------- /backbone.viewcache.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * backbone.viewcache.js v1.1.2 3 | * Copyright 2014, 2015 Ingmar Hergst 4 | * backbone.viewcache.js may be freely distributed under the MIT license. 5 | */ 6 | (function(root, factory){ 7 | if (typeof define === 'function' && define.amd) { 8 | // AMD. Register as an anonymous module. 9 | define(['backbone', 'underscore', 'jquery'], factory); 10 | } else if (typeof exports === 'object') { 11 | // Node. Does not work with strict CommonJS, but only CommonJS-like 12 | // environments that support module.exports, like Node. 13 | var Backbone = require('backbone'), _ = require('underscore'); 14 | Backbone.$ = Backbone.$ || require('jquery'); 15 | module.exports = factory(Backbone, _, Backbone.$); 16 | } else { 17 | // Browser globals. 18 | factory(root.Backbone, root._, root.Backbone.$); 19 | } 20 | }(this, function(Backbone, _, $, undefined){ 21 | 22 | var defaultConfig = { 23 | 24 | // Automatically save and restore the cached view’s scroll position. 25 | // Useful when re-inserting view elements into the DOM. 26 | retainScrollPosition: true, 27 | 28 | // Element that will be used for retaining the view’s scroll position. 29 | // Can be a selector string or DOM element. 30 | scrollElement: window, 31 | 32 | // Cached view’s expiry time in seconds, or falsy for no expiry. 33 | // Can be overridden per view with the view’s `setCacheExpiry` method. 34 | cacheExpiry: undefined, 35 | 36 | // Time in seconds to have Backbone.ViewCache automatically clear 37 | // expired views from the cache with `Backbone.ViewCache.beforeRoute`. 38 | // Defaults to the value of the "cacheExpiry" configuration setting. 39 | checkExpireds: undefined, 40 | 41 | // When restoring the cached view’s scroll position, scroll to the top of 42 | // `scrollElement` if the view currently has no saved scroll position. 43 | scrollToTopByDefault: true 44 | 45 | }; 46 | 47 | var config = defaultConfig; 48 | 49 | // Store for Backbone.View instances. 50 | var cachedViews = {}; 51 | 52 | // Holds the URL route fragment of the last visited view. 53 | var lastFragment; 54 | 55 | // Holds the time to determine whether expired views should be removed from 56 | // the cache. 57 | var clearExpiredsTime; 58 | 59 | function setClearExpiredsTime() { 60 | var expireTime = config.checkExpireds || config.cacheExpiry; 61 | 62 | if (expireTime) { 63 | clearExpiredsTime = _.now() + expireTime * 1000; 64 | } 65 | } 66 | 67 | // Updates the cached view’s scroll position for given fragment; 68 | // "save" or "restore", depending on the action parameter value. 69 | function scrollPosition(action, fragment) { 70 | var cachedView; 71 | 72 | if (config.retainScrollPosition) { 73 | cachedView = Backbone.ViewCache.get(fragment); 74 | cachedView && cachedView[action + 'ScrollPosition'](); 75 | } 76 | } 77 | 78 | function removeFromCache(key) { 79 | delete cachedViews[key]; 80 | } 81 | 82 | function removeFromCacheIfExpired(key) { 83 | var cachedView = cachedViews[key], 84 | expiry = cachedView && cachedView._cacheExpiry; 85 | 86 | if (expiry && _.now() > expiry) { 87 | removeFromCache(key); 88 | } 89 | } 90 | 91 | function clearCache(expireds) { 92 | var key, f = expireds ? removeFromCacheIfExpired : removeFromCache; 93 | 94 | for (key in cachedViews) { 95 | if (cachedViews.hasOwnProperty(key)) f(key); 96 | } 97 | } 98 | 99 | function getFragment(fragment) { 100 | return _.isString(fragment) ? fragment : Backbone.history.fragment; 101 | } 102 | 103 | // Add scroll position and cache expiry methods to Backbone.View.prototype. 104 | _.extend(Backbone.View.prototype, { 105 | 106 | saveScrollPosition: function() { 107 | this._scrollPosition = $(config.scrollElement).scrollTop(); 108 | }, 109 | 110 | restoreScrollPosition: function() { 111 | if (this._scrollPosition) { 112 | $(config.scrollElement).scrollTop(this._scrollPosition); 113 | } else if (config.scrollToTopByDefault) { 114 | $(config.scrollElement).scrollTop(0); 115 | } 116 | }, 117 | 118 | setCacheExpiry: function(expirationSeconds) { 119 | if (expirationSeconds) { 120 | this._cacheExpiry = _.now() + expirationSeconds * 1000; 121 | } 122 | } 123 | 124 | }); 125 | 126 | Backbone.ViewCache = { 127 | 128 | // Gets or sets the configuration. 129 | config: function(obj) { 130 | if (obj) { 131 | if (!obj.checkExpireds) { 132 | obj.checkExpireds = obj.cacheExpiry || defaultConfig.cacheExpiry; 133 | } 134 | 135 | config = _.defaults(obj, defaultConfig); 136 | } 137 | 138 | return config; 139 | }, 140 | 141 | // Gets a view from the cache using the current or a given URL fragment. 142 | // Returns the view, or `undefined` if not in cache. 143 | get: function(fragment) { 144 | fragment = getFragment(fragment); 145 | removeFromCacheIfExpired(fragment); 146 | return cachedViews[fragment]; 147 | }, 148 | 149 | // Sets a view into the cache using the current or a given URL fragment. 150 | // Returns the view. 151 | // 152 | // Unless the view already has a cache expiry time, it will be set using 153 | // the "cacheExpiry" configuration setting value. This can also be forced 154 | // with the boolean `forceCacheUpdate` parameter. 155 | set: function(view, fragment, forceCacheUpdate) { 156 | if (_.isBoolean(fragment)) forceCacheUpdate = fragment; 157 | fragment = getFragment(fragment); 158 | 159 | if (!view._cacheExpiry || forceCacheUpdate) { 160 | view.setCacheExpiry(config.cacheExpiry); 161 | } 162 | 163 | // Initial set of `clearExpiredsTime` (to start auto-clearExpireds) when 164 | // the first view with a defined cache expiry time is set into the cache. 165 | if (!clearExpiredsTime && view._cacheExpiry) { 166 | setClearExpiredsTime(); 167 | } 168 | 169 | cachedViews[fragment] = view; 170 | return view; 171 | }, 172 | 173 | // Removes a view from the cache using the current or a given URL fragment. 174 | remove: function(fragment) { 175 | fragment = getFragment(fragment); 176 | removeFromCache(fragment); 177 | }, 178 | 179 | // Removes all views from the cache. 180 | clear: function() { 181 | clearCache(); 182 | }, 183 | 184 | // Removes all expired views from the cache. 185 | clearExpireds: function() { 186 | clearCache(true); 187 | }, 188 | 189 | // Pre-route hook. To be called manually, for example in your app's router 190 | // `execute` method. Necessary for scroll positions and auto-clearExpireds. 191 | beforeRoute: function() { 192 | if (!_.isUndefined(lastFragment)) { 193 | scrollPosition('save', lastFragment); 194 | 195 | // Store the last URL fragment for convenience. 196 | this.lastUrlFragment = lastFragment; 197 | 198 | // Clear expired views from the cache. 199 | if (clearExpiredsTime && _.now() > clearExpiredsTime) { 200 | clearCache(true); 201 | setClearExpiredsTime(); 202 | } 203 | } 204 | }, 205 | 206 | // Post-route hook. To be called manually, for example in your app's router 207 | // `execute` method. Necessary for scroll positions. 208 | afterRoute: function() { 209 | var fragment = getFragment(); 210 | scrollPosition('restore', fragment); 211 | lastFragment = fragment; 212 | } 213 | 214 | }; 215 | 216 | return Backbone.ViewCache; 217 | 218 | })); 219 | -------------------------------------------------------------------------------- /backbone.viewcache.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * backbone.viewcache.js v1.1.2 3 | * Copyright 2014, 2015 Ingmar Hergst 4 | * backbone.viewcache.js may be freely distributed under the MIT license. 5 | */ 6 | !function(e,o){if("function"==typeof define&&define.amd)define(["backbone","underscore","jquery"],o);else if("object"==typeof exports){var r=require("backbone"),n=require("underscore");r.$=r.$||require("jquery"),module.exports=o(r,n,r.$)}else o(e.Backbone,e._,e.Backbone.$)}(this,function(e,o,r,n){function i(){var e=h.checkExpireds||h.cacheExpiry;e&&(f=o.now()+1e3*e)}function c(o,r){var n;h.retainScrollPosition&&(n=e.ViewCache.get(r),n&&n[o+"ScrollPosition"]())}function t(e){delete y[e]}function s(e){var r=y[e],n=r&&r._cacheExpiry;n&&o.now()>n&&t(e)}function l(e){var o,r=e?s:t;for(o in y)y.hasOwnProperty(o)&&r(o)}function a(r){return o.isString(r)?r:e.history.fragment}var u,f,p={retainScrollPosition:!0,scrollElement:window,cacheExpiry:n,checkExpireds:n,scrollToTopByDefault:!0},h=p,y={};return o.extend(e.View.prototype,{saveScrollPosition:function(){this._scrollPosition=r(h.scrollElement).scrollTop()},restoreScrollPosition:function(){this._scrollPosition?r(h.scrollElement).scrollTop(this._scrollPosition):h.scrollToTopByDefault&&r(h.scrollElement).scrollTop(0)},setCacheExpiry:function(e){e&&(this._cacheExpiry=o.now()+1e3*e)}}),e.ViewCache={config:function(e){return e&&(e.checkExpireds||(e.checkExpireds=e.cacheExpiry||p.cacheExpiry),h=o.defaults(e,p)),h},get:function(e){return e=a(e),s(e),y[e]},set:function(e,r,n){return o.isBoolean(r)&&(n=r),r=a(r),e._cacheExpiry&&!n||e.setCacheExpiry(h.cacheExpiry),!f&&e._cacheExpiry&&i(),y[r]=e,e},remove:function(e){e=a(e),t(e)},clear:function(){l()},clearExpireds:function(){l(!0)},beforeRoute:function(){o.isUndefined(u)||(c("save",u),this.lastUrlFragment=u,f&&o.now()>f&&(l(!0),i()))},afterRoute:function(){var e=a();c("restore",e),u=e}},e.ViewCache}); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.viewcache", 3 | "description": "Maintains a simple cache of Backbone views, retaining the view’s scroll position by default.", 4 | "main": "backbone.viewcache.js", 5 | "authors": [ 6 | "Ingmar Hergst" 7 | ], 8 | "license": "MIT", 9 | "keywords": [ 10 | "backbone", 11 | "viewcache", 12 | "view", 13 | "cache" 14 | ], 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "package.json" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.viewcache", 3 | "description": "Maintains a simple cache of Backbone views, retaining the view’s scroll position by default.", 4 | "version": "1.1.2", 5 | "author": "Ingmar Hergst", 6 | "license": "MIT", 7 | "main": "backbone.viewcache.js", 8 | "repository": "ingmarh/backbone.viewcache", 9 | "scripts": { 10 | "lint": "eslint $npm_package_main", 11 | "minify": "uglifyjs $npm_package_main -cm --comments '/^!/' -o $npm_package_name.min.js", 12 | "preversion": "npm run lint", 13 | "version": "npm run version:update && npm run minify && git add package.json $npm_package_name.*", 14 | "version:update": "sed -i \"1,/v.*/ s/[0-9].*/$npm_package_version/\" $npm_package_main" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^3.14.1", 18 | "uglify-js": "^2.7.5" 19 | } 20 | } 21 | --------------------------------------------------------------------------------