├── .gitignore ├── .travis.yml ├── gruntfile.js ├── test-runner.html ├── README.md ├── LICENSE ├── package.json ├── backbone.containerview.min.js ├── backbone.containerview.min.map ├── backbone.containerview.js └── test-runner.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.11" 5 | - "0.10" 6 | - "0.8" 7 | 8 | before_script: 9 | - npm install -g grunt-cli -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | uglify: { 6 | options: { 7 | banner: '// Backbone.ContainerView <%= pkg.version %>\n// (c) 2014 Greg MacWilliam\n// Freely distributed under the MIT license\n', 8 | sourceMapRoot: './', 9 | sourceMap: '<%= pkg.name %>.min.map', 10 | sourceMapUrl: '<%= pkg.name %>.min.map' 11 | }, 12 | target: { 13 | src: '<%= pkg.name %>.js', 14 | dest: '<%= pkg.name %>.min.js' 15 | } 16 | }, 17 | mocha_phantomjs: { 18 | all: ['test-runner.html'] 19 | } 20 | }); 21 | 22 | grunt.loadNpmTasks("grunt-contrib-uglify"); 23 | grunt.loadNpmTasks('grunt-mocha-phantomjs'); 24 | 25 | grunt.registerTask("default", ["mocha_phantomjs", "uglify"]); 26 | grunt.registerTask("test", ["mocha_phantomjs"]); 27 | grunt.registerTask("build", ["uglify"]); 28 | }; -------------------------------------------------------------------------------- /test-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Backbone.ContainerView Test Suite 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 20 | 21 | 22 | 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backbone.ContainerView 2 | 3 | ![build status](https://api.travis-ci.org/gmac/backbone.containerview.png) 4 | 5 | ContainerView is a lightweight `Backbone.View` extention/mixin that provides fast and efficient subview rendering and lifecycle management. Its design focuses on memory management, rendering optimizations, and convenience workflows. 6 | 7 | ## Help & Documentation 8 | 9 | All documentation is available on the [wiki](https://github.com/gmac/backbone.containerview/wiki): 10 | 11 | - [Overview](https://github.com/gmac/backbone.containerview/wiki/Overview) 12 | - [Installation](https://github.com/gmac/backbone.containerview/wiki/Installation) 13 | - [Getting Started](https://github.com/gmac/backbone.containerview/wiki/Getting-Started) 14 | - [API Documentation](https://github.com/gmac/backbone.containerview/wiki/API-Documentation) 15 | 16 | ## Tests 17 | 18 | Open `test-runner.html` in a web browser, or run: 19 | 20 | npm install 21 | npm install -g grunt-cli 22 | grunt test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014 Greg MacWilliam. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.containerview", 3 | "description": "A fast and efficient subview manager and renderer.", 4 | "homepage": "https://github.com/gmac/backbone.containerview", 5 | "url": "https://github.com/gmac/backbone.containerview", 6 | "keywords": [ 7 | "backbone", 8 | "plugin", 9 | "view", 10 | "layout", 11 | "manager" 12 | ], 13 | "author": { 14 | "name": "Greg MacWilliam", 15 | "email": "gmacwill77@gmail.com", 16 | "web": "https://github.com/gmac" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/gmac/backbone.containerview.git" 21 | }, 22 | "github": "https://github.com/gmac/backbone.containerview", 23 | "license": "MIT", 24 | "dependencies": { 25 | "jquery": "~2.1.0-beta3", 26 | "backbone": "~1.1.0", 27 | "underscore": "~1.5.2" 28 | }, 29 | "main": "backbone.viewkit.js", 30 | "version": "0.1.0", 31 | "devDependencies": { 32 | "mocha": "~1.15.1", 33 | "chai": "~1.8.1", 34 | "sinon": "~1.7.3", 35 | "grunt": "~0.4.2", 36 | "grunt-contrib-uglify": "~0.2.7", 37 | "grunt-mocha-phantomjs": "~0.3.2" 38 | }, 39 | "scripts": { 40 | "test": "grunt test" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backbone.containerview.min.js: -------------------------------------------------------------------------------- 1 | // Backbone.ContainerView 0.1.0 2 | // (c) 2014 Greg MacWilliam 3 | // Freely distributed under the MIT license 4 | 5 | !function(a,b){"undefined"!=typeof exports?module.exports=b(require("backbone"),require("underscore")):"function"==typeof define&&define.amd?define(["backbone","underscore"],b):b(a.Backbone,a._)}(this,function(a,b){function c(b,c){if(b instanceof a.View)return!0;if(c)throw"Not a View instance";return!1}function d(a,b){return b?b instanceof e?b:a.$(b):a.$el}var e=a.$,f=a.View.prototype,g="*",h=a.ContainerView=a.View.extend({contentView:null,contentModels:null,_sv:function(a){return a&&b.isArray(a)&&(this.__sv=a),this.__sv||(this.__sv=[]),this.__sv},addSubview:function(a){a=b.isArray(a)?a:Array.prototype.slice.call(arguments);var d=b.filter(a,function(a){return c(a,!0)&&a!==this?(this._sv().push(a),!0):void 0},this);return d.length?1==d.length?d[0]:d:this},releaseSubviews:function(a,d){d=d||{};var e=a,f=this._sv(),h=f,i=d.remove;return e===g?(i&&b.invoke(f,"remove"),h.length=0):c(a)?(i&&a.remove(),h=b.without(f,a)):h=b.reject(f,function(a){return a.$el.is(e)?(i&&a.remove(),!0):void 0}),this._sv(h),this},removeSubviews:function(a,b){return b=b||{},b.remove=!0,this.releaseSubviews(a,b)},numSubviews:function(){return this._sv().length},createSubcontainer:function(a){var b=d(this,a);return b.is(this.$el)?this:this.addSubview(h.create(b))},contentFilter:function(){return!0},contentSort:null,renderContent:function(){var a=[],d=this.contentView,e=this.contentModels,f=document.createDocumentFragment();return b.isFunction(d)&&b.isArray(e)?(b.isFunction(this.contentSort)&&(e=e.slice(),e.sort(b.bind(this.contentSort,this))),b.each(e,function(b,c){if(this.contentFilter(b,c)){var e=new d({model:b});a.push(e),e.render(),f.appendChild(e.el)}},this)):c(d)&&(d.render(),a.push(d),f.appendChild(d.el)),this.$el.html(f),this.removeSubviews(g),this._sv(a),this},open:function(d,e){return e instanceof a.Collection?e=e.models:e instanceof a.Model&&(e=[e]),this.contentModels=b.isArray(e)?e:null,this.contentView=b.isFunction(d)||c(d,!0)?d:null,this.renderContent()},close:function(){return this.$el.empty(),this.removeSubviews(g),this.contentView=this.contentModels=null,this},append:function(a,b){return a=this.addSubview(a),a.render(),d(this,b).append(a.$el),this},swapIn:function(a,b){return b?(a=this.addSubview(a),a.render(),d(this,b).replaceWith(a.$el),this):this.append(a,b)},remove:function(){var a=f.remove.apply(this,arguments);return this.close(),a}},{create:function(a){return new h({el:a})},install:function(c){a.View.prototype=c!==!1?b.extend({},f,h.prototype):f}});return h}); 6 | //# sourceMappingURL=backbone.containerview.min.map -------------------------------------------------------------------------------- /backbone.containerview.min.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"backbone.containerview.min.js","sources":["backbone.containerview.js"],"names":["root","factory","exports","module","require","define","amd","Backbone","_","this","isView","view","error","View","resolveViewSelector","selector","$","$el","ViewPrototype","prototype","ALL","ContainerView","extend","contentView","contentModels","_sv","subviews","isArray","__sv","addSubview","views","Array","slice","call","arguments","added","filter","push","length","releaseSubviews","options","revised","remove","invoke","without","reject","is","removeSubviews","numSubviews","createSubcontainer","$container","create","contentFilter","contentSort","renderContent","content","document","createDocumentFragment","isFunction","sort","bind","each","model","index","render","appendChild","el","html","open","models","Collection","Model","close","empty","append","swapIn","replaceWith","result","apply","install","enable"],"mappings":"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","sourceRoot":"./"} -------------------------------------------------------------------------------- /backbone.containerview.js: -------------------------------------------------------------------------------- 1 | // Backbone.ContainerView 2 | // (c) 2014 Greg MacWilliam 3 | // Freely distributed under the MIT license 4 | (function (root, factory) { 5 | 6 | if (typeof exports !== 'undefined') { 7 | module.exports = factory(require('backbone'), require('underscore')); 8 | } else if (typeof define === 'function' && define.amd) { 9 | define(['backbone', 'underscore'], factory); 10 | } else { 11 | factory(root.Backbone, root._); 12 | } 13 | 14 | }(this, function (Backbone, _) { 15 | 16 | var $ = Backbone.$; 17 | var ViewPrototype = Backbone.View.prototype; 18 | var ALL = '*'; 19 | 20 | function isView(view, error) { 21 | if (view instanceof Backbone.View) return true; 22 | if (error) throw ('Not a View instance'); 23 | return false; 24 | } 25 | 26 | function resolveViewSelector(view, selector) { 27 | if (!selector) return view.$el; 28 | if (selector instanceof $) return selector; 29 | return view.$(selector); 30 | } 31 | 32 | // ContainerView 33 | // -------------------------- 34 | var ContainerView = Backbone.ContainerView = Backbone.View.extend({ 35 | contentView: null, 36 | contentModels: null, 37 | 38 | // Subviews list accessor: 39 | // Creates a managed subview array if one does not yet exist. 40 | // Subviews list should always be accessed through this method. 41 | _sv: function (subviews) { 42 | if (subviews && _.isArray(subviews)) this.__sv = subviews; 43 | if (!this.__sv) this.__sv = []; 44 | return this.__sv; 45 | }, 46 | 47 | // Adds a managed subview to the container: 48 | addSubview: function (views) { 49 | views = _.isArray(views) ? views : Array.prototype.slice.call(arguments); 50 | 51 | var added = _.filter(views, function (view) { 52 | if (isView(view, true) && view !== this) { 53 | this._sv().push(view); 54 | return true; 55 | } 56 | }, this); 57 | 58 | if (!added.length) return this; 59 | return added.length == 1 ? added[0] : added; 60 | }, 61 | 62 | // Finds and removes a managed subview or view selector: 63 | // accepts a View object reference or selector string. 64 | releaseSubviews: function (view, options) { 65 | options = options || {}; 66 | var selector = view; 67 | var subviews = this._sv(); 68 | var revised = subviews; 69 | var remove = options.remove; 70 | 71 | if (selector === ALL) { 72 | // Releases all subview references: 73 | if (remove) _.invoke(subviews, 'remove'); 74 | revised.length = 0; 75 | 76 | } else if (isView(view)) { 77 | // Removes a single View instance: 78 | if (remove) view.remove(); 79 | revised = _.without(subviews, view); 80 | 81 | } else { 82 | // Removes all subviews that match the provided selector: 83 | revised = _.reject(subviews, function (view) { 84 | if (view.$el.is(selector)) { 85 | if (remove) view.remove(); 86 | return true; 87 | } 88 | }); 89 | } 90 | 91 | this._sv(revised); 92 | return this; 93 | }, 94 | 95 | // Convenience method for releasing AND removing subviews: 96 | removeSubviews: function (view, options) { 97 | options = options || {}; 98 | options.remove = true; 99 | return this.releaseSubviews(view, options); 100 | }, 101 | 102 | // Specifies the number of subviews currently managed by the container: 103 | numSubviews: function () { 104 | return this._sv().length; 105 | }, 106 | 107 | // Creates a new managed container view within the view: 108 | createSubcontainer: function (selector) { 109 | var $container = resolveViewSelector(this, selector); 110 | if (!$container.is(this.$el)) { 111 | return this.addSubview(ContainerView.create($container)); 112 | } 113 | return this; 114 | }, 115 | 116 | // Filter method used while rendering model lists: 117 | contentFilter: function (model) { 118 | return true; 119 | }, 120 | 121 | // Sort method used while rendering model lists: 122 | contentSort: null, 123 | 124 | // Renders the current model/view content configuration: 125 | renderContent: function () { 126 | var subviews = []; 127 | var contentView = this.contentView; 128 | var contentModels = this.contentModels; 129 | var content = document.createDocumentFragment(); 130 | 131 | // Render view constructor with models list: 132 | if (_.isFunction(contentView) && _.isArray(contentModels)) { 133 | 134 | // Sort models array when a sort method is defined: 135 | if (_.isFunction(this.contentSort)) { 136 | contentModels = contentModels.slice(); 137 | contentModels.sort(_.bind(this.contentSort, this)); 138 | } 139 | 140 | // Loop through collection and render all models that pass the filter: 141 | _.each(contentModels, function (model, index) { 142 | if (this.contentFilter(model, index)) { 143 | var view = new contentView({ 144 | model: model 145 | }); 146 | subviews.push(view); 147 | view.render(); 148 | content.appendChild(view.el); 149 | } 150 | }, this); 151 | 152 | // Render single view: 153 | } else if (isView(contentView)) { 154 | contentView.render(); 155 | subviews.push(contentView); 156 | content.appendChild(contentView.el); 157 | } 158 | 159 | // Replace container content, then cleanup old views and cache new subviews: 160 | this.$el.html(content); 161 | this.removeSubviews(ALL); 162 | this._sv(subviews); 163 | return this; 164 | }, 165 | 166 | // Opens a single view, or a view constructor with a collection of models: 167 | open: function (view, models) { 168 | // Convert collection/model instances to an array: 169 | if (models instanceof Backbone.Collection) models = models.models; 170 | else if (models instanceof Backbone.Model) models = [models]; 171 | 172 | this.contentModels = _.isArray(models) ? models : null; 173 | this.contentView = (_.isFunction(view) || isView(view, true)) ? view : null; 174 | return this.renderContent(); 175 | }, 176 | 177 | // Closes the region by emptying the display and releasing all content references: 178 | // The region is still usable for presenting content after calling "close". 179 | close: function () { 180 | this.$el.empty(); 181 | this.removeSubviews(ALL); 182 | this.contentView = this.contentModels = null; 183 | return this; 184 | }, 185 | 186 | // Adds a one-off view onto the end of the list: 187 | append: function (view, selector) { 188 | view = this.addSubview(view); 189 | view.render(); 190 | resolveViewSelector(this, selector).append(view.$el); 191 | return this; 192 | }, 193 | 194 | // Inserts a subview by replacing the targeted selector element: 195 | swapIn: function (view, selector) { 196 | if (!selector) { 197 | return this.append(view, selector); 198 | } 199 | 200 | view = this.addSubview(view); 201 | view.render(); 202 | resolveViewSelector(this, selector).replaceWith(view.$el); 203 | return this; 204 | }, 205 | 206 | // Removes the view by emptying, releasing all content, and orphaning the container: 207 | // The region is no longer usable after being removed. 208 | remove: function () { 209 | var result = ViewPrototype.remove.apply(this, arguments); 210 | this.close(); 211 | return result; 212 | } 213 | 214 | // STATIC API: 215 | }, { 216 | // Convenience method for creating a new container view: 217 | create: function (selector) { 218 | return new ContainerView({ 219 | el: selector 220 | }); 221 | }, 222 | 223 | // Installs ContainerView methods globally onto Backbone.View: 224 | install: function (enable) { 225 | Backbone.View.prototype = (enable !== false) ? 226 | _.extend({}, ViewPrototype, ContainerView.prototype) : ViewPrototype; 227 | } 228 | }); 229 | 230 | return ContainerView; 231 | })); -------------------------------------------------------------------------------- /test-runner.js: -------------------------------------------------------------------------------- 1 | var ContainerView = Backbone.ContainerView; 2 | 3 | describe('Backbone.ContainerView', function() { 4 | 5 | var view; 6 | var luke; 7 | var leia; 8 | var models; 9 | 10 | var ItemView = Backbone.View.extend({ 11 | tagName: 'span', 12 | attributes: function() { 13 | return {'data-name': this.model.get('name')}; 14 | } 15 | }); 16 | 17 | beforeEach(function() { 18 | view = new ContainerView({el: '
'}); 19 | luke = new Backbone.View({el: '
'}); 20 | leia = new Backbone.View({el: '
'}); 21 | models = [ 22 | new Backbone.Model({name: 'luke'}), 23 | new Backbone.Model({name: 'leia'}) 24 | ]; 25 | }); 26 | 27 | it('should define a Backbone.ContainerView constructor function', function() { 28 | expect(Backbone.ContainerView).to.exist; 29 | expect(_.isFunction(Backbone.ContainerView)).to.be.true; 30 | }); 31 | 32 | it('should extend Backbone.ContainerView instances from Backbone.View', function() { 33 | expect(view).to.be.instanceof(Backbone.View); 34 | }); 35 | 36 | /*it('should define Backbone.View as its default "_super" reference', function() { 37 | expect(view._super).to.equal(Backbone.View); 38 | });*/ 39 | 40 | it('should provide an array singleton for caching subview references', function() { 41 | var cache = view._sv(); 42 | expect(cache).to.be.instanceof(Array); 43 | expect(cache).to.equal(view._sv()); 44 | }); 45 | 46 | it('should provide a static "create" method for generating instances', function() { 47 | var container = ContainerView.create(''); 48 | expect(container).to.be.instanceof(ContainerView); 49 | expect(container.el.tagName.toLowerCase()).to.equal('span'); 50 | }); 51 | 52 | it('addSubview: should throw an error when adding a non-view object', function() { 53 | expect(function() { 54 | view.addSubview({}); 55 | }).to.throw(); 56 | }); 57 | 58 | it('numSubviews: should specify the current number of managed subviews view', function() { 59 | view.addSubview(luke); 60 | expect(view.numSubviews()).to.equal(1); 61 | 62 | view.addSubview(leia); 63 | expect(view.numSubviews()).to.equal(2); 64 | }); 65 | 66 | it('addSubview: should add and return a single view instance', function() { 67 | var returned = view.addSubview(luke); 68 | expect(view.numSubviews()).to.equal(1); 69 | expect(view._sv()[0]).to.equal(luke); 70 | expect(returned).to.equal(luke); 71 | }); 72 | 73 | it('addSubview: should add and return an array of view instances', function() { 74 | var returned = view.addSubview([luke, leia]); 75 | expect(view.numSubviews()).to.equal(2); 76 | expect(view._sv()[0]).to.equal(luke); 77 | expect(view._sv()[1]).to.equal(leia); 78 | expect(returned).to.be.instanceof(Array); 79 | expect(returned).to.have.length(2); 80 | }); 81 | 82 | it('addSubview: should add and return a list of view instance arguments', function() { 83 | var returned = view.addSubview(luke, leia); 84 | expect(view.numSubviews()).to.equal(2); 85 | expect(view._sv()[0]).to.equal(luke); 86 | expect(view._sv()[1]).to.equal(leia); 87 | expect(returned).to.be.instanceof(Array); 88 | expect(returned).to.have.length(2); 89 | }); 90 | 91 | it('releaseSubviews: should release a specific subview instance', function() { 92 | view.addSubview(luke, leia); 93 | view.releaseSubviews(leia); 94 | expect(view.numSubviews()).to.equal(1); 95 | expect(view._sv()[0]).to.equal(luke); 96 | }); 97 | 98 | it('releaseSubviews: should release and remove a specific subview instance', function() { 99 | var removeLeia = sinon.spy(leia, 'remove'); 100 | view.addSubview(luke, leia); 101 | view.releaseSubviews(leia, {remove: true}); 102 | expect(removeLeia.calledOnce).to.be.true; 103 | }); 104 | 105 | it('releaseSubviews: should release a selected subview instance', function() { 106 | view.addSubview(luke, leia); 107 | view.releaseSubviews('.luke'); 108 | expect(view.numSubviews()).to.equal(1); 109 | expect(view._sv()[0]).to.equal(leia); 110 | }); 111 | 112 | it('releaseSubviews: should release and remove a selected subview instance', function() { 113 | var removeLuke = sinon.spy(luke, 'remove'); 114 | view.addSubview(luke, leia); 115 | view.releaseSubviews('.luke', {remove: true}); 116 | expect(removeLuke.calledOnce).to.be.true; 117 | }); 118 | 119 | it('releaseSubviews: should use "*" to release all subview instances', function() { 120 | view.addSubview(luke, leia); 121 | view.releaseSubviews('*'); 122 | expect(view.numSubviews()).to.equal(0); 123 | }); 124 | 125 | it('releaseSubviews: should use "*" to release and remove all subview instances', function() { 126 | var removeLuke = sinon.spy(luke, 'remove'); 127 | var removeLeia = sinon.spy(leia, 'remove'); 128 | view.addSubview(luke, leia); 129 | view.releaseSubviews('*', {remove: true}); 130 | expect(removeLuke.calledOnce).to.be.true; 131 | expect(removeLeia.calledOnce).to.be.true; 132 | }); 133 | 134 | it('createSubcontainer: should create a new managed container for a selected child element', function() { 135 | var region = view.createSubcontainer('.region'); 136 | expect(view.numSubviews()).to.equal(1); 137 | expect(region).to.be.instanceof(ContainerView); 138 | }); 139 | 140 | it('append: should add a new managed subview into the view\'s root container element', function() { 141 | view.append(luke); 142 | expect(view.$el.children()).to.have.length(2); 143 | expect(view.numSubviews()).to.equal(1); 144 | }); 145 | 146 | it('append: should add a new managed subview into a selected container element', function() { 147 | view.append(luke, '.region'); 148 | expect(view.$el.children()).to.have.length(1); 149 | expect(view.$('.region').children()).to.have.length(1); 150 | expect(view.numSubviews()).to.equal(1); 151 | }); 152 | 153 | it('swapIn: should add a new managed subview in place of a selected container element', function() { 154 | view.swapIn(luke, '.region'); 155 | expect(view.$el.children()).to.have.length(1); 156 | expect(view.$('.region')).to.have.length(0); 157 | expect(view.$('.luke')).to.have.length(1); 158 | expect(view.numSubviews()).to.equal(1); 159 | }); 160 | 161 | it('swapIn: should defer to "append" if no target selector is specified', function() { 162 | view.swapIn(luke); 163 | expect(view.$el.children()).to.have.length(2); 164 | expect(view.$('.region')).to.have.length(1); 165 | expect(view.$('.luke')).to.have.length(1); 166 | expect(view.numSubviews()).to.equal(1); 167 | }); 168 | 169 | /*it('empty: should empty the container element, and remove all subviews', function() { 170 | luke.remove = sinon.spy(); 171 | leia.remove = sinon.spy(); 172 | 173 | // Add subviews and validate configuration: 174 | view.append(luke); 175 | view.append(leia); 176 | expect(view.$el.children()).to.have.length(3); 177 | expect(view.numSubviews()).to.equal(2); 178 | 179 | // Empty the view, and validate cleanup: 180 | view.empty(); 181 | expect(view.$el.children()).to.have.length(0); 182 | expect(view.numSubviews()).to.equal(0); 183 | expect(luke.remove.calledOnce).to.be.true; 184 | expect(leia.remove.calledOnce).to.be.true; 185 | });*/ 186 | 187 | it('open: should open a single subview into the container', function() { 188 | view.open(luke); 189 | expect(view.$el.children()).to.have.length(1); 190 | expect(view.$('.luke')).to.have.length(1); 191 | expect(view.numSubviews()).to.equal(1); 192 | }); 193 | 194 | it('open: should replace existing container contents with opened view', function() { 195 | view.open(luke); 196 | expect(view.numSubviews()).to.equal(1); 197 | 198 | view.open(leia); 199 | expect(view.$el.children()).to.have.length(1); 200 | expect(view.$('.leia')).to.have.length(1); 201 | expect(view.numSubviews()).to.equal(1); 202 | }); 203 | 204 | it('open: should open a list of views, rendered from a view class and models array', function() { 205 | view.open(ItemView, models); 206 | expect(view.$el.children()).to.have.length(2); 207 | expect(view.$('[data-name="luke"]')).to.have.length(1); 208 | expect(view.$('[data-name="leia"]')).to.have.length(1); 209 | expect(view.numSubviews()).to.equal(2); 210 | }); 211 | 212 | it('open: opening a new list should remove/replace old content', function() { 213 | var remove = sinon.spy(Backbone.View.prototype, 'remove'); 214 | 215 | view.open(luke); 216 | expect(view.$el.children()).to.have.length(1); 217 | expect(view.numSubviews()).to.equal(1); 218 | 219 | view.open(ItemView, models); 220 | expect(view.$el.children()).to.have.length(2); 221 | expect(view.numSubviews()).to.equal(2); 222 | 223 | expect(remove.callCount).to.equal(1); 224 | remove.restore(); 225 | }); 226 | 227 | it('open: opening new content should remove/replace an old list', function() { 228 | var remove = sinon.spy(Backbone.View.prototype, 'remove'); 229 | 230 | view.open(ItemView, models); 231 | expect(view.$el.children()).to.have.length(2); 232 | expect(view.numSubviews()).to.equal(2); 233 | 234 | view.open(luke); 235 | expect(view.$el.children()).to.have.length(1); 236 | expect(view.numSubviews()).to.equal(1); 237 | 238 | expect(remove.callCount).to.equal(2); 239 | remove.restore(); 240 | }); 241 | 242 | it('open: should store opened content references on the view', function() { 243 | view.open(ItemView, models); 244 | expect(view.contentView).to.equal(ItemView); 245 | expect(view.contentModels).to.equal(models); 246 | }); 247 | 248 | it('close: should empty the container', function() { 249 | view.open(ItemView, models); 250 | expect(view.$el.children()).to.have.length(2); 251 | expect(view.numSubviews()).to.equal(2); 252 | 253 | view.close(); 254 | expect(view.$el.children()).to.have.length(0); 255 | expect(view.numSubviews()).to.equal(0); 256 | }); 257 | 258 | it('close: should nullify content references', function() { 259 | view.open(ItemView, models); 260 | expect(view.contentView).to.equal(ItemView); 261 | expect(view.contentModels).to.equal(models); 262 | 263 | view.close(); 264 | expect(view.contentView).to.be.null; 265 | expect(view.contentModels).to.be.null; 266 | }); 267 | 268 | it('renderContent: should empty the container when there is no opened content', function() { 269 | expect(view.$el.children()).to.have.length(1); 270 | view.renderContent(); 271 | expect(view.$el.children()).to.have.length(0); 272 | }); 273 | 274 | it('renderContent: should render and display a single view instance as content', function() { 275 | var render = sinon.spy(luke, 'render'); 276 | view.contentView = luke; 277 | view.renderContent(); 278 | 279 | expect(view.$el.children()).to.have.length(1); 280 | expect(view.$el.children()[0]).to.equal(luke.$el[0]); 281 | expect(render.calledOnce).to.be.true; 282 | }); 283 | 284 | it('renderContent: should empty the container when a view constructor is provided without models', function() { 285 | view.contentView = ItemView; 286 | view.renderContent(); 287 | expect(view.$el.children()).to.have.length(0); 288 | expect(view.numSubviews()).to.equal(0); 289 | }); 290 | 291 | it('renderContent: should render a view constructor with a list of models into the container', function() { 292 | view.contentView = ItemView; 293 | view.contentModels = models; 294 | view.renderContent(); 295 | expect(view.$el.children()).to.have.length(2); 296 | expect(view.numSubviews()).to.equal(2); 297 | }); 298 | 299 | it('contentFilter: should provide an automatic-pass by default', function() { 300 | expect(view.contentFilter()).to.be.true; 301 | }); 302 | 303 | it('contentFilter: should filter the list of rendered models', function() { 304 | view.contentFilter = function(model) { 305 | return model.get('name') === 'luke'; 306 | }; 307 | 308 | view.open(ItemView, models); 309 | expect(view.$el.children().eq(0).is('[data-name="luke"]')).to.be.true; 310 | expect(view.$el.children()).to.have.length(1); 311 | expect(view.numSubviews()).to.equal(1); 312 | }); 313 | 314 | it('contentSort: should be unimplemented by default', function() { 315 | expect(view.contentSort).to.be.null; 316 | }); 317 | 318 | it('contentSort: should sort the render order of the provided content models', function() { 319 | // Expect original order to be "luke", "leia": 320 | expect(models[0].get('name')).to.equal('luke'); 321 | expect(models[1].get('name')).to.equal('leia'); 322 | 323 | // Sort alphabetically: 324 | view.contentSort = function(model1, model2) { 325 | return model1.get('name').localeCompare(model2.get('name')); 326 | }; 327 | 328 | view.open(ItemView, models); 329 | 330 | // Expect render order to be "leia", "luke": 331 | expect(view.$el.children().eq(0).is('[data-name="leia"]')).to.be.true; 332 | expect(view.$el.children().eq(1).is('[data-name="luke"]')).to.be.true; 333 | }); 334 | 335 | it('remove: should call superclass "remove" and then "empty" (in that order for best performance)', function() { 336 | var superCall = sinon.spy(Backbone.View.prototype, 'remove'); 337 | //var emptyCall = sinon.spy(view, 'empty'); 338 | 339 | view.remove(); 340 | expect(superCall.calledOnce).to.be.true; 341 | //expect(emptyCall.calledOnce).to.be.true; 342 | //expect(superCall.calledBefore(emptyCall)).to.be.true; 343 | superCall.restore(); 344 | }); 345 | 346 | it('remove: should call "remove" on all managed subviews', function() { 347 | var remove = sinon.spy(Backbone.View.prototype, 'remove'); 348 | 349 | view.addSubview(luke, leia); 350 | view.remove(); 351 | expect(remove.callCount).to.equal(3); 352 | remove.restore(); 353 | }); 354 | }); 355 | --------------------------------------------------------------------------------