├── .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 | 
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 |
--------------------------------------------------------------------------------