This page uses the AMD build in the dist directory, not the files in src. In other words, run the grunt command first to build a current version.
45 |
46 |
47 |
48 |
Performance statistics
49 |
50 |
51 |
52 |
Content
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
68 |
69 |
77 |
78 |
79 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Mon Dec 30 2013 16:14:03 GMT+0100 (CET)
3 |
4 | // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 | // + For automated testing with Grunt, some settings in this config file +
6 | // + are overridden in Gruntfile.js. Check both locations. +
7 | // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 |
9 | module.exports = function(config) {
10 | config.set({
11 |
12 | // base path, that will be used to resolve files and exclude
13 | basePath: '',
14 |
15 |
16 | // frameworks to use
17 | //
18 | // Available for chai (installed with karma-chai-plugins):
19 | // sinon-chai, chai-as-promised, chai-jquery. Enable as needed.
20 | //
21 | // NB sinon-chai includes Sinon; chai-jquery does _not_ include jQuery
22 | frameworks: ['mocha', 'chai', 'sinon-chai'],
23 |
24 |
25 | // list of files / patterns to load in the browser
26 | files: [
27 | // Component dependencies
28 |
29 | // Using the latest jQuery by default. Switch to jQuery 1.x or 2.x as needed.
30 | //
31 | // NB Tests run through the interactive web interface use jQuery 1.x. Use `grunt interactive` or `grunt webtest`
32 | // for them.
33 |
34 | 'bower_components/jquery/dist/jquery.js',
35 | // 'bower_components/jquery-legacy-v1/dist/jquery.js',
36 | // 'bower_components/jquery-legacy-v2/dist/jquery.js',
37 |
38 | 'bower_components/underscore/underscore.js',
39 | 'bower_components/backbone/backbone.js',
40 |
41 | // Component under test (main)
42 | 'src/backbone.declarative.views.js',
43 |
44 | // Marionette (must be loaded after Backbone.Declarative.Views)
45 | //
46 | // Using the latest Marionette by default. Switch to Marionette 2.x as needed, or run Karma with the config for
47 | // legacy Marionette.
48 | //
49 | // NB Tests run through the interactive web interface use Marionette 2.x. Use `grunt interactive` or `grunt webtest`
50 | // for them.
51 |
52 | // 'bower_components/marionette-legacy/lib/backbone.marionette.js',
53 | 'bower_components/backbone.radio/build/backbone.radio.js',
54 | 'bower_components/marionette/lib/backbone.marionette.js',
55 |
56 | // Test helpers
57 | 'node_modules/chai-subset/lib/chai-subset.js',
58 | 'spec/chai-helpers/**/*.js',
59 | 'spec/helpers/**/*.js',
60 |
61 | // Tests
62 | 'spec/**/*.+(spec|test|tests).js'
63 | ],
64 |
65 |
66 | // list of files to exclude
67 | exclude: [
68 |
69 | ],
70 |
71 |
72 | // test results reporter to use
73 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage', 'mocha'
74 | reporters: ['progress'],
75 |
76 |
77 | // web server port
78 | port: 9876,
79 |
80 |
81 | // enable / disable colors in the output (reporters and logs)
82 | colors: true,
83 |
84 |
85 | // level of logging
86 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
87 | logLevel: config.LOG_INFO,
88 |
89 |
90 | // enable / disable watching file and executing tests whenever any file changes
91 | autoWatch: false,
92 |
93 |
94 | // Start these browsers, currently available:
95 | // - Chrome
96 | // - ChromeCanary
97 | // - Firefox
98 | // - Opera
99 | // - Safari
100 | // - PhantomJS
101 | // - SlimerJS
102 | // - IE (Windows only)
103 | //
104 | // ATTN Interactive debugging in PhpStorm/WebStorm doesn't work with PhantomJS. Use Firefox or Chrome instead.
105 | browsers: ['PhantomJS'],
106 |
107 |
108 | // If browser does not capture in given timeout [ms], kill it
109 | captureTimeout: 60000,
110 |
111 |
112 | // Continuous Integration mode
113 | // if true, it capture browsers, run tests and exit
114 | singleRun: false
115 | });
116 | };
117 |
--------------------------------------------------------------------------------
/demo/bower-check.js:
--------------------------------------------------------------------------------
1 | // Checks that 'bower install' has been run in the demo dir if demo/bower.json isn't empty.
2 |
3 | // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 | // + +
5 | // + This is a blocking script, triggering _synchronous_ http subrequests. Use locally only. +
6 | // + +
7 | // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 |
9 | ( function ( window ) {
10 | "use strict";
11 |
12 | var BOWER_DEMO_COMPONENTS_DIR = "/demo/bower_demo_components",
13 |
14 | msg = "
" +
15 | "
Bower components for the demo seem to be missing. Install them first:
" +
16 | "
" +
17 | "
Open a command prompt in the demo directory of the project.
" +
18 | "
Run bower install
" +
19 | "
" +
20 | "
If this is a false positive and the packages are in fact in place, " +
21 | "disable the check by removing bower-check.js at the top of the <body>.
" +
22 | "
";
23 |
24 | getJSON( "/demo/bower.json", false, function ( data ) {
25 |
26 | var i, j, depNames,
27 | exists = false,
28 | files = [
29 | 'bower.json', 'package.json',
30 | 'readme.md', 'Readme.md', 'README.md', 'README',
31 | 'license.txt', 'LICENSE.txt', 'LICENSE',
32 | 'Gruntfile.js',
33 | 'composer.json', 'component.json'
34 | ];
35 |
36 | if ( data && data.dependencies ) {
37 |
38 | depNames = Object.keys( data.dependencies );
39 |
40 | // Bower packages don't necessarily have a bower.json. file. The only file guaranteed to be there after
41 | // install is `.bower.json` (note the leading dot), but it is hidden and won't be served over http.
42 | //
43 | // So instead, we are looking for a bunch of files which are very likely to be there. If none of them is,
44 | // for none of the projects, the dependencies are most likely not installed.
45 | for ( i = 0; i < depNames.length; i++ ) {
46 | for ( j = 0; j < files.length; j++ ) {
47 |
48 | get(
49 | BOWER_DEMO_COMPONENTS_DIR + "/" + depNames[i] + "/" + files[j], false,
50 | function ( data ) {
51 | exists = !!data;
52 | }
53 | );
54 |
55 | if ( exists ) return;
56 |
57 | }
58 | }
59 |
60 | window.document.write( msg );
61 | }
62 |
63 | } );
64 |
65 |
66 | // Helper functions in the absence of a library like jQuery (not loaded at this point)
67 | function get ( url, async, cb, cbError ) {
68 | var data,
69 | request = new XMLHttpRequest;
70 |
71 | request.open( 'GET', url, async );
72 |
73 | request.onload = function () {
74 | if ( this.status >= 200 && this.status < 400 ) {
75 | // Success!
76 | data = this.response;
77 | } else {
78 | // We reached our target server, but it returned an error. Most likely, the bower.json file is missing.
79 | // We just return undefined here.
80 | data = undefined;
81 | }
82 | cb( data );
83 | };
84 |
85 | request.onerror = function ( err ) {
86 | // There was a connection error of some sort.
87 | if ( cbError ) cbError( err );
88 | };
89 |
90 | request.send();
91 | }
92 |
93 | function getJSON ( url, async, cb, cbError ) {
94 | get( url, async, function ( data ) {
95 |
96 | if ( data ) data = JSON.parse( data );
97 | cb( data );
98 |
99 | }, cbError );
100 | }
101 |
102 | }( window ));
--------------------------------------------------------------------------------
/demo/amd/rjs/output/parts/marionette-app.js:
--------------------------------------------------------------------------------
1 | // base.js
2 |
3 | define( 'local.base',[
4 |
5 | 'underscore',
6 | 'backbone',
7 | 'usertiming',
8 | 'backbone.declarative.views'
9 |
10 | ], function ( _, Backbone, performance ) {
11 |
12 | var eventBus = _.extend( {}, Backbone.Events ),
13 |
14 | Model = Backbone.Model.extend(),
15 |
16 | Collection = Backbone.Collection.extend(
17 | { model: Model },
18 | {
19 | create: function ( modelCount ) {
20 | var i,
21 | collection = new Collection(),
22 | models = [];
23 |
24 | for ( i = 0; i < modelCount; i++ ) models.push( new collection.model( { number: i + 1 } ) );
25 | collection.reset( models );
26 |
27 | return collection;
28 | }
29 | }
30 | ),
31 |
32 | StatsView = Backbone.View.extend( {
33 |
34 | template: "#stats-template",
35 |
36 | parent: ".stats",
37 |
38 | initialize: function ( options ) {
39 | var compiledTemplate = this.declarativeViews.getCachedTemplate().compiled;
40 | this.template = compiledTemplate || _.template( this.declarativeViews.getCachedTemplate(). html );
41 |
42 | options || ( options = {} );
43 |
44 | if ( options.parent ) this.parent = options.parent;
45 | this.$parent = Backbone.$( this.parent );
46 | this.$el.appendTo( this.$parent );
47 |
48 | this.listenTo( eventBus, "createStats", this.render );
49 | },
50 |
51 | render: function ( stats ) {
52 | stats.totalDuration = Math.round( this.getTotalDuration() );
53 | this.$el.html( this.template( stats ) );
54 | },
55 |
56 | getTotalDuration: function () {
57 | // Query the document height. This is to assess the true total render time, including the painting.
58 | // The calculation of the document height should be blocked by the browser until all item elements have
59 | // been painted.
60 | var docHeight = $( document ).height();
61 |
62 | performance.measure( "paint", "create-itemViews-start" );
63 | return performance.getEntriesByName( "paint" )[0].duration;
64 | }
65 |
66 | } );
67 |
68 | Backbone.DeclarativeViews.custom.compiler = _.template;
69 |
70 | return {
71 | Model: Model,
72 | Collection: Collection,
73 | StatsView: StatsView,
74 | eventBus: eventBus
75 | }
76 |
77 | } );
78 |
79 | // views-marionette.js
80 |
81 | define( 'local.views-marionette',[
82 |
83 | 'underscore',
84 | 'backbone',
85 | 'marionette',
86 | 'usertiming',
87 | 'local.base',
88 | 'backbone.declarative.views'
89 |
90 | ], function ( _, Backbone, Marionette, performance, base ) {
91 |
92 | // Integrate with Marionette
93 | Backbone.DeclarativeViews.joinMarionette();
94 |
95 | var MarionetteBaseView = Marionette.ItemView || Marionette.View, // Base view type, depending on Marionette version
96 |
97 | ItemView = MarionetteBaseView.extend( {
98 |
99 | appendTo: function ( $parent ) {
100 | if ( !( $parent instanceof Backbone.$ ) ) $parent = Backbone.$( $parent );
101 | $parent.append( this.$el );
102 | return this;
103 | }
104 |
105 | } ),
106 |
107 | ListView = Marionette.CollectionView.extend( {
108 |
109 | initialize: function ( options ) {
110 | options || ( options = {} ); // jshint ignore:line
111 |
112 | if ( options.parent ) this.parent = options.parent;
113 | this.$parent = Backbone.$( this.parent );
114 | },
115 |
116 | onBeforeRender: function () {
117 | // Start timer
118 | performance.clearMarks();
119 | performance.clearMeasures();
120 | performance.mark( "create-itemViews-start" );
121 | },
122 |
123 | onRender: function () {
124 | var duration;
125 |
126 | // Measure the time it took to create the itemViews
127 | performance.measure( "create-itemViews", "create-itemViews-start" );
128 | duration = performance.getEntriesByName( "create-itemViews" )[0].duration;
129 |
130 | if ( ! this.$el.parent().length ) this.$el.appendTo( this.$parent );
131 |
132 | base.eventBus.trigger( "createStats", { itemViewCount : this.children.length, duration: Math.round( duration ) } );
133 | }
134 |
135 | } );
136 |
137 | return {
138 | ItemView: ItemView,
139 | ListView: ListView
140 | }
141 |
142 | } );
143 |
144 | // marionette.js
145 |
146 | require( [
147 |
148 | 'local.base',
149 | 'local.views-marionette'
150 |
151 | ], function ( base, marionetteViews ) {
152 |
153 | var count = 1000,
154 |
155 | ItemView = marionetteViews.ItemView.extend( { template: "#item-template" } ),
156 |
157 | listView = new marionetteViews.ListView( {
158 | template: "#list-template",
159 | childView: ItemView,
160 | parent: ".container",
161 | collection: base.Collection.create( count )
162 | } );
163 |
164 | new base.StatsView();
165 | listView.render();
166 |
167 | } );
168 |
169 | define("local.marionette", function(){});
170 |
171 |
--------------------------------------------------------------------------------
/demo/amd/rjs/output/parts/backbone-app.js:
--------------------------------------------------------------------------------
1 | // base.js
2 |
3 | define( 'local.base',[
4 |
5 | 'underscore',
6 | 'backbone',
7 | 'usertiming',
8 | 'backbone.declarative.views'
9 |
10 | ], function ( _, Backbone, performance ) {
11 |
12 | var eventBus = _.extend( {}, Backbone.Events ),
13 |
14 | Model = Backbone.Model.extend(),
15 |
16 | Collection = Backbone.Collection.extend(
17 | { model: Model },
18 | {
19 | create: function ( modelCount ) {
20 | var i,
21 | collection = new Collection(),
22 | models = [];
23 |
24 | for ( i = 0; i < modelCount; i++ ) models.push( new collection.model( { number: i + 1 } ) );
25 | collection.reset( models );
26 |
27 | return collection;
28 | }
29 | }
30 | ),
31 |
32 | StatsView = Backbone.View.extend( {
33 |
34 | template: "#stats-template",
35 |
36 | parent: ".stats",
37 |
38 | initialize: function ( options ) {
39 | var compiledTemplate = this.declarativeViews.getCachedTemplate().compiled;
40 | this.template = compiledTemplate || _.template( this.declarativeViews.getCachedTemplate(). html );
41 |
42 | options || ( options = {} );
43 |
44 | if ( options.parent ) this.parent = options.parent;
45 | this.$parent = Backbone.$( this.parent );
46 | this.$el.appendTo( this.$parent );
47 |
48 | this.listenTo( eventBus, "createStats", this.render );
49 | },
50 |
51 | render: function ( stats ) {
52 | stats.totalDuration = Math.round( this.getTotalDuration() );
53 | this.$el.html( this.template( stats ) );
54 | },
55 |
56 | getTotalDuration: function () {
57 | // Query the document height. This is to assess the true total render time, including the painting.
58 | // The calculation of the document height should be blocked by the browser until all item elements have
59 | // been painted.
60 | var docHeight = $( document ).height();
61 |
62 | performance.measure( "paint", "create-itemViews-start" );
63 | return performance.getEntriesByName( "paint" )[0].duration;
64 | }
65 |
66 | } );
67 |
68 | Backbone.DeclarativeViews.custom.compiler = _.template;
69 |
70 | return {
71 | Model: Model,
72 | Collection: Collection,
73 | StatsView: StatsView,
74 | eventBus: eventBus
75 | }
76 |
77 | } );
78 |
79 | // views-backbone.js
80 |
81 | define( 'local.views-backbone',[
82 |
83 | 'underscore',
84 | 'backbone',
85 | 'usertiming',
86 | 'local.base',
87 | 'backbone.declarative.views'
88 |
89 | ], function ( _, Backbone, performance, base ) {
90 |
91 | var ItemView = Backbone.View.extend( {
92 |
93 | initialize: function ( options ) {
94 | this.template = this.declarativeViews.getCachedTemplate().compiled;
95 | },
96 |
97 | render: function () {
98 | this.$el.html( this.template( this.model.attributes ) );
99 | return this;
100 | },
101 |
102 | appendTo: function ( $parent ) {
103 | if ( !( $parent instanceof Backbone.$ ) ) $parent = Backbone.$( $parent );
104 | $parent.append( this.$el );
105 | return this;
106 | }
107 |
108 | } ),
109 |
110 | ListView = Backbone.View.extend( {
111 |
112 | initialize: function ( options ) {
113 | options || ( options = {} ); // jshint ignore:line
114 |
115 | if ( options.ItemView ) this.ItemView = options.ItemView;
116 | if ( options.parent ) this.parent = options.parent;
117 | this.$parent = Backbone.$( this.parent );
118 | },
119 |
120 | render: function () {
121 | var duration, renderDuration,
122 | els = [];
123 |
124 | this.itemViews = [];
125 |
126 | // Start timer
127 | performance.clearMarks();
128 | performance.clearMeasures();
129 | performance.mark( "create-itemViews-start" );
130 |
131 | this.collection.each( function ( model ) {
132 | //Backbone.DeclarativeViews.clearCache();
133 | var itemView = new this.ItemView( { model: model } );
134 | itemView.render();
135 |
136 | this.itemViews.push( itemView );
137 | els.push( itemView.el );
138 | }, this );
139 |
140 | // Measure itemView creation time
141 | performance.measure( "create-itemViews", "create-itemViews-start" );
142 | duration = performance.getEntriesByName( "create-itemViews" )[0].duration;
143 |
144 | this.$el.append( els );
145 | this.$el.appendTo( this.$parent );
146 |
147 | // Measure render duration time (total from beginning of itemView creation)
148 | performance.measure( "render", "create-itemViews-start" );
149 | renderDuration = performance.getEntriesByName( "render" )[0].duration;
150 |
151 | base.eventBus.trigger( "createStats", {
152 | itemViewCount : this.itemViews.length,
153 | duration: Math.round( duration ),
154 | renderDuration: Math.round( renderDuration )
155 | } );
156 | },
157 |
158 | destroy: function () {
159 | _.each( this.itemViews, function ( itemView ) {
160 | itemView.remove();
161 | } );
162 |
163 | this.remove();
164 | }
165 |
166 | } );
167 |
168 | return {
169 | ItemView: ItemView,
170 | ListView: ListView
171 | }
172 |
173 | } );
174 |
175 | // plain.js
176 |
177 | require( [
178 |
179 | 'local.base',
180 | 'local.views-backbone'
181 |
182 | ], function ( base, backboneViews ) {
183 |
184 | var count = 1000,
185 |
186 | ItemView = backboneViews.ItemView.extend( {
187 | template: "#item-template"
188 | } ),
189 |
190 | listView = new backboneViews.ListView( {
191 | template: "#list-template",
192 | parent: ".container",
193 | ItemView: ItemView,
194 | collection: base.Collection.create( count )
195 | } );
196 |
197 | new base.StatsView();
198 | listView.render();
199 |
200 | } );
201 |
202 | define("local.plain", function(){});
203 |
204 |
--------------------------------------------------------------------------------
/dist/backbone.declarative.views.min.js:
--------------------------------------------------------------------------------
1 | // Backbone.Declarative.Views, v4.1.2
2 | // Copyright (c) 2014-2017 Michael Heim, Zeilenwechsel.de
3 | // Distributed under MIT license
4 | // http://github.com/hashchange/backbone.declarative.views
5 |
6 |
7 | !function(a,b){"use strict";var c="object"==typeof exports&&exports&&!exports.nodeType&&"object"==typeof module&&module&&!module.nodeType;"function"==typeof define&&"object"==typeof define.amd&&define.amd?define(["exports","underscore","backbone"],b):c?b(exports,require("underscore"),require("backbone")):b({},_,Backbone)}(0,function(a,_,Backbone){"use strict";function b(a,b,c){var d;return a&&_.isString(a)&&(d=B[a],d||(d=k(a,b,c)),d.invalid&&(d=void 0),d&&(d=j(d))),d}function c(a,c){var d,e=a.declarativeViews.meta;return e.processed?d=e.inGlobalCache?b(e.originalTemplateProp,a,c):void 0:a.template&&_.isString(a.template)?(e.originalTemplateProp=a.template,d=b(a.template,a,c),e.processed=!0,e.inGlobalCache=!0,d&&M.trigger("cacheEntry:view:process",j(d),e.originalTemplateProp,a,c)):(d=void 0,e.processed=!0,e.inGlobalCache=!1),d&&M.trigger("cacheEntry:view:fetch",d,e.originalTemplateProp,a,c),d}function d(a){B={},!a&&Backbone.Marionette&&Backbone.Marionette.TemplateCache&&Backbone.Marionette.TemplateCache.clear()}function e(a){var b=!1,c=_.toArray(arguments),d=_.last(c);if(c.length&&_.isBoolean(d)&&(b=c.pop()),c.length>1)_.each(c,function(a){e(a,b)});else if(_.isArray(a)||_.isArguments(a))_.each(a,function(a){e(a,b)});else{if(!a)throw new H("Missing argument: string identifying the template. The string should be a template selector or the raw HTML of a template, as provided to the template property of a view when the cache entry was created");if(_.isString(a)&&(m(a),!b&&Backbone.Marionette&&Backbone.Marionette.TemplateCache))try{Backbone.Marionette.TemplateCache.clear(a)}catch(a){}}}function f(a){var b=a.declarativeViews.meta;b.processed?b.inGlobalCache&&m(b.originalTemplateProp):a.template&&_.isString(a.template)&&m(a.template)}function g(a){var b;try{if(b=N(a),!N.contains(document.documentElement,b[0]))throw new Error}catch(c){if(b=h(a),b.html()!==a)throw new Error("Failed to wrap template string in script tag without altering it")}return b}function h(a){var b=N("").attr("type","text/x-template").text(a),c=i(a);return c&&b.attr(c),b}function i(a){var b={},c=z.exec(a),d=c&&c[0];return d&&_.each(G,function(a,c){var e=a.exec(d),f=e&&e[2];f&&(b[c]=f)}),_.size(b)?b:void 0}function j(a){var b=_.clone(a);return _.isObject(b.attributes)&&(b.attributes=_.clone(b.attributes)),b}function k(a,b,c){var d,e,f,h=Backbone.DeclarativeViews.custom.loadTemplate,i=Backbone.DeclarativeViews.defaults.loadTemplate,j=i!==g,k=a;try{d=h?h(a,b,c):i(a,b,c)}catch(a){if(u(a))throw a;d=""}if((h||j)&&""!==d&&!(d instanceof Backbone.$))throw new K("Invalid return value. The "+(h?"custom":"default")+" loadTemplate function must return a jQuery instance, but it hasn't");return d.length?(e=q(d),f=d.html(),B[k]={html:f,compiled:l(f,d),tagName:e.tagName,className:e.className,id:e.id,attributes:e.attributes,_pluginData:{}},M.trigger("cacheEntry:create",B[k],a,b,c)):B[k]={invalid:!0},B[k]}function l(a,b){var c,d=Backbone.DeclarativeViews.custom.compiler;if(d){if(d&&!_.isFunction(d))throw new K("Invalid custom template compiler set in Backbone.DeclarativeViews.custom.compiler: compiler is not a function");try{c=d(a,b)}catch(c){throw new J('An error occurred while compiling the template. The compiler had been passed the HTML string "'+a+(b?'" as the first argument, and the corresponding template node, wrapped in a jQuery object, as the second argument.':'" as the only argument.')+"\nOriginal error thrown by the compiler:\n"+c.message)}}return c}function m(a){B[a]&&delete B[a]}function n(a,b){var c=o(),d="data-"+a,e=b&&b.isJSON?"json":"primitives",f=F[e];if(0===a.indexOf("data-"))throw new K('registerDataAttribute(): Illegal attribute name "'+a+'", must be registered without "data-" prefix');if("html"===a||"compiled"===a)throw new K('registerDataAttribute(): Cannot register attribute name "'+a+'" because it is reserved');if(_.contains(c,a))throw new K('registerDataAttribute(): Cannot register attribute name "'+a+'" because it has already been registered');f.push(a),F[e]=_.uniq(f),G[d]=new RegExp(d+"\\s*=\\s*(['\"])([\\s\\S]+?)\\1"),z=p()}function o(){return F.primitives.concat(F.json)}function p(){return new RegExp(")[\\s\\S])*?data-(?:"+o().join("|")+")\\s*=\\s*(['\"])[\\s\\S]+?\\1[\\s\\S]*?-->")}function q(a){return r(a),a.data()}function r(a){var b={},c=[];N.hasData(a[0])&&(_.each(F.primitives,function(d){var e=a.attr("data-"+d);void 0===e?c.push(d):b[w(d)]=e}),_.each(F.json,function(d){var e=a.attr("data-"+d);if(void 0===e)c.push(d);else try{b[w(d)]=N.parseJSON(e)}catch(a){c.push(d)}}),c.length&&a.removeData(c),_.size(b)&&a.data(b))}function s(a,b){a.getCachedTemplate=Backbone.DeclarativeViews.getCachedTemplate,a.clearCachedTemplate=Backbone.DeclarativeViews.clearCachedTemplate,a.clearCache=Backbone.DeclarativeViews.clearCache,a.custom=Backbone.DeclarativeViews.custom,b&&(C.push(b),C=_.unique(C))}function t(){D=!0}function u(a){return a instanceof H||a instanceof I||a instanceof J||a instanceof K||a instanceof L}function v(){Backbone.Marionette&&Backbone.Marionette.TemplateCache&&!E&&(y=Backbone.Marionette.TemplateCache.clear,Backbone.Marionette.TemplateCache.clear=function(){arguments.length?Backbone.DeclarativeViews.clearCachedTemplate(arguments,!0):Backbone.DeclarativeViews.clearCache(!0),y.apply(this,arguments)},E=!0)}function w(a){return a.replace(/([^-])-([a-z])/g,function(a,b,c){return b+c.toUpperCase()})}function x(a){function b(a){this.message=a,Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack}return b.prototype=new Error,b.prototype.name=a,b.prototype.constructor=b,b}var y,z,A=Backbone.View,B={},C=[],D=!1,E=!1,F={primitives:[],json:[]},G={},H=x("Backbone.DeclarativeViews.Error"),I=x("Backbone.DeclarativeViews.TemplateError"),J=x("Backbone.DeclarativeViews.CompilerError"),K=x("Backbone.DeclarativeViews.CustomizationError"),L=x("Backbone.DeclarativeViews.ConfigurationError"),M=_.clone(Backbone.Events),N=Backbone.$;_.extend(Backbone.View.prototype,{tagName:function(){return(c(this)||{}).tagName||"div"},className:function(){return(c(this)||{}).className||void 0},id:function(){return(c(this)||{}).id||void 0},attributes:function(){return(c(this)||{}).attributes||void 0}}),Backbone.View=Backbone.View.extend({constructor:function(a){a&&void 0!==a.template&&(this.template=a.template),this.declarativeViews={meta:{viewId:_.uniqueId("view-")},getCachedTemplate:_.partial(c,this),clearCachedTemplate:_.partial(f,this)},_.each(C,function(a){this[a]=this.declarativeViews},this),D&&c(this,a),A.apply(this,arguments)}}),Backbone.DeclarativeViews={getCachedTemplate:b,clearCachedTemplate:e,clearCache:d,joinMarionette:v,Error:H,TemplateError:I,CompilerError:J,CustomizationError:K,ConfigurationError:L,plugins:{registerDataAttribute:n,getDataAttributes:q,updateJqueryDataCache:r,registerCacheAlias:s,enforceTemplateLoading:t,tryCompileTemplate:l,events:M},defaults:{loadTemplate:g},custom:{loadTemplate:void 0,compiler:void 0},version:"4.1.2"},n("tag-name"),n("class-name"),n("id"),n("attributes",{isJSON:!0}),a.info="Backbone.Declarative.Views has loaded. Don't use the exported value of the module. Its functionality is available inside the Backbone namespace."});
8 | //# sourceMappingURL=backbone.declarative.views.min.js.map
--------------------------------------------------------------------------------
/spec/helpers/data-provider.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Code lifted from Leche because I didn't get it to work with Karma.
3 | *
4 | * See https://github.com/box/leche#mocha-data-provider for usage instructions.
5 | *
6 | * LIMITATION:
7 | *
8 | * - You can't use data in your dataSets which is created or populated in an outer before() or beforeEach() hook. The
9 | * hooks run _after_ withData() has been invoked.
10 | *
11 | * Tests contained in the withData callback are executed in order, though, so you can use data from outer before() and
12 | * beforeEach() hooks in there.
13 | *
14 | * To clarify the execution order, consider this setup:
15 | *
16 | * var foo=0;
17 | *
18 | * beforeEach( function () { foo = 10 } );
19 | *
20 | * withData( [foo], function ( importedFoo ) {
21 | *
22 | * console.log( foo ); // => prints 0, beforeEach hasn't run yet
23 | * console.log( importedFoo ); // => prints 0, beforeEach hadn't run when the data set for withData
24 | * // was created
25 | *
26 | * describe( "a suite", function () {
27 | *
28 | * console.log( foo ); // => prints 0, beforeEach hasn't run yet
29 | *
30 | * beforeEach( function () {
31 | * console.log( foo ); // => prints 10, beforeEach has run
32 | * } );
33 | *
34 | * it( "is a test", function () {
35 | * console.log( foo ); // => prints 10, beforeEach has run
36 | * } );
37 | *
38 | * } );
39 | *
40 | * } );
41 | *
42 | * tldr; before/beforeEach cannot be used for withData itself, or its data sets. Refer to it only inside tests or
43 | * hooks.
44 | *
45 | * Legal
46 | * -----
47 | *
48 | * Original file available at https://github.com/box/leche
49 | *
50 | * Copyright
51 | * (c) 2014 Box, Inc.
52 | * (c) 2015-2017 Michael Heim, Zeilenwechsel.de
53 | *
54 | * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
55 | * except in compliance with the License. You may obtain a copy of the License at
56 | * http://www.apache.org/licenses/LICENSE-2.0
57 | *
58 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
59 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
60 | * specific language governing permissions and limitations under the License.
61 | */
62 |
63 | /**
64 | * A data provider for use with Mocha. Generates describe() blocks. Use this around a call to it() to run the test over
65 | * a series of data.
66 | *
67 | * @throws {Error} if dataset is missing or an empty array
68 | * @param {Object|Array} dataset the data to test
69 | * @param {Function} testFunction the function to call for each piece of data
70 | */
71 | function describeWithData ( dataset, testFunction ) {
72 | _withData( describe, dataset, testFunction );
73 | }
74 |
75 | /**
76 | * A data provider for use with Mocha. Generates it() blocks. Use this around test code which would normally be placed
77 | * into it(). Each key in the data set becomes the name of a test.
78 | *
79 | * @throws {Error} if dataset is missing or an empty array
80 | * @param {Object|Array} dataset the data to test
81 | * @param {Function} testFunction the function to call for each piece of data
82 | */
83 | function itWithData ( dataset, testFunction ) {
84 | _withData( it, dataset, testFunction );
85 | }
86 |
87 | /**
88 | * A data provider for use with Mocha. Use this to run tests over a series of data.
89 | *
90 | * @throws {Error} if dataset is missing or an empty array
91 | * @param {Function} mochaFunction the Mocha function to use for generating the block, ie `define` or `it`
92 | * @param {Object|Array} dataset the data to test
93 | * @param {Function} testFunction the function to call for each piece of data
94 | * @param {string} [prefix=""] prefix for each dataset name (when turned into the test name)
95 | * @param {string} [suffix=""] prefix for each dataset name (when turned into the test name)
96 | */
97 | function _withData ( mochaFunction, dataset, testFunction, prefix, suffix ) {
98 |
99 | prefix || ( prefix = "" );
100 | suffix || ( suffix = "" );
101 |
102 | // check for missing or null argument
103 | if ( typeof dataset !== 'object' || dataset === null ) {
104 | throw new Error( 'First argument must be an object or non-empty array.' );
105 | }
106 |
107 | /*
108 | * The dataset needs to be normalized so it looks like:
109 | * {
110 | * "name1": [ "data1", "data2" ],
111 | * "name2": [ "data3", "data4" ],
112 | * }
113 | */
114 | var namedDataset = dataset;
115 | if ( dataset instanceof Array ) {
116 |
117 | // arrays must have at least one item
118 | if ( dataset.length ) {
119 | namedDataset = createNamedDataset( dataset );
120 | } else {
121 | throw new Error( 'First argument must be an object or non-empty array.' );
122 | }
123 | }
124 |
125 | /*
126 | * For each name, create a new describe() or it() block containing the name.
127 | * This causes the dataset info to be output into the console, making
128 | * it easier to determine which dataset caused a problem when there's an
129 | * error.
130 | */
131 | for ( var name in namedDataset ) {
132 | if ( namedDataset.hasOwnProperty( name ) ) {
133 | //jshint loopfunc:true
134 |
135 | mochaFunction( prefix + name + suffix, (function ( name ) {
136 | return function () {
137 |
138 | var args = namedDataset[name];
139 |
140 | if ( !(args instanceof Array) ) {
141 | args = [args];
142 | }
143 |
144 | testFunction.apply( this, args );
145 | };
146 | }( name )) );
147 | }
148 | }
149 | }
150 |
151 | /**
152 | * Converts an array into an object whose keys are a string representation of the each array item and whose values are
153 | * each array item. This is to normalize the information into an object so that other operations can assume objects are
154 | * always used. For an array like this:
155 | *
156 | * [ "foo", "bar" ]
157 | *
158 | * It creates an object like this:
159 | *
160 | * { "foo": "foo", "bar": "bar" }
161 | *
162 | * If there are duplicate values in the array, only the last value is represented in the resulting object.
163 | *
164 | * @param {Array} array the array to convert
165 | * @returns {Object} an object representing the array
166 | * @private
167 | */
168 | function createNamedDataset ( array ) {
169 | var result = {};
170 |
171 | for ( var i = 0, len = array.length; i < len; i++ ) {
172 | result[array[i].toString()] = array[i];
173 | }
174 |
175 | return result;
176 | }
177 |
178 |
179 |
--------------------------------------------------------------------------------
/spec/custom.compiler.spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it */
2 | (function () {
3 | "use strict";
4 |
5 | describe( 'Custom template compiler', function () {
6 |
7 | var dataAttributes, innerTemplateHtml, outerTemplateHtml, $templateNode, View,
8 | cleanup = function () {
9 | Backbone.DeclarativeViews.clearCache();
10 | Backbone.Marionette.TemplateCache.clear();
11 | };
12 |
13 | beforeEach( function () {
14 |
15 | cleanup();
16 |
17 | dataAttributes = {
18 | "data-tag-name": "ul",
19 | "data-class-name": "listClass",
20 | "data-id": "listId",
21 | "data-attributes": '{ "lang": "en", "title": "title from data attributes" }'
22 | };
23 |
24 | var baseTemplateHtml = '';
25 | innerTemplateHtml = '
Inner list item
';
26 |
27 | $templateNode = $( baseTemplateHtml )
28 | .html( innerTemplateHtml )
29 | .attr( dataAttributes )
30 | .appendTo( "body" );
31 |
32 | outerTemplateHtml = $templateNode.prop( "outerHTML" ) ;
33 |
34 | Backbone.DeclarativeViews.custom.compiler = function ( html, $template ) {
35 | return _.template( html );
36 | };
37 |
38 | View = Backbone.View.extend();
39 | } );
40 |
41 | afterEach( function () {
42 | cleanup();
43 | $templateNode.remove();
44 | Backbone.DeclarativeViews.custom.compiler = undefined;
45 | } );
46 |
47 | describe( 'The custom compiler method receives', function () {
48 |
49 | var compilerSpy;
50 |
51 | beforeEach( function () {
52 | compilerSpy = sinon.spy( Backbone.DeclarativeViews.custom, "compiler" );
53 | new View( { template: "#template" } );
54 | } );
55 |
56 | afterEach( function () {
57 | Backbone.DeclarativeViews.custom.compiler.restore();
58 | } );
59 |
60 | it( 'exactly two arguments', function () {
61 | expect( Backbone.DeclarativeViews.custom.compiler.getCall( 0 ).args ).to.be.of.length( 2 );
62 | } );
63 |
64 | it( 'the inner HTML of the template as the first argument', function () {
65 | expect( Backbone.DeclarativeViews.custom.compiler ).to.have.been.calledWith( innerTemplateHtml );
66 | } );
67 |
68 | it( 'the $template node, wrapped in a jQuery object, as the second argument', function () {
69 | var firstArg = Backbone.DeclarativeViews.custom.compiler.getCall( 0 ).args[1];
70 | expect( firstArg ).to.be.instanceOf( jQuery );
71 | expect( firstArg ).to.be.of.length( 1 );
72 | expect( firstArg.prop( "outerHTML" ) ).to.equal( outerTemplateHtml );
73 | } );
74 |
75 | } );
76 |
77 | describe( 'The return value of the custom compiler', function () {
78 |
79 | it( 'is allowed to be anything without throwing an error (checking with string return value)', function () {
80 | // It _should_ be a function, but it is not enforced.
81 | Backbone.DeclarativeViews.custom.compiler = function () { return "A random string!" };
82 | expect( function () { new View( { template: "#template" } ); } ).not.to.throw();
83 | } );
84 |
85 | it( 'is allowed to be anything without throwing an error (checking with undefined return value)', function () {
86 | // It _should_ be a function, but it is not enforced.
87 | Backbone.DeclarativeViews.custom.compiler = function () { return undefined; };
88 | expect( function () { new View( { template: "#template" } ); } ).not.to.throw();
89 | } );
90 |
91 | it( 'appears as the "compiled" property in the cache entry', function () {
92 | var view = new View( { template: "#template" } ),
93 | cached = view.declarativeViews.getCachedTemplate();
94 |
95 | expect( cached ).to.haveOwnProperty( "compiled" );
96 | expect( cached.compiled ).to.be.a( "function" );
97 | expect( cached.compiled() ).to.equal( ( _.template( innerTemplateHtml ) )() );
98 | } );
99 |
100 | } );
101 |
102 | describe( 'The presence of a custom compiler does not affect', function () {
103 |
104 | var view;
105 |
106 | beforeEach( function () {
107 | view = new View( { template: "#template" } );
108 | } );
109 |
110 | it( 'the other cached values', function () {
111 | expect( view.declarativeViews.getCachedTemplate() ).to.returnCacheValueFor( dataAttributes, outerTemplateHtml, _.template( innerTemplateHtml ) );
112 | } );
113 |
114 | it( 'the el of the view', function () {
115 | expect( view ).to.have.exactElProperties( dataAttributesToProperties( dataAttributes ) );
116 | } );
117 |
118 | } );
119 |
120 | describe( 'If defined, the compiler must be a function. An error is thrown', function () {
121 |
122 | it( 'if it is an object', function () {
123 | Backbone.DeclarativeViews.custom.compiler = {};
124 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CustomizationError, "Invalid custom template compiler set in Backbone.DeclarativeViews.custom.compiler: compiler is not a function" );
125 | } );
126 |
127 | it( 'if it is a string', function () {
128 | Backbone.DeclarativeViews.custom.compiler = "a string";
129 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CustomizationError, "Invalid custom template compiler set in Backbone.DeclarativeViews.custom.compiler: compiler is not a function" );
130 | } );
131 |
132 | it( 'if it is an array', function () {
133 | Backbone.DeclarativeViews.custom.compiler = [];
134 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CustomizationError, "Invalid custom template compiler set in Backbone.DeclarativeViews.custom.compiler: compiler is not a function" );
135 | } );
136 |
137 | } );
138 |
139 | describe( 'If the compiler throws an error when called', function () {
140 |
141 | it( 'Backbone.Declarative.Views throws a (friendly) error', function () {
142 | Backbone.DeclarativeViews.custom.compiler = function () { throw new Error( "compiler error message" ); };
143 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CompilerError, "An error occurred while compiling the template" );
144 | } );
145 |
146 | } );
147 |
148 | describe( 'If no compiler is defined', function () {
149 |
150 | it( 'the "compiled" property in the cache entry is set to undefined', function () {
151 | Backbone.DeclarativeViews.custom.compiler = undefined;
152 | var view = new View( { template: "#template" } ),
153 | cached = view.declarativeViews.getCachedTemplate();
154 |
155 | expect( cached.compiled ).to.be.undefined;
156 | } );
157 |
158 | } );
159 |
160 | } );
161 |
162 | })();
--------------------------------------------------------------------------------
/spec/helpers/fixture-utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates a raw HTML string for a template. A comment containing the el configuration can be inserted into it.
3 | *
4 | * By default, the comment is inserted at the beginning of the HTML string.
5 | *
6 | * @param {Function} createTemplateFn the function returning the template HTML, except for the el comment. It must
7 | * be possible to pass the el comment as an insertion. Expected signature:
8 | * createTemplateFn( templateLanguage, insertion )
9 | * @param {string} templateLanguage currently supported: "Handlebars", "EJS" (EJS), "ES6". It is up to the template
10 | * creation function what to make of it (e.g., can be ignored if there are no
11 | * template vars)
12 | * @param {Object} elCommentConfig
13 | * @param {Function} [elCommentConfig.createContent] a function generating the content of the comment which defines
14 | * the el, except for the outer comment tags (not wrapped in
15 | * ""). Called with the data attributes (as a hash).
16 | * Expected signature: createContent( dataAttributes )
17 | * @param {boolean} [elCommentConfig.noComment=false] flag: comment should be omitted
18 | * @param {boolean} [elCommentConfig.trailing=false] flag: comment should be placed at the end of the HTML
19 | * @param {boolean} [elCommentConfig.among=false] flag: comment should be placed inside the HTML
20 | * @param {Object} [dataAttributes] hash containing the data attributes to be set in the comment
21 | *
22 | * @returns {string}
23 | */
24 | function createRawHtml( createTemplateFn, templateLanguage, elCommentConfig, dataAttributes ) {
25 | // Construct the HTML string
26 | var comment = elCommentConfig.noComment ? "" : "",
27 |
28 | insertion = elCommentConfig.among ? comment : "",
29 | isLeading = ! elCommentConfig.trailing && !elCommentConfig.among,
30 | isTrailing = elCommentConfig.trailing,
31 |
32 | baseTemplate = createTemplateFn( templateLanguage, insertion );
33 |
34 | return isLeading ? comment + baseTemplate : isTrailing ? baseTemplate + comment : baseTemplate;
35 | }
36 |
37 | /**
38 | * Creates the content of a complex template. It should contain as many pitfalls for correct processing as possible.
39 | *
40 | * Features:
41 | *
42 | * - has a top-level comment (single line)
43 | * - has a top-level comment (multi-line)
44 | * - has a comment containing a tag
45 | * - has numerous top-level tags
46 | * - has text at top level, not enclosed in another tag
47 | * - has tags nested in invalid ways, creating invalid HTML (
inside
)
48 | * - has significant whitespace which must be preserved
49 | * - has template vars inside a tag itself (defining attributes)
50 | * - has a template var define the tag type
51 | * - has templating instructions (if/else)
52 | * - has a script tag inside the template, containing Javascript
53 | * - has a reference to a partial template
54 | *
55 | * NB For ES6 templates, only the start and end delimiters used for a variable are supported, other constructs (if, loop,
56 | * partial) are omitted here.
57 | *
58 | * @param {string} templateLanguage values are "Handlebars", "EJS" (EJS), "ES6"
59 | * @param {object} [options]
60 | * @param {string} [options.indentation=""] a string of whitespace, e.g. " " or "" (no indentation)
61 | * @param {string} [options.insertion=""] an additional string which is inserted somewhere in the middle of the
62 | * content (if left undefined, a blank line, plus insertion, appears instead)
63 | * @returns {string}
64 | */
65 | function createComplexTemplate ( templateLanguage, options ) {
66 | var t = getTemplateLanguageConstructs( templateLanguage ),
67 |
68 | indent = options && options.indentation || "",
69 | insert = options && options.insertion || "",
70 |
71 | lines = [
72 | '',
73 | '',
78 |
79 | t.if,
80 | '
This is a %%paragraph',
81 | 'Some random %%text&& with different line breaks.
',
82 | t.else,
83 | '
This is a %%header&&
',
84 | t.endIf,
85 | t.if,
86 | '
',
87 | t.endIf,
88 |
89 | insert,
90 |
91 | 'Some top-level %%text&&, not wrapped in a tag.
'
107 | ],
108 |
109 | innerContent = _.map( lines, function ( line ) {
110 | return indent + line;
111 | } ).join( "\n" );
112 |
113 | return innerContent.replace( /%%/g, t.startDelimiter ).replace( /&&/g, t.endDelimiter );
114 | }
115 |
116 | /**
117 | * Returns the language constructs for a given template language, for use in template creation.
118 | *
119 | * NB For ES6 templates, only the start and end delimiters used for a variable are supported, other constructs (if, loop,
120 | * partial) are omitted here. Empty strings are returned for the instead.
121 | *
122 | * @param {string} templateLanguage
123 | * @returns {Object}
124 | */
125 | function getTemplateLanguageConstructs ( templateLanguage ) {
126 | var constructs;
127 |
128 | switch ( templateLanguage.toLowerCase() ) {
129 |
130 | case "handlebars":
131 | constructs = {
132 | startDelimiter: "{{",
133 | endDelimiter: "}}",
134 | if: "{{#if isActive}}",
135 | else: "{{else}}",
136 | endIf: "{{/if}}",
137 | loop: "{{#each looped as |value index|}}",
138 | endLoop: "{{/each}}",
139 | partial: '{{> userMessage tagName="h2" }}'
140 | };
141 |
142 | break;
143 | case "ejs":
144 | constructs = {
145 | startDelimiter: "<%= ",
146 | endDelimiter: " %>",
147 | if: "<% if (isActive) { %>",
148 | else: "<% } else { %>",
149 | endIf: "<% } %>",
150 | loop: "<% looped.forEach(function(item) { %>",
151 | endLoop: "<% }); %>",
152 | partial: "<%- include('user/show', {user: user}); %>"
153 | };
154 | break;
155 | case "es6":
156 | constructs = {
157 | startDelimiter: "${",
158 | endDelimiter: "}",
159 | if: "",
160 | else: "",
161 | endIf: "",
162 | loop: "",
163 | endLoop: "",
164 | partial: ""
165 | };
166 | break;
167 | default:
168 | throw new Error( 'Unsupported template language "' + templateLanguage + '"' );
169 | }
170 |
171 | return constructs;
172 | }
173 |
--------------------------------------------------------------------------------
/spec/helpers/various-utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Transforms a HTML5 data-* attributes hash to a hash of Javascript properties.
3 | *
4 | * - The key names are changed. The "data-" prefix is dropped, and hyphen notation is changed to camel case.
5 | * - Some values are changed. If a data attribute is used to store an object, it has been converted to a JSON string.
6 | * So JSON-string values are converted back into actual objects.
7 | *
8 | * Example:
9 | * {
10 | * "data-my-prop": "some value",
11 | * "data-hash": '{ "foo": "nested foo", "bar": "nested bar" }'
12 | * }
13 | * =>
14 | * {
15 | * myProp: "some value",
16 | * hash: { foo: "nested foo", bar: "nested bar" }
17 | * }
18 | *
19 | * @param {Object} dataAttributesHash
20 | * @returns {Object}
21 | */
22 | function dataAttributesToProperties ( dataAttributesHash ) {
23 | var transformed = {};
24 |
25 | $.each( dataAttributesHash, function ( key, value ) {
26 | // Drop the "data-" prefix, then convert to camelCase
27 | key = toCamelCase( key.replace( /^data-/, "" ) );
28 |
29 | try {
30 | value = $.parseJSON( value );
31 | } catch ( err ) {}
32 |
33 | transformed[key] = value;
34 | } );
35 |
36 | return transformed;
37 | }
38 |
39 | /**
40 | * Transforms a hash of Javascript properties into a HTML5 data-* attributes hash. Is the inverse function of
41 | * `dataAttributesToProperties()`.
42 | *
43 | * @param {Object} attributesHash
44 | * @returns {Object}
45 | */
46 | function propertiesToDataAttributes ( attributesHash ) {
47 | var transformed = {};
48 |
49 | $.each( attributesHash, function ( key, value ) {
50 | // Convert camelCase to dashed notation, then add the "data-" prefix
51 | key = "data-" + toDashed( key );
52 |
53 | if ( $.isPlainObject( value ) ) value = JSON.stringify( value );
54 |
55 | transformed[key] = value;
56 | } );
57 |
58 | return transformed;
59 | }
60 |
61 | /**
62 | * Transforms a hash of HTML attributes - e.g., data attributes - into a string.
63 | *
64 | * The string has a trailing slash (or newline), but not a leading one. Double quotes are used by default around values,
65 | * except around JSON strings which have to be enclosed by single quotes.
66 | *
67 | * The string can be used to create the HTML of a tag with the given attributes. Data attributes containing a
68 | * stringified JSON structure are fully supported (simply by using single quotes around attribute values there).
69 | *
70 | * The following formatting option are available:
71 | *
72 | * - The order of the attributes can be reversed.
73 | * - Attributes can be separated by newlines instead of spaces.
74 | * - Redundant whitespace can be inserted around the "=" of each assignment.
75 | * - Values can be enclosed in single quotes, rather than double quotes, where possible (skipped for values containing a
76 | * double quote character).
77 | *
78 | * @param {Object} attributesHash
79 | * @param {Object} [options]
80 | * @param {boolean} [options.reverse=false]
81 | * @param {boolean} [options.multiline=false]
82 | * @param {string} [options.extraSpace=""]
83 | * @param {boolean} [options.preferSingleQuotes=false]
84 | * @returns {string}
85 | */
86 | function attributesHashToString ( attributesHash, options ) {
87 | var reduce = options && options.reverse ? _.reduceRight : _.reduce,
88 | separator = options && options.multiline ? "\n" : " ",
89 | spacing = options && options.extrSpace || "",
90 | defaultQuote = options && options.preferSingleQuotes ? "'" : '"';
91 |
92 | return reduce( attributesHash, function ( attrString, value, key ) {
93 | var quote = value.indexOf( '"' ) !== -1 ? "'" : value.indexOf( "'" ) !== -1 ? '"' : defaultQuote;
94 | return attrString + key + spacing + "=" + spacing + quote + value + quote + separator;
95 | }, "" );
96 | }
97 |
98 | /**
99 | * Returns a transformed hash in which all camelCased property names have been replaced by dashed property names. The
100 | * input hash remains untouched.
101 | *
102 | * Property values are not modified.
103 | *
104 | * Simple implementation, but good enough for the attribute names we deal with here.
105 | *
106 | * Example: { fooBar: "whatEver" } => { "foo-bar": "whatEver" }
107 | *
108 | * @param {Object} hash
109 | * @returns {Object}
110 | */
111 | function toDashedProperties ( hash ) {
112 | var transformed = {};
113 |
114 | _.each( hash, function ( value, key ) {
115 | transformed[toDashed( key )] = value;
116 | } );
117 |
118 | return transformed;
119 | }
120 |
121 | /**
122 | * Returns a transformed hash in which all dashed property names have been replaced by camelCased property names. The
123 | * input hash remains untouched.
124 | *
125 | * Property values are not modified.
126 | *
127 | * Simple implementation, but good enough for the attribute names we deal with here.
128 | *
129 | * Example: { "foo-bar": "what-ever" } => { fooBar: "what-ever" }
130 | *
131 | * @param {Object} hash
132 | * @returns {Object}
133 | */
134 | function toCamelCasedProperties ( hash ) {
135 | var transformed = {};
136 |
137 | _.each( hash, function ( value, key ) {
138 | transformed[toCamelCase( key )] = value;
139 | } );
140 |
141 | return transformed;
142 | }
143 |
144 | /**
145 | * Converts a camelCased label into a dashed label.
146 | *
147 | * Simple implementation, but good enough for the attribute names we deal with here.
148 | *
149 | * @param {string} label
150 | * @returns {string}
151 | */
152 | function toDashed ( label ) {
153 | return label.replace( /([a-z])([A-Z])/g, function ( $0, $1, $2 ) {
154 | return $1 + "-" + $2.toLowerCase();
155 | } );
156 | }
157 |
158 | /**
159 | * Converts a dashed label into a camelCased label.
160 | *
161 | * Simple implementation, but good enough for the attribute names we deal with here.
162 | *
163 | * @param {string} label
164 | * @returns {string}
165 | */
166 | function toCamelCase ( label ) {
167 | return label.replace( /-([a-z])/gi, function ( $0, $1 ) {
168 | return $1.toUpperCase();
169 | } );
170 | }
171 |
172 | /**
173 | * Returns an array of dashed key names, which are the alternative names for all camel-cased key names in a hash.
174 | *
175 | * Simple key names (not camel-cased) are ignored and don't show up in the array.
176 | *
177 | * E.g., dashedKeyAlternatives( { foo: "whatever", barBaz: "whatever" } ) returns [ "bar-baz" ].
178 | *
179 | * @param {Object} hash
180 | * @returns {string[]}
181 | */
182 | function dashedKeyAlternatives ( hash ) {
183 | var keys = _.keys( toDashedProperties( hash ) );
184 | return _.filter( keys, function ( key ) {
185 | return key.search(/[^-]-[a-z]/) !== -1;
186 | } );
187 | }
188 |
189 | /**
190 | * Combines various hashes with a shallow _.extend(). Doesn't modify the input hashes.
191 | *
192 | * Syntactic sugar only, as a simple way of saying _.extend( {}, hashA, hashB, hashN );
193 | *
194 | * @param {...Object} hashes
195 | * @returns {Object}
196 | */
197 | function combine ( hashA, hashB, hashN ) {
198 | var hashes = _.toArray( arguments );
199 |
200 | return _.extend.apply( undefined, [ {} ].concat( hashes ) );
201 | }
202 |
203 | /**
204 | * Checks the Marionette version and returns true if it is less than 3.
205 | *
206 | * NB Marionette 1 and 2 don't expose the version, so we look for the version string of Marionette 3 (or above) and
207 | * negate the result.
208 | *
209 | * @returns {boolean}
210 | */
211 | function isMarionetteLt3 () {
212 | return !( Backbone.Marionette.VERSION && Backbone.Marionette.VERSION[0] >= 3 );
213 | }
214 |
215 | /**
216 | * Returns the basic Marionette view type: Marionette.ItemView for Marionette 1 and 2, Marionette.View for Marionette 3.
217 | *
218 | * @returns {Backbone.Marionette.ItemView|Backbone.Marionette.View}
219 | */
220 | function getMarionetteView () {
221 | return Backbone.Marionette.ItemView || Backbone.Marionette.View;
222 | }
223 |
--------------------------------------------------------------------------------
/demo/amd/rjs/output/parts/marionette-precompiled-app.js:
--------------------------------------------------------------------------------
1 | // base.js
2 |
3 | define( 'local.base',[
4 |
5 | 'underscore',
6 | 'backbone',
7 | 'usertiming',
8 | 'backbone.declarative.views'
9 |
10 | ], function ( _, Backbone, performance ) {
11 |
12 | var eventBus = _.extend( {}, Backbone.Events ),
13 |
14 | Model = Backbone.Model.extend(),
15 |
16 | Collection = Backbone.Collection.extend(
17 | { model: Model },
18 | {
19 | create: function ( modelCount ) {
20 | var i,
21 | collection = new Collection(),
22 | models = [];
23 |
24 | for ( i = 0; i < modelCount; i++ ) models.push( new collection.model( { number: i + 1 } ) );
25 | collection.reset( models );
26 |
27 | return collection;
28 | }
29 | }
30 | ),
31 |
32 | StatsView = Backbone.View.extend( {
33 |
34 | template: "#stats-template",
35 |
36 | parent: ".stats",
37 |
38 | initialize: function ( options ) {
39 | var compiledTemplate = this.declarativeViews.getCachedTemplate().compiled;
40 | this.template = compiledTemplate || _.template( this.declarativeViews.getCachedTemplate(). html );
41 |
42 | options || ( options = {} );
43 |
44 | if ( options.parent ) this.parent = options.parent;
45 | this.$parent = Backbone.$( this.parent );
46 | this.$el.appendTo( this.$parent );
47 |
48 | this.listenTo( eventBus, "createStats", this.render );
49 | },
50 |
51 | render: function ( stats ) {
52 | stats.totalDuration = Math.round( this.getTotalDuration() );
53 | this.$el.html( this.template( stats ) );
54 | },
55 |
56 | getTotalDuration: function () {
57 | // Query the document height. This is to assess the true total render time, including the painting.
58 | // The calculation of the document height should be blocked by the browser until all item elements have
59 | // been painted.
60 | var docHeight = $( document ).height();
61 |
62 | performance.measure( "paint", "create-itemViews-start" );
63 | return performance.getEntriesByName( "paint" )[0].duration;
64 | }
65 |
66 | } );
67 |
68 | Backbone.DeclarativeViews.custom.compiler = _.template;
69 |
70 | return {
71 | Model: Model,
72 | Collection: Collection,
73 | StatsView: StatsView,
74 | eventBus: eventBus
75 | }
76 |
77 | } );
78 |
79 | // views-marionette.js
80 |
81 | define( 'local.views-marionette',[
82 |
83 | 'underscore',
84 | 'backbone',
85 | 'marionette',
86 | 'usertiming',
87 | 'local.base',
88 | 'backbone.declarative.views'
89 |
90 | ], function ( _, Backbone, Marionette, performance, base ) {
91 |
92 | // Integrate with Marionette
93 | Backbone.DeclarativeViews.joinMarionette();
94 |
95 | var MarionetteBaseView = Marionette.ItemView || Marionette.View, // Base view type, depending on Marionette version
96 |
97 | ItemView = MarionetteBaseView.extend( {
98 |
99 | appendTo: function ( $parent ) {
100 | if ( !( $parent instanceof Backbone.$ ) ) $parent = Backbone.$( $parent );
101 | $parent.append( this.$el );
102 | return this;
103 | }
104 |
105 | } ),
106 |
107 | ListView = Marionette.CollectionView.extend( {
108 |
109 | initialize: function ( options ) {
110 | options || ( options = {} ); // jshint ignore:line
111 |
112 | if ( options.parent ) this.parent = options.parent;
113 | this.$parent = Backbone.$( this.parent );
114 | },
115 |
116 | onBeforeRender: function () {
117 | // Start timer
118 | performance.clearMarks();
119 | performance.clearMeasures();
120 | performance.mark( "create-itemViews-start" );
121 | },
122 |
123 | onRender: function () {
124 | var duration;
125 |
126 | // Measure the time it took to create the itemViews
127 | performance.measure( "create-itemViews", "create-itemViews-start" );
128 | duration = performance.getEntriesByName( "create-itemViews" )[0].duration;
129 |
130 | if ( ! this.$el.parent().length ) this.$el.appendTo( this.$parent );
131 |
132 | base.eventBus.trigger( "createStats", { itemViewCount : this.children.length, duration: Math.round( duration ) } );
133 | }
134 |
135 | } );
136 |
137 | return {
138 | ItemView: ItemView,
139 | ListView: ListView
140 | }
141 |
142 | } );
143 |
144 | define('local.precompiled.templates',['handlebars.runtime'], function(Handlebars) {
145 | Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
146 | templates['item-template'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
147 | var helper;
148 |
149 | return "\n"
150 | + container.escapeExpression(((helper = (helper = helpers.number || (depth0 != null ? depth0.number : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"number","hash":{},"data":data}) : helper)))
151 | + "\n";
152 | },"useData":true});
153 | templates['list-template'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
154 | return "\n";
155 | },"useData":true});
156 | templates['stats-template'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
157 | var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
158 |
159 | return "\nGenerating "
160 | + alias4(((helper = (helper = helpers.itemViewCount || (depth0 != null ? depth0.itemViewCount : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"itemViewCount","hash":{},"data":data}) : helper)))
161 | + " item views took "
162 | + alias4(((helper = (helper = helpers.duration || (depth0 != null ? depth0.duration : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"duration","hash":{},"data":data}) : helper)))
163 | + "ms. In total, it took "
164 | + alias4(((helper = (helper = helpers.renderDuration || (depth0 != null ? depth0.renderDuration : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"renderDuration","hash":{},"data":data}) : helper)))
165 | + "ms until they had all been appended to the DOM. Finally, when painting was over, "
166 | + alias4(((helper = (helper = helpers.totalDuration || (depth0 != null ? depth0.totalDuration : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"totalDuration","hash":{},"data":data}) : helper)))
167 | + "ms had passed.\n";
168 | },"useData":true});
169 | return templates;
170 | });
171 |
172 | // marionette-precompiled.js
173 |
174 | require( [
175 |
176 | 'local.base',
177 | 'local.views-marionette',
178 | 'marionette.handlebars',
179 | 'precompiled.declarative.handlebars.templates',
180 | 'local.precompiled.templates'
181 |
182 | ], function ( base, marionetteViews ) {
183 |
184 | var count = 1000,
185 |
186 | ItemView = marionetteViews.ItemView.extend( {
187 | template: "item-template" // without leading "#": is ID of precompiled template, not selector
188 | } ),
189 |
190 | listView = new marionetteViews.ListView( {
191 | template: "list-template", // without leading "#"
192 | childView: ItemView,
193 | parent: ".container",
194 | collection: base.Collection.create( count )
195 | } );
196 |
197 | new base.StatsView( { template: "stats-template" } ); // without leading "#"
198 | listView.render();
199 |
200 | } );
201 |
202 | define("local.marionette-precompiled", function(){});
203 |
204 |
--------------------------------------------------------------------------------
/dist/backbone.declarative.views.min.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["backbone.declarative.views.js"],"names":["root","factory","supportsExports","exports","nodeType","module","define","amd","require","_","Backbone","this","getTemplateData","templateProp","view","viewOptions","data","isString","templateCache","_createTemplateCache","invalid","undefined","_copyCacheEntry","getViewTemplateData","meta","declarativeViews","processed","inGlobalCache","originalTemplateProp","template","events","trigger","clearCache","fromMarionette","Marionette","TemplateCache","clear","clearCachedTemplate","args","toArray","arguments","lastArg","last","length","isBoolean","pop","each","singleProp","isArray","isArguments","GenericError","_clearCachedTemplate","err","clearViewTemplateCache","loadTemplate","templateProperty","$template","$","contains","document","documentElement","Error","_wrapRawTemplate","html","templateString","$wrapper","attr","text","elDataAttributes","_getEmbeddedElAttributes","elDefinitionMatch","rxElDefinitionComment","exec","elDefinitionComment","rxRegisteredDataAttributes","rxAttributeMatcher","attributeName","match","attributeValue","size","cacheEntry","copy","clone","isObject","attributes","customLoader","DeclarativeViews","custom","defaultLoader","defaults","modifiedDefaultLoader","cacheId","_isDeclarativeViewsErrorType","CustomizationError","_getDataAttributes","compiled","_tryCompileTemplate","tagName","className","id","_pluginData","customCompiler","compiler","isFunction","CompilerError","message","_registerDataAttribute","name","options","existingNames","_getRegisteredDataAttributeNames","fullName","type","isJSON","names","registeredDataAttributes","indexOf","push","uniq","RegExp","_createElDefinitionCommentRx","primitives","concat","json","join","$elem","_updateJQueryDataCache","add","remove","hasData","toCamelCase","parseJSON","removeData","_registerCacheAlias","namespaceObject","instanceCachePropertyName","getCachedTemplate","instanceCacheAliases","unique","_enforceTemplateLoading","enforceTemplateLoading","error","TemplateError","ConfigurationError","joinMarionette","isMarionetteInitialized","originalClearCache","apply","dashed","replace","$0","$1","$2","toUpperCase","createCustomErrorType","CustomError","captureStackTrace","constructor","stack","prototype","originalConstructor","View","Events","extend","viewId","uniqueId","partial","alias","plugins","registerDataAttribute","getDataAttributes","updateJqueryDataCache","registerCacheAlias","tryCompileTemplate","version","info"],"mappings":"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","file":"backbone.declarative.views.min.js"}
--------------------------------------------------------------------------------
/demo/amd/rjs/output/parts/backbone-precompiled-app.js:
--------------------------------------------------------------------------------
1 | // base.js
2 |
3 | define( 'local.base',[
4 |
5 | 'underscore',
6 | 'backbone',
7 | 'usertiming',
8 | 'backbone.declarative.views'
9 |
10 | ], function ( _, Backbone, performance ) {
11 |
12 | var eventBus = _.extend( {}, Backbone.Events ),
13 |
14 | Model = Backbone.Model.extend(),
15 |
16 | Collection = Backbone.Collection.extend(
17 | { model: Model },
18 | {
19 | create: function ( modelCount ) {
20 | var i,
21 | collection = new Collection(),
22 | models = [];
23 |
24 | for ( i = 0; i < modelCount; i++ ) models.push( new collection.model( { number: i + 1 } ) );
25 | collection.reset( models );
26 |
27 | return collection;
28 | }
29 | }
30 | ),
31 |
32 | StatsView = Backbone.View.extend( {
33 |
34 | template: "#stats-template",
35 |
36 | parent: ".stats",
37 |
38 | initialize: function ( options ) {
39 | var compiledTemplate = this.declarativeViews.getCachedTemplate().compiled;
40 | this.template = compiledTemplate || _.template( this.declarativeViews.getCachedTemplate(). html );
41 |
42 | options || ( options = {} );
43 |
44 | if ( options.parent ) this.parent = options.parent;
45 | this.$parent = Backbone.$( this.parent );
46 | this.$el.appendTo( this.$parent );
47 |
48 | this.listenTo( eventBus, "createStats", this.render );
49 | },
50 |
51 | render: function ( stats ) {
52 | stats.totalDuration = Math.round( this.getTotalDuration() );
53 | this.$el.html( this.template( stats ) );
54 | },
55 |
56 | getTotalDuration: function () {
57 | // Query the document height. This is to assess the true total render time, including the painting.
58 | // The calculation of the document height should be blocked by the browser until all item elements have
59 | // been painted.
60 | var docHeight = $( document ).height();
61 |
62 | performance.measure( "paint", "create-itemViews-start" );
63 | return performance.getEntriesByName( "paint" )[0].duration;
64 | }
65 |
66 | } );
67 |
68 | Backbone.DeclarativeViews.custom.compiler = _.template;
69 |
70 | return {
71 | Model: Model,
72 | Collection: Collection,
73 | StatsView: StatsView,
74 | eventBus: eventBus
75 | }
76 |
77 | } );
78 |
79 | // views-backbone.js
80 |
81 | define( 'local.views-backbone',[
82 |
83 | 'underscore',
84 | 'backbone',
85 | 'usertiming',
86 | 'local.base',
87 | 'backbone.declarative.views'
88 |
89 | ], function ( _, Backbone, performance, base ) {
90 |
91 | var ItemView = Backbone.View.extend( {
92 |
93 | initialize: function ( options ) {
94 | this.template = this.declarativeViews.getCachedTemplate().compiled;
95 | },
96 |
97 | render: function () {
98 | this.$el.html( this.template( this.model.attributes ) );
99 | return this;
100 | },
101 |
102 | appendTo: function ( $parent ) {
103 | if ( !( $parent instanceof Backbone.$ ) ) $parent = Backbone.$( $parent );
104 | $parent.append( this.$el );
105 | return this;
106 | }
107 |
108 | } ),
109 |
110 | ListView = Backbone.View.extend( {
111 |
112 | initialize: function ( options ) {
113 | options || ( options = {} ); // jshint ignore:line
114 |
115 | if ( options.ItemView ) this.ItemView = options.ItemView;
116 | if ( options.parent ) this.parent = options.parent;
117 | this.$parent = Backbone.$( this.parent );
118 | },
119 |
120 | render: function () {
121 | var duration, renderDuration,
122 | els = [];
123 |
124 | this.itemViews = [];
125 |
126 | // Start timer
127 | performance.clearMarks();
128 | performance.clearMeasures();
129 | performance.mark( "create-itemViews-start" );
130 |
131 | this.collection.each( function ( model ) {
132 | //Backbone.DeclarativeViews.clearCache();
133 | var itemView = new this.ItemView( { model: model } );
134 | itemView.render();
135 |
136 | this.itemViews.push( itemView );
137 | els.push( itemView.el );
138 | }, this );
139 |
140 | // Measure itemView creation time
141 | performance.measure( "create-itemViews", "create-itemViews-start" );
142 | duration = performance.getEntriesByName( "create-itemViews" )[0].duration;
143 |
144 | this.$el.append( els );
145 | this.$el.appendTo( this.$parent );
146 |
147 | // Measure render duration time (total from beginning of itemView creation)
148 | performance.measure( "render", "create-itemViews-start" );
149 | renderDuration = performance.getEntriesByName( "render" )[0].duration;
150 |
151 | base.eventBus.trigger( "createStats", {
152 | itemViewCount : this.itemViews.length,
153 | duration: Math.round( duration ),
154 | renderDuration: Math.round( renderDuration )
155 | } );
156 | },
157 |
158 | destroy: function () {
159 | _.each( this.itemViews, function ( itemView ) {
160 | itemView.remove();
161 | } );
162 |
163 | this.remove();
164 | }
165 |
166 | } );
167 |
168 | return {
169 | ItemView: ItemView,
170 | ListView: ListView
171 | }
172 |
173 | } );
174 |
175 | define('local.precompiled.templates',['handlebars.runtime'], function(Handlebars) {
176 | Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
177 | templates['item-template'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
178 | var helper;
179 |
180 | return "\n"
181 | + container.escapeExpression(((helper = (helper = helpers.number || (depth0 != null ? depth0.number : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"number","hash":{},"data":data}) : helper)))
182 | + "\n";
183 | },"useData":true});
184 | templates['list-template'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
185 | return "\n";
186 | },"useData":true});
187 | templates['stats-template'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
188 | var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
189 |
190 | return "\nGenerating "
191 | + alias4(((helper = (helper = helpers.itemViewCount || (depth0 != null ? depth0.itemViewCount : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"itemViewCount","hash":{},"data":data}) : helper)))
192 | + " item views took "
193 | + alias4(((helper = (helper = helpers.duration || (depth0 != null ? depth0.duration : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"duration","hash":{},"data":data}) : helper)))
194 | + "ms. In total, it took "
195 | + alias4(((helper = (helper = helpers.renderDuration || (depth0 != null ? depth0.renderDuration : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"renderDuration","hash":{},"data":data}) : helper)))
196 | + "ms until they had all been appended to the DOM. Finally, when painting was over, "
197 | + alias4(((helper = (helper = helpers.totalDuration || (depth0 != null ? depth0.totalDuration : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"totalDuration","hash":{},"data":data}) : helper)))
198 | + "ms had passed.\n";
199 | },"useData":true});
200 | return templates;
201 | });
202 |
203 | // plain-precompiled.js
204 |
205 | require( [
206 |
207 | 'local.base',
208 | 'local.views-backbone',
209 | 'precompiled.declarative.handlebars.templates',
210 | 'local.precompiled.templates'
211 |
212 | ], function ( base, backboneViews ) {
213 |
214 | var count = 1000,
215 |
216 | ItemView = backboneViews.ItemView.extend( {
217 | template: "item-template" // without leading "#": is ID of precompiled template, not selector
218 | } ),
219 |
220 | listView = new backboneViews.ListView( {
221 | template: "list-template", // without leading "#"
222 | parent: ".container",
223 | ItemView: ItemView,
224 | collection: base.Collection.create( count )
225 | } );
226 |
227 | new base.StatsView( { template: "stats-template" } ); // without leading "#"
228 | listView.render();
229 |
230 | } );
231 |
232 | define("local.plain-precompiled", function(){});
233 |
234 |
--------------------------------------------------------------------------------
/spec/custom.loader.spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it */
2 | (function () {
3 | "use strict";
4 |
5 | describe( 'Custom loader', function () {
6 |
7 | var dataAttributes, outerTemplateHtml, innerTemplateHtml, View,
8 | cleanup = function () {
9 | Backbone.DeclarativeViews.clearCache();
10 | Backbone.Marionette.TemplateCache.clear();
11 | };
12 |
13 | beforeEach( function () {
14 |
15 | cleanup();
16 |
17 | dataAttributes = {
18 | "data-tag-name": "ul",
19 | "data-class-name": "listClass",
20 | "data-id": "listId",
21 | "data-attributes": '{ "lang": "en", "title": "title from data attributes" }'
22 | };
23 |
24 | // Construct the HTML strings
25 | // NB Normalize the template HTML by piping it through jQuery
26 | innerTemplateHtml = '
Inner list item
';
27 | outerTemplateHtml = $( '' )
28 | .html( innerTemplateHtml )
29 | .attr( dataAttributes )
30 | .prop( 'outerHTML' );
31 |
32 | Backbone.DeclarativeViews.custom.loadTemplate = function () {
33 | return $( outerTemplateHtml );
34 | };
35 |
36 | View = Backbone.View.extend();
37 | } );
38 |
39 | afterEach( function () {
40 | cleanup();
41 | Backbone.DeclarativeViews.custom.loadTemplate = undefined;
42 | } );
43 |
44 | describe( 'Arguments provided to the custom loadTemplate method', function () {
45 |
46 | var view;
47 |
48 | beforeEach( function () {
49 | sinon.spy( Backbone.DeclarativeViews.custom, "loadTemplate" );
50 | } );
51 |
52 | afterEach( function () {
53 | Backbone.DeclarativeViews.custom.loadTemplate.restore();
54 | } );
55 |
56 | describe( 'When the custom loader is called in the context of a view', function () {
57 |
58 | describe( 'with the template being set as a property on the view class', function () {
59 |
60 | beforeEach( function () {
61 | View = Backbone.View.extend( { template: "#template" } );
62 | view = new View();
63 | } );
64 |
65 | it( 'it receives the template property of the view as first argument', function () {
66 | expect( Backbone.DeclarativeViews.custom.loadTemplate ).to.have.been.calledWith( "#template" );
67 | } );
68 |
69 | it( 'it receives the view as second argument', function () {
70 | expect( Backbone.DeclarativeViews.custom.loadTemplate ).to.have.been.calledWith( "#template", view );
71 | } );
72 |
73 | } );
74 |
75 | describe( 'with the template being passed in as an option during view instantiation', function () {
76 |
77 | beforeEach( function () {
78 | view = new View( { template: "#template" } );
79 | } );
80 |
81 | it( 'it receives the template option passed to the view as first argument', function () {
82 | expect( Backbone.DeclarativeViews.custom.loadTemplate ).to.have.been.calledWith( "#template" );
83 | } );
84 |
85 | it( 'it receives the view as second argument', function () {
86 | expect( Backbone.DeclarativeViews.custom.loadTemplate ).to.have.been.calledWith( "#template", view );
87 | } );
88 |
89 | } );
90 |
91 | describe( 'with a template being set as a property on the view class, and a different template being passed in as an option', function () {
92 |
93 | beforeEach( function () {
94 | View = Backbone.View.extend( { template: "#template" } );
95 | view = new View( { template: "#otherTemplate" } );
96 | } );
97 |
98 | it( 'it receives the template option passed to the view as first argument', function () {
99 | expect( Backbone.DeclarativeViews.custom.loadTemplate ).to.have.been.calledWith( "#otherTemplate" );
100 | } );
101 |
102 | it( 'it receives the view as second argument', function () {
103 | expect( Backbone.DeclarativeViews.custom.loadTemplate ).to.have.been.calledWith( "#otherTemplate", view );
104 | } );
105 |
106 | } );
107 |
108 | } );
109 |
110 | describe( 'When the custom loader is called from the global API', function () {
111 |
112 | describe( 'with the template selector provided as the only argument', function () {
113 |
114 | beforeEach( function () {
115 | Backbone.DeclarativeViews.getCachedTemplate( "#template" );
116 | } );
117 |
118 | it( 'the loader receives the template selector as first argument', function () {
119 | expect( Backbone.DeclarativeViews.custom.loadTemplate ).to.have.been.calledWith( "#template" );
120 | } );
121 |
122 | it( 'the second argument provided to the loader is undefined', function () {
123 | expect( Backbone.DeclarativeViews.custom.loadTemplate ).to.have.been.calledWith( "#template", undefined );
124 | } );
125 |
126 | } );
127 |
128 | describe( 'with the template selector and a view provided as arguments', function () {
129 |
130 | beforeEach( function () {
131 | view = new View();
132 | Backbone.DeclarativeViews.getCachedTemplate( "#template", view );
133 | } );
134 |
135 | it( 'the loader receives the template selector as first argument', function () {
136 | expect( Backbone.DeclarativeViews.custom.loadTemplate ).to.have.been.calledWith( "#template" );
137 | } );
138 |
139 | it( 'the loader receives the view as second argument', function () {
140 | expect( Backbone.DeclarativeViews.custom.loadTemplate ).to.have.been.calledWith( "#template", view );
141 | } );
142 |
143 | } );
144 |
145 | } );
146 |
147 | } );
148 |
149 | describe( 'When the jQuery object of the template is provided by a custom loadTemplate method', function () {
150 |
151 | var view;
152 |
153 | beforeEach( function () {
154 | view = new View( { template: "#template" } );
155 | } );
156 |
157 | it( 'its data attributes get applied to the el', function () {
158 | var attributes = JSON.parse( dataAttributes["data-attributes"] );
159 |
160 | expect( view.el.tagName.toLowerCase() ).to.equal( dataAttributes["data-tag-name"] );
161 | expect( view.$el.attr( "class" ) ).to.equal( dataAttributes["data-class-name"] );
162 | expect( view.$el.attr( "id" ) ).to.equal( dataAttributes["data-id"] );
163 | expect( view.$el.attr( "lang" ) ).to.equal( attributes.lang );
164 | expect( view.$el.attr( "title" ) ).to.equal( attributes.title );
165 | } );
166 |
167 | it( 'it gets stored in the cache', function () {
168 | expect( view.declarativeViews.getCachedTemplate() ).to.returnCacheValueFor( dataAttributes, outerTemplateHtml );
169 | } );
170 |
171 | it( 'its inner HTML is returned as the template HTML by the cache', function () {
172 | expect( view.declarativeViews.getCachedTemplate().html ).to.eql( innerTemplateHtml );
173 | } );
174 |
175 | } );
176 |
177 | describe( 'Backbone default behaviour remains unchanged', function () {
178 |
179 | it( 'when the view does not reference a template', function () {
180 | View = Backbone.View.extend();
181 | expect( View ).to.createElWithStandardMechanism;
182 | } );
183 |
184 | it( 'when the view references a template, but the return value of the custom loader does not have data attributes describing the el', function () {
185 | Backbone.DeclarativeViews.custom.loadTemplate = function () { return $( "
Some HTML
" ); };
186 | View = Backbone.View.extend( { template: "#noDataAttr" } );
187 | expect( View ).to.createElWithStandardMechanism;
188 | } );
189 |
190 | it( 'when the view references a template with a string that the custom loader cannot process, throwing a generic error', function () {
191 | // NB ... but if the loader raises the alarm deliberately and throws one of the error types belonging to
192 | // Backbone.DeclarativeViews, business as usual is over: the exception is rethrown and bubbles up. See
193 | // test further below.
194 | Backbone.DeclarativeViews.custom.loadTemplate = function () { throw new Error( "loadTemplate blew up" ); };
195 | View = Backbone.View.extend( { template: "#throwsError" } );
196 | expect( View ).to.createElWithStandardMechanism;
197 | } );
198 |
199 | it( 'when the view has a template property, but it is a function rather than a selector', function () {
200 | // That is because only string properties get forwarded to the cache mechanism, and eventually to the
201 | // custom loader.
202 | View = Backbone.View.extend( { template: function () {
203 | return "";
204 | } } );
205 |
206 | expect( View ).to.createElWithStandardMechanism;
207 | } );
208 |
209 | } );
210 |
211 | describe( 'A Marionette view remains true to its standard behaviour and throws an error on render(), or maybe even earlier', function () {
212 |
213 | it( 'when the view does not reference a template', function () {
214 | View = getMarionetteView().extend();
215 | var view = new View();
216 | expect( function () { view.render(); } ).to.throw( Error );
217 | } );
218 |
219 | it( 'when the view references a template for which the custom loader returns undefined', function () {
220 | // This already blows up when the view is instantiated because the invalid return value becomes a
221 | // problem not during render(), but when the el is constructed.
222 | Backbone.DeclarativeViews.custom.loadTemplate = function () {};
223 | View = getMarionetteView().extend( { template: "#returnsVoid" } );
224 |
225 | expect( function () {
226 | var view = new View();
227 | view.render(); }
228 | ).to.throw( Error );
229 | } );
230 |
231 | it( 'when the view references a template with a string that the custom loader cannot process, throwing an error', function () {
232 | Backbone.DeclarativeViews.custom.loadTemplate = function () { throw new Error( "loadTemplate blew up" ); };
233 | View = getMarionetteView().extend( { template: "#throwsError" } );
234 | var view = new View();
235 | expect( function () { view.render(); } ).to.throw( Error );
236 | } );
237 |
238 | } );
239 |
240 | describe( 'Custom loader error checking', function () {
241 |
242 | describe( 'A friendly error is thrown', function () {
243 |
244 | it( 'when the custom loader returns a value without throwing an error, but that value is not a jQuery object', function () {
245 | Backbone.DeclarativeViews.custom.loadTemplate = function () { return "
Returned template HTML is not wrapped in jQuery object
" };
246 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CustomizationError, "Invalid return value. The custom loadTemplate function must return a jQuery instance" );
247 | } );
248 |
249 | it( 'when the custom loader returns undefined without throwing an error', function () {
250 | Backbone.DeclarativeViews.custom.loadTemplate = function () {};
251 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CustomizationError, "Invalid return value. The custom loadTemplate function must return a jQuery instance" );
252 | } );
253 |
254 | } );
255 |
256 | describe( 'An error raised deliberately in the custom loader, with one of the error types in Backbone.DeclarativeViews, bubbles up uncaught', function () {
257 |
258 | // Generic errors in the loader are caught and suppressed, and Backbone just creates a standard el (see
259 | // tests above). However, he error types of Backbone.DeclarativeViews are allowed to bubble up. That
260 | // way, a custom loader can bypass the error handling and raise the alarm if it needs to.
261 |
262 | it( 'when that error is of type Backbone.Backbone.DeclarativeViews.Error', function () {
263 | Backbone.DeclarativeViews.custom.loadTemplate = function () { throw new Backbone.DeclarativeViews.Error( "a message from the loader" ); };
264 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.Error, "a message from the loader" );
265 | } );
266 |
267 | it( 'when that error is of type Backbone.Backbone.DeclarativeViews.TemplateError', function () {
268 | Backbone.DeclarativeViews.custom.loadTemplate = function () { throw new Backbone.DeclarativeViews.TemplateError( "a message from the loader" ); };
269 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.TemplateError, "a message from the loader" );
270 | } );
271 |
272 | it( 'when that error is of type Backbone.Backbone.DeclarativeViews.CompilerError', function () {
273 | Backbone.DeclarativeViews.custom.loadTemplate = function () { throw new Backbone.DeclarativeViews.CompilerError( "a message from the loader" ); };
274 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CompilerError, "a message from the loader" );
275 | } );
276 |
277 | it( 'when that error is of type Backbone.Backbone.DeclarativeViews.CustomizationError', function () {
278 | Backbone.DeclarativeViews.custom.loadTemplate = function () { throw new Backbone.DeclarativeViews.CustomizationError( "a message from the loader" ); };
279 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CustomizationError, "a message from the loader" );
280 | } );
281 |
282 | } );
283 |
284 | } );
285 |
286 |
287 | } );
288 |
289 | })();
--------------------------------------------------------------------------------
/spec/default.loader.spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it */
2 | (function () {
3 | "use strict";
4 |
5 | describe( 'Default loader modification', function () {
6 |
7 | var dataAttributes, outerTemplateHtml, innerTemplateHtml, View,
8 | originalLoader,
9 | cleanup = function () {
10 | Backbone.DeclarativeViews.clearCache();
11 | Backbone.Marionette.TemplateCache.clear();
12 | };
13 |
14 | beforeEach( function () {
15 |
16 | cleanup();
17 |
18 | dataAttributes = {
19 | "data-tag-name": "ul",
20 | "data-class-name": "listClass",
21 | "data-id": "listId",
22 | "data-attributes": '{ "lang": "en", "title": "title from data attributes" }'
23 | };
24 |
25 | // Construct the HTML strings
26 | // NB Normalize the template HTML by piping it through jQuery
27 | innerTemplateHtml = '
Inner list item
';
28 | outerTemplateHtml = $( '' )
29 | .html( innerTemplateHtml )
30 | .attr( dataAttributes )
31 | .prop( 'outerHTML' );
32 |
33 | originalLoader = Backbone.DeclarativeViews.defaults.loadTemplate;
34 |
35 | Backbone.DeclarativeViews.defaults.loadTemplate = function () {
36 | return $( outerTemplateHtml );
37 | };
38 |
39 | View = Backbone.View.extend();
40 | } );
41 |
42 | afterEach( function () {
43 | cleanup();
44 | Backbone.DeclarativeViews.defaults.loadTemplate = originalLoader;
45 | } );
46 |
47 | describe( 'Arguments provided to the (modified) default loadTemplate method', function () {
48 |
49 | var view;
50 |
51 | beforeEach( function () {
52 | sinon.spy( Backbone.DeclarativeViews.defaults, "loadTemplate" );
53 | } );
54 |
55 | afterEach( function () {
56 | Backbone.DeclarativeViews.defaults.loadTemplate.restore();
57 | } );
58 |
59 | describe( 'When the default loader is called in the context of a view', function () {
60 |
61 | describe( 'with the template being set as a property on the view class', function () {
62 |
63 | beforeEach( function () {
64 | View = Backbone.View.extend( { template: "#template" } );
65 | view = new View();
66 | } );
67 |
68 | it( 'it receives the template property of the view as first argument', function () {
69 | expect( Backbone.DeclarativeViews.defaults.loadTemplate ).to.have.been.calledWith( "#template" );
70 | } );
71 |
72 | it( 'it receives the view as second argument', function () {
73 | expect( Backbone.DeclarativeViews.defaults.loadTemplate ).to.have.been.calledWith( "#template", view );
74 | } );
75 |
76 | } );
77 |
78 | describe( 'with the template being passed in as an option during view instantiation', function () {
79 |
80 | beforeEach( function () {
81 | view = new View( { template: "#template" } );
82 | } );
83 |
84 | it( 'it receives the template option passed to the view as first argument', function () {
85 | expect( Backbone.DeclarativeViews.defaults.loadTemplate ).to.have.been.calledWith( "#template" );
86 | } );
87 |
88 | it( 'it receives the view as second argument', function () {
89 | expect( Backbone.DeclarativeViews.defaults.loadTemplate ).to.have.been.calledWith( "#template", view );
90 | } );
91 |
92 | } );
93 |
94 | describe( 'with a template being set as a property on the view class, and a different template being passed in as an option', function () {
95 |
96 | beforeEach( function () {
97 | View = Backbone.View.extend( { template: "#template" } );
98 | view = new View( { template: "#otherTemplate" } );
99 | } );
100 |
101 | it( 'it receives the template option passed to the view as first argument', function () {
102 | expect( Backbone.DeclarativeViews.defaults.loadTemplate ).to.have.been.calledWith( "#otherTemplate" );
103 | } );
104 |
105 | it( 'it receives the view as second argument', function () {
106 | expect( Backbone.DeclarativeViews.defaults.loadTemplate ).to.have.been.calledWith( "#otherTemplate", view );
107 | } );
108 |
109 | } );
110 |
111 | } );
112 |
113 | describe( 'When the default loader is called from the global API', function () {
114 |
115 | describe( 'with the template selector provided as the only argument', function () {
116 |
117 | beforeEach( function () {
118 | Backbone.DeclarativeViews.getCachedTemplate( "#template" );
119 | } );
120 |
121 | it( 'the loader receives the template selector as first argument', function () {
122 | expect( Backbone.DeclarativeViews.defaults.loadTemplate ).to.have.been.calledWith( "#template" );
123 | } );
124 |
125 | it( 'the second argument provided to the loader is undefined', function () {
126 | expect( Backbone.DeclarativeViews.defaults.loadTemplate ).to.have.been.calledWith( "#template", undefined );
127 | } );
128 |
129 | } );
130 |
131 | describe( 'with the template selector and a view provided as arguments', function () {
132 |
133 | beforeEach( function () {
134 | view = new View();
135 | Backbone.DeclarativeViews.getCachedTemplate( "#template", view );
136 | } );
137 |
138 | it( 'the loader receives the template selector as first argument', function () {
139 | expect( Backbone.DeclarativeViews.defaults.loadTemplate ).to.have.been.calledWith( "#template" );
140 | } );
141 |
142 | it( 'the loader receives the view as second argument', function () {
143 | expect( Backbone.DeclarativeViews.defaults.loadTemplate ).to.have.been.calledWith( "#template", view );
144 | } );
145 |
146 | } );
147 |
148 | } );
149 |
150 | } );
151 |
152 | describe( 'When the jQuery object of the template is provided by a (modified) default loadTemplate method', function () {
153 |
154 | var view;
155 |
156 | beforeEach( function () {
157 | view = new View( { template: "#template" } );
158 | } );
159 |
160 | it( 'its data attributes get applied to the el', function () {
161 | var attributes = JSON.parse( dataAttributes["data-attributes"] );
162 |
163 | expect( view.el.tagName.toLowerCase() ).to.equal( dataAttributes["data-tag-name"] );
164 | expect( view.$el.attr( "class" ) ).to.equal( dataAttributes["data-class-name"] );
165 | expect( view.$el.attr( "id" ) ).to.equal( dataAttributes["data-id"] );
166 | expect( view.$el.attr( "lang" ) ).to.equal( attributes.lang );
167 | expect( view.$el.attr( "title" ) ).to.equal( attributes.title );
168 | } );
169 |
170 | it( 'it gets stored in the cache', function () {
171 | expect( view.declarativeViews.getCachedTemplate() ).to.returnCacheValueFor( dataAttributes, outerTemplateHtml );
172 | } );
173 |
174 | it( 'its inner HTML is returned as the template HTML by the cache', function () {
175 | expect( view.declarativeViews.getCachedTemplate().html ).to.eql( innerTemplateHtml );
176 | } );
177 |
178 | } );
179 |
180 | describe( 'Backbone default behaviour remains unchanged', function () {
181 |
182 | it( 'when the view does not reference a template', function () {
183 | View = Backbone.View.extend();
184 | expect( View ).to.createElWithStandardMechanism;
185 | } );
186 |
187 | it( 'when the view references a template, but the return value of the default loader does not have data attributes describing the el', function () {
188 | Backbone.DeclarativeViews.defaults.loadTemplate = function () { return $( "
Some HTML
" ); };
189 | View = Backbone.View.extend( { template: "#noDataAttr" } );
190 | expect( View ).to.createElWithStandardMechanism;
191 | } );
192 |
193 | it( 'when the view references a template with a string that the default loader cannot process, throwing a generic error', function () {
194 | // NB ... but if the loader raises the alarm deliberately and throws one of the error types belonging to
195 | // Backbone.DeclarativeViews, business as usual is over: the exception is rethrown and bubbles up. See
196 | // test further below.
197 | Backbone.DeclarativeViews.defaults.loadTemplate = function () { throw new Error( "loadTemplate blew up" ); };
198 | View = Backbone.View.extend( { template: "#throwsError" } );
199 | expect( View ).to.createElWithStandardMechanism;
200 | } );
201 |
202 | it( 'when the view has a template property, but it is a function rather than a selector', function () {
203 | // That is because only string properties get forwarded to the cache mechanism, and eventually to the
204 | // default loader.
205 | View = Backbone.View.extend( { template: function () {
206 | return "";
207 | } } );
208 |
209 | expect( View ).to.createElWithStandardMechanism;
210 | } );
211 |
212 | } );
213 |
214 | describe( 'A Marionette view remains true to its standard behaviour and throws an error on render(), or maybe even earlier', function () {
215 |
216 | it( 'when the view does not reference a template', function () {
217 | View = getMarionetteView().extend();
218 | var view = new View();
219 | expect( function () { view.render(); } ).to.throw( Error );
220 | } );
221 |
222 | it( 'when the view references a template for which the default loader returns undefined', function () {
223 | // This already blows up when the view is instantiated because the invalid return value becomes a
224 | // problem not during render(), but when the el is constructed.
225 | Backbone.DeclarativeViews.defaults.loadTemplate = function () {};
226 | View = getMarionetteView().extend( { template: "#returnsVoid" } );
227 |
228 | expect( function () {
229 | var view = new View();
230 | view.render(); }
231 | ).to.throw( Error );
232 | } );
233 |
234 | it( 'when the view references a template with a string that the default loader cannot process, throwing an error', function () {
235 | Backbone.DeclarativeViews.defaults.loadTemplate = function () { throw new Error( "loadTemplate blew up" ); };
236 | View = getMarionetteView().extend( { template: "#throwsError" } );
237 | var view = new View();
238 | expect( function () { view.render(); } ).to.throw( Error );
239 | } );
240 |
241 | } );
242 |
243 | describe( 'Default loader error checking', function () {
244 |
245 | describe( 'A friendly error is thrown', function () {
246 |
247 | it( 'when the (modified) default loader returns a value without throwing an error, but that value is not a jQuery object', function () {
248 | Backbone.DeclarativeViews.defaults.loadTemplate = function () { return "
Returned template HTML is not wrapped in jQuery object
" };
249 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CustomizationError, "Invalid return value. The default loadTemplate function must return a jQuery instance" );
250 | } );
251 |
252 | it( 'when the (modified) default loader returns undefined without throwing an error', function () {
253 | Backbone.DeclarativeViews.defaults.loadTemplate = function () {};
254 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CustomizationError, "Invalid return value. The default loadTemplate function must return a jQuery instance" );
255 | } );
256 |
257 | } );
258 |
259 | describe( 'An error raised deliberately in the (modified) default loader, with one of the error types in Backbone.DeclarativeViews, bubbles up uncaught', function () {
260 |
261 | // Generic errors in the loader are caught and suppressed, and Backbone just creates a standard el (see
262 | // tests above). However, he error types of Backbone.DeclarativeViews are allowed to bubble up. That
263 | // way, a default loader can bypass the error handling and raise the alarm if it needs to.
264 |
265 | it( 'when that error is of type Backbone.Backbone.DeclarativeViews.Error', function () {
266 | Backbone.DeclarativeViews.defaults.loadTemplate = function () { throw new Backbone.DeclarativeViews.Error( "a message from the loader" ); };
267 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.Error, "a message from the loader" );
268 | } );
269 |
270 | it( 'when that error is of type Backbone.Backbone.DeclarativeViews.TemplateError', function () {
271 | Backbone.DeclarativeViews.defaults.loadTemplate = function () { throw new Backbone.DeclarativeViews.TemplateError( "a message from the loader" ); };
272 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.TemplateError, "a message from the loader" );
273 | } );
274 |
275 | it( 'when that error is of type Backbone.Backbone.DeclarativeViews.CompilerError', function () {
276 | Backbone.DeclarativeViews.defaults.loadTemplate = function () { throw new Backbone.DeclarativeViews.CompilerError( "a message from the loader" ); };
277 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CompilerError, "a message from the loader" );
278 | } );
279 |
280 | it( 'when that error is of type Backbone.Backbone.DeclarativeViews.CustomizationError', function () {
281 | Backbone.DeclarativeViews.defaults.loadTemplate = function () { throw new Backbone.DeclarativeViews.CustomizationError( "a message from the loader" ); };
282 | expect( function () { new View( { template: "#template" } ); } ).to.throw( Backbone.DeclarativeViews.CustomizationError, "a message from the loader" );
283 | } );
284 |
285 | } );
286 |
287 | } );
288 |
289 |
290 | } );
291 |
292 | })();
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /*global module:false*/
2 | module.exports = function (grunt) {
3 |
4 | var LIVERELOAD_PORT = 35731,
5 | HTTP_PORT = 9400,
6 | KARMA_PORT = 9877,
7 | WATCHED_FILES_SRC = [
8 | 'src/**/*'
9 | ],
10 | WATCHED_FILES_SPEC = [
11 | 'spec/**/*'
12 | ],
13 | WATCHED_FILES_DIST = [
14 | 'dist/**/*'
15 | ],
16 | WATCHED_FILES_DEMO = [
17 | 'demo/**/*'
18 | ],
19 |
20 | SINON_SOURCE_DIR = 'node_modules/karma-chai-plugins/node_modules/sinon/lib/sinon/',
21 |
22 | path = require( "path" ),
23 | requireFromString = require( "require-from-string" ),
24 |
25 | /**
26 | * Receives an object and the name of a path property on that object. Translates the path property to a new path,
27 | * based on a directory prefix. Does not return anything, modifies the object itself.
28 | *
29 | * The directory prefix can be relative (e.g. "../../"). It may or may not end in a slash.
30 | *
31 | * @param {string} dirPrefix
32 | * @param {Object} object
33 | * @param {string} propertyName
34 | * @param {boolean} [verbose=false]
35 | */
36 | translatePathProperty = function ( dirPrefix, object, propertyName, verbose ) {
37 | var originalPath = object[propertyName];
38 |
39 | if ( originalPath ) {
40 | object[propertyName] = path.normalize( dirPrefix + path.sep + originalPath );
41 | if ( verbose ) grunt.log.writeln( 'Translating path property "' + propertyName + '": ' + originalPath + " => " + object[propertyName] );
42 | }
43 | },
44 |
45 | /**
46 | * Reads an r.js build profile and returns it as an options set for a grunt-contrib-requirejs task.
47 | *
48 | * For a discussion, see https://github.com/gruntjs/grunt-contrib-requirejs/issues/13
49 | *
50 | * Paths in the build profile are relative to the profile location. In the returned options object, they are
51 | * transformed to be relative to the Gruntfile. (The list is nowhere near complete. More properties need to be
52 | * transformed as build profiles become more complex.)
53 | *
54 | * @param {string} profilePath relative to the Gruntfile
55 | * @param {boolean} [verbose=false]
56 | * @returns {Object}
57 | */
58 | getRequirejsBuildProfile = function ( profilePath, verbose ) {
59 | var profileContent = grunt.file.read( profilePath ),
60 | profile = requireFromString( "module.exports = " + profileContent ),
61 |
62 | dirPrefix = path.dirname( profilePath );
63 |
64 | if ( verbose ) grunt.log.writeln( "Loading r.js build profile " + profilePath );
65 |
66 | // Add more paths here as needed.
67 | translatePathProperty( dirPrefix, profile, "mainConfigFile", verbose );
68 | translatePathProperty( dirPrefix, profile, "out", verbose );
69 |
70 | if ( verbose ) grunt.log.writeln();
71 |
72 | return profile;
73 | };
74 |
75 | // Project configuration.
76 | grunt.config.init({
77 | pkg: grunt.file.readJSON('package.json'),
78 | meta: {
79 | version: '<%= pkg.version %>',
80 | banner: '// Backbone.Declarative.Views, v<%= meta.version %>\n' +
81 | '// Copyright (c) 2014-<%= grunt.template.today("yyyy") %> Michael Heim, Zeilenwechsel.de\n' +
82 | '// Distributed under MIT license\n' +
83 | '// http://github.com/hashchange/backbone.declarative.views\n' +
84 | '\n'
85 | },
86 |
87 | preprocess: {
88 | build: {
89 | files: {
90 | 'dist/backbone.declarative.views.js': 'src/backbone.declarative.views.js'
91 | }
92 | },
93 | interactive: {
94 | files: {
95 | 'web-mocha/index.html': 'web-mocha/_index.html'
96 | }
97 | }
98 | },
99 |
100 | concat: {
101 | options: {
102 | banner: "<%= meta.banner %>",
103 | process: function( src, filepath ) {
104 | var bowerVersion = grunt.file.readJSON( "bower.json" ).version,
105 | npmVersion = grunt.file.readJSON( "package.json" ).version;
106 |
107 | if ( npmVersion === undefined || npmVersion === "" ) grunt.fail.fatal( "Version number not specified in package.json. Specify it in bower.json and package.json" );
108 | if ( npmVersion !== bowerVersion ) grunt.fail.fatal( "Version numbers in package.json and bower.json are not identical. Make them match." + " " + npmVersion );
109 | if ( ! /^\d+\.\d+.\d+$/.test( npmVersion ) ) grunt.fail.fatal( 'Version numbers in package.json and bower.json are not semantic. Provide a version number in the format n.n.n, e.g "1.2.3"' );
110 | return src.replace( "__COMPONENT_VERSION_PLACEHOLDER__", npmVersion );
111 | }
112 | },
113 | build: {
114 | src: 'dist/backbone.declarative.views.js',
115 | dest: 'dist/backbone.declarative.views.js'
116 | }
117 | },
118 |
119 | uglify: {
120 | options: {
121 | banner: "<%= meta.banner %>",
122 | mangle: {
123 | except: ['jQuery', 'Zepto', 'Backbone', '_']
124 | },
125 | sourceMap: true
126 | },
127 | core: {
128 | src: 'dist/backbone.declarative.views.js',
129 | dest: 'dist/backbone.declarative.views.min.js'
130 | }
131 | },
132 |
133 | karma: {
134 | options: {
135 | configFile: 'karma.conf.js',
136 | browsers: ['PhantomJS'],
137 | port: KARMA_PORT
138 | },
139 | test: {
140 | reporters: ['progress'],
141 | singleRun: true
142 | },
143 | "test-legacy": {
144 | configFile: 'karma.legacy.conf.js',
145 | reporters: ['progress'],
146 | singleRun: true
147 | },
148 | build: {
149 | reporters: ['progress'],
150 | singleRun: true
151 | }
152 | },
153 |
154 | jshint: {
155 | components: {
156 | // Workaround for merging .jshintrc with Gruntfile options, see http://goo.gl/Of8QoR
157 | options: grunt.util._.merge({
158 | globals: {
159 | // Add vars which are shared between various sub-components
160 | // (before concatenation makes them local)
161 | }
162 | }, grunt.file.readJSON('.jshintrc')),
163 | files: {
164 | src: ['src/**/*.js']
165 | }
166 | },
167 | concatenated: {
168 | options: grunt.util._.merge({
169 | // Suppressing 'W034: Unnecessary directive "use strict"'.
170 | // Redundant nested "use strict" is ok in concatenated file,
171 | // no adverse effects.
172 | '-W034': true
173 | }, grunt.file.readJSON('.jshintrc')),
174 | files: {
175 | src: 'dist/**/backbone.declarative.views.js'
176 | }
177 | }
178 | },
179 |
180 | 'sails-linker': {
181 | options: {
182 | startTag: '',
183 | endTag: '',
184 | fileTmpl: '',
185 | // relative doesn't seem to have any effect, ever
186 | relative: true,
187 | // appRoot is a misnomer for "strip out this prefix from the file path before inserting",
188 | // should be stripPrefix
189 | appRoot: ''
190 | },
191 | interactive_spec: {
192 | options: {
193 | startTag: '',
194 | endTag: ''
195 | },
196 | files: {
197 | // the target file is changed in place; for generating copies, run preprocess first
198 | 'web-mocha/index.html': ['spec/**/*.+(spec|test|tests).js']
199 | }
200 | },
201 | interactive_sinon: {
202 | options: {
203 | startTag: '',
204 | endTag: ''
205 | },
206 | files: {
207 | // the target file is changed in place; for generating copies, run preprocess first
208 | //
209 | // The util/core.js file must be loaded first, and typeof.js must be loaded before match.js.
210 | //
211 | // mock.js must be loaded last (specifically, after spy.js). For the pattern achieving it, see
212 | // http://gruntjs.com/configuring-tasks#globbing-patterns
213 | 'web-mocha/index.html': [
214 | SINON_SOURCE_DIR + 'util/core.js',
215 | SINON_SOURCE_DIR + 'typeof.js',
216 | SINON_SOURCE_DIR + '**/*.js',
217 | '!' + SINON_SOURCE_DIR + 'mock.js',
218 | SINON_SOURCE_DIR + 'mock.js'
219 | ]
220 | }
221 | }
222 | },
223 |
224 | requirejs : {
225 | unifiedPlainBuild : {
226 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/unified/plain-build-config.js', false )
227 | },
228 | unifiedMarionetteBuild : {
229 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/unified/marionette-build-config.js', false )
230 | },
231 | unifiedPrecompiledPlainBuild : {
232 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/unified/plain-precompiled-build-config.js', false )
233 | },
234 | unifiedPrecompiledMarionetteBuild : {
235 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/unified/marionette-precompiled-build-config.js', false )
236 | },
237 | splitBuildVendor : {
238 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/vendor-config.js', false )
239 | },
240 | splitBuildBackboneApp : {
241 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/backbone-app-config.js', false )
242 | },
243 | splitBuildMarionetteApp : {
244 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/marionette-app-config.js', false )
245 | },
246 | splitBuildPrecompiledBackboneApp : {
247 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/backbone-precompiled-app-config.js', false )
248 | },
249 | splitBuildPrecompiledMarionetteApp : {
250 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/marionette-precompiled-app-config.js', false )
251 | }
252 | },
253 |
254 | // Use focus to run Grunt watch with a hand-picked set of simultaneous watch targets.
255 | focus: {
256 | demo: {
257 | include: ['livereloadDemo']
258 | },
259 | demoCi: {
260 | include: ['build', 'livereloadDemo']
261 | },
262 | demoCiDirty: {
263 | include: ['buildDirty', 'livereloadDemo']
264 | }
265 | },
266 |
267 | // Use watch to monitor files for changes, and to kick off a task then.
268 | watch: {
269 | options: {
270 | nospawn: false
271 | },
272 | // Live-reloads the web page when the source files or the spec files change. Meant for test pages.
273 | livereloadTest: {
274 | options: {
275 | livereload: LIVERELOAD_PORT
276 | },
277 | files: WATCHED_FILES_SRC.concat( WATCHED_FILES_SPEC )
278 | },
279 | // Live-reloads the web page when the dist files or the demo files change. Meant for demo pages.
280 | livereloadDemo: {
281 | options: {
282 | livereload: LIVERELOAD_PORT
283 | },
284 | files: WATCHED_FILES_DEMO.concat( WATCHED_FILES_DIST )
285 | },
286 | // Runs the "build" task (ie, runs linter and tests, then compiles the dist files) when the source files or the
287 | // spec files change. Meant for continuous integration tasks ("ci", "demo-ci").
288 | build: {
289 | tasks: ['build'],
290 | files: WATCHED_FILES_SRC.concat( WATCHED_FILES_SPEC )
291 | },
292 | // Runs the "build-dirty" task (ie, compiles the dist files without running linter and tests) when the source
293 | // files change. Meant for "dirty" continuous integration tasks ("ci-dirty", "demo-ci-dirty").
294 | buildDirty: {
295 | tasks: ['build-dirty'],
296 | files: WATCHED_FILES_SRC
297 | }
298 | },
299 |
300 | // Spins up a web server.
301 | connect: {
302 | options: {
303 | port: HTTP_PORT,
304 | // For restricting access to localhost only, change the hostname from '*' to 'localhost'
305 | hostname: '*',
306 | open: true,
307 | base: '.'
308 | },
309 | livereload: {
310 | livereload: LIVERELOAD_PORT
311 | },
312 | test: {
313 | options: {
314 | open: 'http://localhost:<%= connect.options.port %>/web-mocha/',
315 | livereload: LIVERELOAD_PORT
316 | }
317 | },
318 | testNoReload: {
319 | options: {
320 | open: 'http://localhost:<%= connect.options.port %>/web-mocha/',
321 | keepalive: true
322 | }
323 | },
324 | demo: {
325 | options: {
326 | open: 'http://localhost:<%= connect.options.port %>/demo/',
327 | livereload: LIVERELOAD_PORT
328 | }
329 | }
330 | },
331 |
332 | replace: {
333 | version: {
334 | src: ['bower.json', 'package.json'],
335 | overwrite: true,
336 | replacements: [{
337 | from: /"version"\s*:\s*"((\d+\.\d+\.)(\d+))"\s*,/,
338 | to: function (matchedWord, index, fullText, regexMatches) {
339 | var version = grunt.option('inc') ? regexMatches[1] + (parseInt(regexMatches[2], 10) + 1) : grunt.option('to');
340 |
341 | if (version === undefined) grunt.fail.fatal('Version number not specified. Use the --to option, e.g. --to=1.2.3, or the --inc option to increment the revision');
342 | if (typeof version !== "string") grunt.fail.fatal('Version number is not a string. Provide a semantic version number, e.g. --to=1.2.3');
343 | if (!/^\d+\.\d+.\d+$/.test(version)) grunt.fail.fatal('Version number is not semantic. Provide a version number in the format n.n.n, e.g. --to=1.2.3');
344 |
345 | grunt.log.writeln('Modifying file: Changing the version number from ' + regexMatches[0] + ' to ' + version);
346 | return '"version": "' + version + '",';
347 | }
348 | }]
349 | }
350 | },
351 | getver: {
352 | files: ['bower.json', 'package.json']
353 | }
354 | });
355 |
356 | grunt.loadNpmTasks('grunt-preprocess');
357 | grunt.loadNpmTasks('grunt-contrib-concat');
358 | grunt.loadNpmTasks('grunt-contrib-jshint');
359 | grunt.loadNpmTasks('grunt-contrib-uglify');
360 | grunt.loadNpmTasks('grunt-karma');
361 | grunt.loadNpmTasks('grunt-contrib-watch');
362 | grunt.loadNpmTasks('grunt-contrib-connect');
363 | grunt.loadNpmTasks('grunt-sails-linker');
364 | grunt.loadNpmTasks('grunt-text-replace');
365 | grunt.loadNpmTasks('grunt-contrib-requirejs');
366 | grunt.loadNpmTasks('grunt-focus');
367 |
368 | grunt.registerTask('lint', ['jshint:components']);
369 | grunt.registerTask('hint', ['jshint:components']); // alias
370 | grunt.registerTask('test', ['jshint:components', 'karma:test']);
371 | grunt.registerTask('test-legacy', ['jshint:components', 'karma:test-legacy']);
372 | grunt.registerTask('webtest', ['preprocess:interactive', 'sails-linker:interactive_sinon', 'sails-linker:interactive_spec', 'connect:testNoReload']);
373 | grunt.registerTask('interactive', ['preprocess:interactive', 'sails-linker:interactive_sinon', 'sails-linker:interactive_spec', 'connect:test', 'watch:livereloadTest']);
374 | grunt.registerTask('demo', ['connect:demo', 'focus:demo']);
375 | grunt.registerTask('build', ['jshint:components', 'karma:build', 'preprocess:build', 'concat', 'uglify', 'jshint:concatenated', 'requirejs']);
376 | grunt.registerTask('ci', ['build', 'watch:build']);
377 | grunt.registerTask('setver', ['replace:version']);
378 | grunt.registerTask('getver', function () {
379 | grunt.config.get('getver.files').forEach(function (file) {
380 | var config = grunt.file.readJSON(file);
381 | grunt.log.writeln('Version number in ' + file + ': ' + config.version);
382 | });
383 | });
384 |
385 | // Special tasks, not mentioned in Readme documentation:
386 | //
387 | // - test-legacy:
388 | // runs the unit tests with legacy Marionette (version 2.x)
389 | // - requirejs:
390 | // creates build files for the AMD demo with r.js
391 | // - build-dirty:
392 | // builds the project without running checks (no linter, no tests)
393 | // - ci-dirty:
394 | // builds the project without running checks (no linter, no tests) on every source change
395 | // - demo-ci:
396 | // Runs the demo (= "demo" task), and also rebuilds the project on every source change (= "ci" task)
397 | // - demo-ci-dirty:
398 | // Runs the demo (= "demo" task), and also rebuilds the project "dirty", without tests or linter, on every source
399 | // change (= "ci-dirty" task)
400 | grunt.registerTask('build-dirty', ['preprocess:build', 'concat', 'uglify', 'requirejs']);
401 | grunt.registerTask('ci-dirty', ['build-dirty', 'watch:buildDirty']);
402 | grunt.registerTask('demo-ci', ['build', 'connect:demo', 'focus:demoCi']);
403 | grunt.registerTask('demo-ci-dirty', ['build-dirty', 'connect:demo', 'focus:demoCiDirty']);
404 |
405 | // Make 'build' the default task.
406 | grunt.registerTask('default', ['build']);
407 |
408 |
409 | };
410 |
--------------------------------------------------------------------------------
/spec/marionette.cache.spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it */
2 | (function () {
3 | "use strict";
4 |
5 | // These tests require that Marionette has been loaded (after Backbone.Declarative.Views). We also make sure
6 | // joinMarionette() has been called.
7 | Backbone.DeclarativeViews.joinMarionette();
8 |
9 | describe( 'Integrated cache access of Marionette and Backbone.Declarative.Views', function () {
10 |
11 | var dataAttributes, dataAttributes2, dataAttributes3,
12 | $templateNode, $templateNode2, $templateNode3,
13 | modifiedDataAttributes, modifiedHtml, View,
14 |
15 | cleanup = function () {
16 | Backbone.DeclarativeViews.clearCache();
17 | Backbone.Marionette.TemplateCache.clear();
18 | };
19 |
20 | beforeEach ( function () {
21 | cleanup();
22 |
23 | var genericTemplateTag = '';
24 |
25 | dataAttributes = {
26 | "data-tag-name": "section",
27 | "data-class-name": "dataClass",
28 | "data-id": "dataId",
29 | "data-attributes": '{ "lang": "en", "title": "title from data attributes" }'
30 | };
31 |
32 | dataAttributes2 = {
33 | "data-tag-name": "li",
34 | "data-class-name": "dataClass2",
35 | "data-id": "dataId2",
36 | "data-attributes": '{ "lang": "fr", "title": "title from data attributes 2" }'
37 | };
38 |
39 | dataAttributes3 = {
40 | "data-tag-name": "h2",
41 | "data-class-name": "dataClass3",
42 | "data-id": "dataId3",
43 | "data-attributes": '{ "lang": "de", "title": "title from data attributes 3" }'
44 | };
45 |
46 | $templateNode = $( genericTemplateTag )
47 | .attr( "id", "template" )
48 | .attr( dataAttributes )
49 | .text( "Content of template #1" )
50 | .appendTo( "body" );
51 |
52 | $templateNode2 = $( genericTemplateTag )
53 | .attr( "id", "template2" )
54 | .attr( dataAttributes2 )
55 | .text( "Content of template #2" )
56 | .appendTo( "body" );
57 |
58 | $templateNode3 = $( genericTemplateTag )
59 | .attr( "id", "template3" )
60 | .attr( dataAttributes3 )
61 | .text( "Content of template #3" )
62 | .appendTo( "body" );
63 |
64 | modifiedDataAttributes = {
65 | "data-tag-name": "p",
66 | "data-class-name": "modifiedClass",
67 | "data-id": "modifiedId",
68 | "data-attributes": '{ "lang": "es", "title": "title from modified data attributes" }'
69 | };
70 |
71 | modifiedHtml = "This is the modified template markup.";
72 |
73 | View = Backbone.View.extend();
74 |
75 | // First access, priming the cache
76 | new View( { template: "#template" } );
77 | new View( { template: "#template2" } );
78 | new View( { template: "#template3" } );
79 | } );
80 |
81 | afterEach( function () {
82 | cleanup();
83 | $templateNode.remove();
84 | $templateNode2.remove();
85 | $templateNode3.remove();
86 | } );
87 |
88 | describe( 'The Marionette.TemplateCache.clear() method', function () {
89 |
90 | describe( 'when called with arguments', function () {
91 |
92 | it( 'clears a given template from the Marionette cache if the template string is a selector', function () {
93 | // We test this by deleting the template node after first access, then clearing the cache.
94 | // On second access, the invalid cache query should throw an error.
95 | $templateNode.remove();
96 | Backbone.Marionette.TemplateCache.clear( "#template" );
97 | expect( function () { Backbone.Marionette.TemplateCache.get( "#template" ) } ).to.throw( Error, 'Could not find template: "#template"' );
98 | } );
99 |
100 | it( 'clears a given template from the DeclarativeViews cache if the template string is a selector', function () {
101 | // We test this by deleting the template node after first access, then clearing the cache.
102 | // On second access, the cache should store the selector string itself (as the node no
103 | // longer exists).
104 | $templateNode.remove();
105 | Backbone.Marionette.TemplateCache.clear( "#template" );
106 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template" ).html ).to.equal( "#template" );
107 | } );
108 |
109 | it( 'allows changes made to the underlying template node to be picked up by Marionette', function () {
110 | $templateNode
111 | .attr( modifiedDataAttributes )
112 | .html( modifiedHtml );
113 |
114 | Backbone.Marionette.TemplateCache.clear( "#template" );
115 | expect( ( Backbone.Marionette.TemplateCache.get( "#template" ) )() ).to.eql( modifiedHtml );
116 | } );
117 |
118 | it( 'allows changes made to the underlying template node to be picked up by Backbone.DeclarativeViews', function () {
119 | $templateNode
120 | .attr( modifiedDataAttributes )
121 | .html( modifiedHtml );
122 |
123 | Backbone.Marionette.TemplateCache.clear( "#template" );
124 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template" ) ).to.returnCacheValueFor( modifiedDataAttributes, $templateNode );
125 | } );
126 |
127 | it( 'clears multiple templates from the Marionette cache when the selectors are passed as multiple arguments', function () {
128 | $templateNode.remove();
129 | $templateNode2.remove();
130 | $templateNode3.remove();
131 |
132 | Backbone.Marionette.TemplateCache.clear( "#template", "#template2", "#template3" );
133 |
134 | expect( function () { Backbone.Marionette.TemplateCache.get( "#template" ) } ).to.throw( Error, 'Could not find template: "#template"' );
135 | expect( function () { Backbone.Marionette.TemplateCache.get( "#template2" ) } ).to.throw( Error, 'Could not find template: "#template2"' );
136 | expect( function () { Backbone.Marionette.TemplateCache.get( "#template3" ) } ).to.throw( Error, 'Could not find template: "#template3"' );
137 | } );
138 |
139 | it( 'clears multiple templates from the DeclarativeViews cache when the selectors are passed as multiple arguments', function () {
140 | $templateNode.remove();
141 | $templateNode2.remove();
142 | $templateNode3.remove();
143 |
144 | Backbone.Marionette.TemplateCache.clear( "#template", "#template2", "#template3" );
145 |
146 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template" ).html ).to.equal( "#template" );
147 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template2" ).html ).to.equal( "#template2" );
148 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template3" ).html ).to.equal( "#template3" );
149 | } );
150 |
151 | it( 'clears multiple templates from the Marionette cache when the selectors are passed as an array', function () {
152 | $templateNode.remove();
153 | $templateNode2.remove();
154 | $templateNode3.remove();
155 |
156 | Backbone.Marionette.TemplateCache.clear( [ "#template", "#template2", "#template3" ] );
157 |
158 | expect( function () { Backbone.Marionette.TemplateCache.get( "#template" ) } ).to.throw( Error, 'Could not find template: "#template"' );
159 | expect( function () { Backbone.Marionette.TemplateCache.get( "#template2" ) } ).to.throw( Error, 'Could not find template: "#template2"' );
160 | expect( function () { Backbone.Marionette.TemplateCache.get( "#template3" ) } ).to.throw( Error, 'Could not find template: "#template3"' );
161 | } );
162 |
163 | it( 'clears multiple templates from the DeclarativeViews cache when the selectors are passed as an array', function () {
164 | $templateNode.remove();
165 | $templateNode2.remove();
166 | $templateNode3.remove();
167 |
168 | Backbone.Marionette.TemplateCache.clear( [ "#template", "#template2", "#template3" ] );
169 |
170 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template" ).html ).to.equal( "#template" );
171 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template2" ).html ).to.equal( "#template2" );
172 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template3" ).html ).to.equal( "#template3" );
173 | } );
174 |
175 | it.skip( 'does not clear other templates from the Marionette cache', function () {
176 | // Delete the template nodes so that their content indeed must come from the cache
177 | //
178 | // ATTN Skipped - Marionette is buggy, clears the whole cache (as of 2.4.7 and 3.0.0). This is not
179 | // caused by the Marionette integration code of Backbone.Declarative.Views.
180 | $templateNode2.remove();
181 | $templateNode3.remove();
182 |
183 | // Clear template #1 from the cache
184 | Backbone.Marionette.TemplateCache.clear( "#template" );
185 |
186 | // Check cache for template #2 and template #3 - still there?
187 | expect( ( Backbone.Marionette.TemplateCache.get( "#template2" ) )() ).to.eql( $templateNode2.html() );
188 | expect( ( Backbone.Marionette.TemplateCache.get( "#template3" ) )() ).to.eql( $templateNode3.html() );
189 | } );
190 |
191 | it( 'does not clear other templates from the DeclarativeViews cache', function () {
192 | // Delete the template nodes so that their content indeed must come from the cache
193 | $templateNode2.remove();
194 | $templateNode3.remove();
195 |
196 | // Clear template #1 from the cache
197 | Backbone.Marionette.TemplateCache.clear( "#template" );
198 |
199 | // Check cache for template #2 and template #3 - still there?
200 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template2" ) ).to.returnCacheValueFor( dataAttributes2, $templateNode2 );
201 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template3" ) ).to.returnCacheValueFor( dataAttributes3, $templateNode3 );
202 | } );
203 |
204 | it( 'fails silently if the template has already been removed from the cache', function () {
205 | // Removing the template node, which is a precondition for verifying that the cache has indeed been
206 | // cleared.
207 | $templateNode.remove();
208 |
209 | Backbone.Marionette.TemplateCache.clear( "#template" );
210 |
211 | // Second call, should go ahead without error
212 | Backbone.Marionette.TemplateCache.clear( "#template" );
213 |
214 | // Checking that the cache is still empty, and querying it throws the expected error for Marionette,
215 | // and stores the selector string for Backbone.Declarative.Views (because the node no longer exists)
216 | expect( function () { Backbone.Marionette.TemplateCache.get( "#template" ) } ).to.throw( Error, 'Could not find template: "#template"' );
217 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template" ).html ).to.equal( "#template" );
218 | } );
219 |
220 | it( 'fails silently if the template is a string containing text which is not wrapped in HTML elements (uncacheable string), and leaves the existing cache intact', function () {
221 | Backbone.Marionette.TemplateCache.clear( "This is plain text with some markup, but not wrapped in an element" );
222 |
223 | // Check the Marionette cache for templates #1 through #3 - still there?
224 | expect( ( Backbone.Marionette.TemplateCache.get( "#template" ) )() ).to.eql( $templateNode.html() );
225 | expect( ( Backbone.Marionette.TemplateCache.get( "#template2" ) )() ).to.eql( $templateNode2.html() );
226 | expect( ( Backbone.Marionette.TemplateCache.get( "#template3" ) )() ).to.eql( $templateNode3.html() );
227 |
228 | // Check the DeclarativeVies cache for templates #1 through #3 - still there?
229 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template" ) ).to.returnCacheValueFor( dataAttributes, $templateNode );
230 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template2" ) ).to.returnCacheValueFor( dataAttributes2, $templateNode2 );
231 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template3" ) ).to.returnCacheValueFor( dataAttributes3, $templateNode3 );
232 | } );
233 |
234 | it( 'fails silently if the template is not a string, and leaves the existing cache intact', function () {
235 | Backbone.Marionette.TemplateCache.clear( function () { return "
Template content
"; } );
236 |
237 | // Check the Marionette cache for templates #1 through #3 - still there?
238 | expect( ( Backbone.Marionette.TemplateCache.get( "#template" ) )() ).to.eql( $templateNode.html() );
239 | expect( ( Backbone.Marionette.TemplateCache.get( "#template2" ) )() ).to.eql( $templateNode2.html() );
240 | expect( ( Backbone.Marionette.TemplateCache.get( "#template3" ) )() ).to.eql( $templateNode3.html() );
241 |
242 | // Check the DeclarativeVies cache for templates #1 through #3 - still there?
243 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template" ) ).to.returnCacheValueFor( dataAttributes, $templateNode );
244 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template2" ) ).to.returnCacheValueFor( dataAttributes2, $templateNode2 );
245 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template3" ) ).to.returnCacheValueFor( dataAttributes3, $templateNode3 );
246 | } );
247 |
248 | it( 'throws an error when called with an empty string argument', function () {
249 | expect( function () { Backbone.Marionette.TemplateCache.clear( "" ); } ).to.throw( Error );
250 | } );
251 |
252 | } );
253 |
254 | describe( 'when called without arguments', function () {
255 |
256 | it( 'clears the entire Marionette cache', function () {
257 | // We test this by deleting all template nodes after first access, then clearing the cache.
258 | // On second access, the invalid cache query should throw an error.
259 | $templateNode.remove();
260 | $templateNode2.remove();
261 | $templateNode3.remove();
262 |
263 | Backbone.Marionette.TemplateCache.clear();
264 |
265 | expect( function () { Backbone.Marionette.TemplateCache.get( "#template" ) } ).to.throw( Error, 'Could not find template: "#template"' );
266 | expect( function () { Backbone.Marionette.TemplateCache.get( "#template2" ) } ).to.throw( Error, 'Could not find template: "#template2"' );
267 | expect( function () { Backbone.Marionette.TemplateCache.get( "#template3" ) } ).to.throw( Error, 'Could not find template: "#template3"' );
268 | } );
269 |
270 | it( 'clears the entire DeclarativeViews cache', function () {
271 | // We test this by deleting all template nodes after first access, then clearing the cache.
272 | // On second access, the cache should store the selector string itself for each selector
273 | // (as the node no longer exists).
274 | $templateNode.remove();
275 | $templateNode2.remove();
276 | $templateNode3.remove();
277 |
278 | Backbone.Marionette.TemplateCache.clear();
279 |
280 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template" ).html ).to.equal( "#template" );
281 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template2" ).html ).to.equal( "#template2" );
282 | expect( Backbone.DeclarativeViews.getCachedTemplate( "#template3" ).html ).to.equal( "#template3" );
283 | } );
284 |
285 | } );
286 |
287 | } );
288 |
289 | } );
290 |
291 | })();
--------------------------------------------------------------------------------