├── demo ├── .bowerrc ├── demo.css ├── demo.js ├── amd │ ├── templates │ │ ├── precompiled │ │ │ ├── sources │ │ │ │ ├── precompiled.handlebars │ │ │ │ ├── lazy-loaded-precompiled.handlebars │ │ │ │ └── lazy-loaded-precompiled-async.handlebars │ │ │ ├── legacy │ │ │ │ ├── amd │ │ │ │ │ └── precompiled.js │ │ │ │ └── non-amd │ │ │ │ │ ├── lazy-loaded-precompiled.js │ │ │ │ │ └── lazy-loaded-precompiled-async.js │ │ │ └── modern │ │ │ │ ├── amd │ │ │ │ └── precompiled.js │ │ │ │ └── non-amd │ │ │ │ ├── lazy-loaded-precompiled.js │ │ │ │ └── lazy-loaded-precompiled-async.js │ │ └── raw │ │ │ ├── lazy-loaded.hbs │ │ │ └── lazy-loaded-async.hbs │ ├── amd.css │ ├── rjs │ │ ├── config │ │ │ ├── unified │ │ │ │ └── build-config.js │ │ │ └── jsbin-parts │ │ │ │ ├── vendor-config.js │ │ │ │ └── app-config.js │ │ ├── build-commands.md │ │ └── output │ │ │ └── parts │ │ │ └── app.js │ ├── index.html │ ├── require-config.js │ └── main.js ├── about.txt ├── bower.json ├── index.html └── bower-check.js ├── .gitignore ├── spec ├── helpers │ ├── various-utils.js │ ├── basic-utils.js │ ├── suite-utils.js │ └── dom-utils.js └── full.spec.js ├── .jshintrc ├── LICENSE ├── web-mocha ├── test-framework.css ├── help.html └── _index.html ├── bower.json ├── dist ├── marionette.handlebars.min.js ├── marionette.handlebars.min.js.map └── marionette.handlebars.js ├── package.json ├── karma.legacy.conf.js ├── karma.conf.js ├── src └── marionette.handlebars.js ├── README.md └── Gruntfile.js /demo/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_demo_components" 3 | } 4 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin: 0.25rem; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | ( function( Backbone, _ ) { 2 | "use strict"; 3 | 4 | }( Backbone, _ )); -------------------------------------------------------------------------------- /demo/amd/templates/precompiled/sources/precompiled.handlebars: -------------------------------------------------------------------------------- 1 | This paragraph is served with a {{origin}} template. -------------------------------------------------------------------------------- /demo/amd/templates/raw/lazy-loaded.hbs: -------------------------------------------------------------------------------- 1 | This paragraph is served with a {{origin}} template. The template has been loaded synchronously. -------------------------------------------------------------------------------- /demo/amd/amd.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin: 0.25rem; 5 | } 6 | 7 | #header { 8 | margin-bottom: 2rem; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /demo/amd/templates/raw/lazy-loaded-async.hbs: -------------------------------------------------------------------------------- 1 | This paragraph is served with a {{origin}} template. This template has been loaded asynchronously. -------------------------------------------------------------------------------- /demo/amd/templates/precompiled/sources/lazy-loaded-precompiled.handlebars: -------------------------------------------------------------------------------- 1 | This paragraph is served with a {{origin}}, precompiled template. The template has been loaded synchronously. -------------------------------------------------------------------------------- /demo/amd/rjs/config/unified/build-config.js: -------------------------------------------------------------------------------- 1 | ({ 2 | mainConfigFile: "../../../require-config.js", 3 | optimize: "none", 4 | name: "local.main", 5 | out: "../../output/unified/build.js" 6 | }) -------------------------------------------------------------------------------- /demo/amd/templates/precompiled/sources/lazy-loaded-precompiled-async.handlebars: -------------------------------------------------------------------------------- 1 | This paragraph is served with a {{origin}}, precompiled template. This template has been loaded asynchronously. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | *.swo 4 | *.orig 5 | tmp/ 6 | ext/ 7 | node_modules/ 8 | bower_components/ 9 | _SpecRunner.html 10 | .idea/ 11 | web-mocha/index.html 12 | reports/ 13 | demo/bower_demo_components/ 14 | spec-helpers-library/ 15 | -------------------------------------------------------------------------------- /demo/amd/rjs/config/jsbin-parts/vendor-config.js: -------------------------------------------------------------------------------- 1 | ({ 2 | mainConfigFile: "../../../require-config.js", 3 | optimize: "none", 4 | name: "local.main", 5 | excludeShallow: [ 6 | "local.main", 7 | "precompiled.templates" 8 | ], 9 | out: "../../output/parts/vendor.js" 10 | }) -------------------------------------------------------------------------------- /demo/about.txt: -------------------------------------------------------------------------------- 1 | If you'd like to build a demo or an interactive playground for this project, 2 | edit the files in this directory. 3 | 4 | With `grunt demo`, the index.html file is displayed in the browser, and the 5 | demo, src and spec directories are monitored for changes. If anything is altered 6 | there, the grunt task will live-reload the demo page. 7 | -------------------------------------------------------------------------------- /spec/helpers/various-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the basic Marionette view type: Marionette.ItemView for Marionette 1 and 2, Marionette.View for Marionette 3. 3 | * 4 | * @returns {Backbone.Marionette.ItemView|Backbone.Marionette.View} 5 | */ 6 | function getMarionetteView () { 7 | return Backbone.Marionette.ItemView || Backbone.Marionette.View; 8 | } 9 | -------------------------------------------------------------------------------- /demo/amd/rjs/config/jsbin-parts/app-config.js: -------------------------------------------------------------------------------- 1 | ({ 2 | mainConfigFile: "../../../require-config.js", 3 | optimize: "none", 4 | name: "local.main", 5 | exclude: [ 6 | "jquery", 7 | "underscore", 8 | "backbone", 9 | "backbone.radio", 10 | "marionette", 11 | "handlebars", 12 | "handlebars.runtime", 13 | "backbone.declarative.views", 14 | "marionette.handlebars" 15 | ], 16 | out: "../../output/parts/app.js" 17 | }) -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "curly": false, 4 | "eqeqeq": true, 5 | "es3": true, 6 | "freeze": true, 7 | "immed": true, 8 | "latedef": "nofunc", 9 | "newcap": true, 10 | "noarg": true, 11 | "noempty": true, 12 | "nonbsp": true, 13 | "undef": true, 14 | 15 | "eqnull": true, 16 | "expr": true, 17 | "sub": true, 18 | 19 | "browser": true, 20 | "jquery": true, 21 | 22 | "strict": true, 23 | 24 | "globals": { 25 | "Backbone": true, 26 | "_": true, 27 | "$": true, 28 | "Handlebars": true, 29 | "JSON": false, 30 | "require": false, 31 | "define": false, 32 | "module": false, 33 | "exports": true 34 | } 35 | } -------------------------------------------------------------------------------- /demo/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "main": "index.html", 4 | "version": "0.0.0", 5 | "homepage": "https://github.com/hashchange/marionette.handlebars", 6 | "authors": [ 7 | "hashchange " 8 | ], 9 | "description": "Marionette.Handlebars demo and playground", 10 | "keywords": [ 11 | "demo" 12 | ], 13 | "license": "MIT", 14 | "private": true, 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "bower_demo_components", 20 | "test", 21 | "tests" 22 | ], 23 | "dependencies": { 24 | "backbone.declarative.views": "^4.0.0", 25 | "foundation": "~5.5.1", 26 | "modernizr": "^2 || ~3.3.1", 27 | "requirejs": "~2.3.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /demo/amd/templates/precompiled/legacy/amd/precompiled.js: -------------------------------------------------------------------------------- 1 | define(['handlebars.runtime'], function(Handlebars) { 2 | Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 3 | return templates['precompiled'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) { 4 | var helper; 5 | 6 | return "This paragraph is served with a " 7 | + this.escapeExpression(((helper = (helper = helpers.origin || (depth0 != null ? depth0.origin : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0,{"name":"origin","hash":{},"data":data}) : helper))) 8 | + " template."; 9 | },"useData":true}); 10 | }); -------------------------------------------------------------------------------- /demo/amd/templates/precompiled/legacy/non-amd/lazy-loaded-precompiled.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 3 | templates['lazy-loaded-precompiled'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) { 4 | var helper; 5 | 6 | return "This paragraph is served with a " 7 | + this.escapeExpression(((helper = (helper = helpers.origin || (depth0 != null ? depth0.origin : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0,{"name":"origin","hash":{},"data":data}) : helper))) 8 | + ", precompiled template. The template has been loaded synchronously."; 9 | },"useData":true}); 10 | })(); -------------------------------------------------------------------------------- /demo/amd/templates/precompiled/modern/amd/precompiled.js: -------------------------------------------------------------------------------- 1 | define(['handlebars.runtime'], function(Handlebars) { 2 | Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 3 | return templates['precompiled'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 4 | var helper; 5 | 6 | return "This paragraph is served with a " 7 | + container.escapeExpression(((helper = (helper = helpers.origin || (depth0 != null ? depth0.origin : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"origin","hash":{},"data":data}) : helper))) 8 | + " template."; 9 | },"useData":true}); 10 | }); -------------------------------------------------------------------------------- /demo/amd/templates/precompiled/legacy/non-amd/lazy-loaded-precompiled-async.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 3 | templates['lazy-loaded-precompiled-async'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) { 4 | var helper; 5 | 6 | return "This paragraph is served with a " 7 | + this.escapeExpression(((helper = (helper = helpers.origin || (depth0 != null ? depth0.origin : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0,{"name":"origin","hash":{},"data":data}) : helper))) 8 | + ", precompiled template. This template has been loaded asynchronously."; 9 | },"useData":true}); 10 | })(); -------------------------------------------------------------------------------- /demo/amd/templates/precompiled/modern/non-amd/lazy-loaded-precompiled.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 3 | templates['lazy-loaded-precompiled'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 4 | var helper; 5 | 6 | return "This paragraph is served with a " 7 | + container.escapeExpression(((helper = (helper = helpers.origin || (depth0 != null ? depth0.origin : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"origin","hash":{},"data":data}) : helper))) 8 | + ", precompiled template. The template has been loaded synchronously."; 9 | },"useData":true}); 10 | })(); -------------------------------------------------------------------------------- /demo/amd/templates/precompiled/modern/non-amd/lazy-loaded-precompiled-async.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 3 | templates['lazy-loaded-precompiled-async'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 4 | var helper; 5 | 6 | return "This paragraph is served with a " 7 | + container.escapeExpression(((helper = (helper = helpers.origin || (depth0 != null ? depth0.origin : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"origin","hash":{},"data":data}) : helper))) 8 | + ", precompiled template. This template has been loaded asynchronously."; 9 | },"useData":true}); 10 | })(); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 Michael Heim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /web-mocha/test-framework.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | .aux { 4 | margin-left: 65px; 5 | position: absolute; 6 | top: 15px; 7 | } 8 | 9 | #test-framework-help-link { 10 | width: 12em; 11 | /*margin-left: 450px;*/ 12 | } 13 | 14 | #alternate-setup { 15 | top: 15px; 16 | } 17 | .aux, 18 | .test-framework-help nav { 19 | margin-top: 1em; 20 | margin-bottom: 1em; 21 | font: 12px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 22 | color: #888; 23 | } 24 | .aux a, 25 | .test-framework-help a { 26 | text-decoration: none; 27 | color: inherit; 28 | } 29 | 30 | .aux a:hover, 31 | .aux a:active, 32 | .test-framework-help nav a:hover, 33 | .test-framework-help nav a:active 34 | { 35 | text-decoration: underline; 36 | } 37 | 38 | .test-framework-help section a { 39 | color: #555; 40 | border-bottom: 1px dotted black; 41 | } 42 | 43 | .test-framework-help section a:hover, 44 | .test-framework-help section a:active { 45 | border-bottom-style: solid; 46 | border-bottom-color: #888; 47 | } 48 | 49 | .test-framework-help { 50 | margin: 0; 51 | padding: 15px 65px; 52 | font: 16px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif 53 | } 54 | 55 | .test-framework-help h1 { 56 | font-size: 20px; 57 | font-weight: 200; 58 | } 59 | 60 | .test-framework-help ul { 61 | padding-left: 0; 62 | } 63 | 64 | .test-framework-help li { 65 | margin-bottom: 0.5em; 66 | } 67 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marionette.handlebars", 3 | "version": "2.0.0", 4 | "homepage": "https://github.com/hashchange/marionette.handlebars", 5 | "authors": [ 6 | "Michael Heim " 7 | ], 8 | "description": "Using Marionette with Handlebars and Mustache templates.", 9 | "main": "dist/marionette.handlebars.js", 10 | "keywords": [ 11 | "backbone", 12 | "marionette", 13 | "handlebars", 14 | "mustache", 15 | "templates", 16 | "views", 17 | "models", 18 | "collections" 19 | ], 20 | "license": "MIT", 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "test", 26 | "tests", 27 | "public", 28 | "reports", 29 | "demo", 30 | "lib-other", 31 | "web-mocha", 32 | "spec", 33 | "src", 34 | "Gruntfile.js", 35 | "karma.conf.js", 36 | "package.json" 37 | ], 38 | "devDependencies": { 39 | "jquery": "^3.0.0 <3.3.0", 40 | "jquery-legacy-v1": "jquery#^1.5.0", 41 | "jquery-legacy-v2": "jquery#^2.0.0", 42 | "handlebars-legacy-v2": "handlebars#^2.0.0", 43 | "handlebars-legacy-v3": "handlebars#^3.0.0", 44 | "marionette-legacy": "marionette#^2.0.0" 45 | }, 46 | "dependencies": { 47 | "backbone": "^1.0.0 <1.4.0", 48 | "underscore": "^1.5.0 <1.9.0", 49 | "marionette": "^1.0.0 || ^2.0.0 || ^3.0.0", 50 | "handlebars": "~1.3.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /spec/helpers/basic-utils.js: -------------------------------------------------------------------------------- 1 | function isNumber ( value ) { 2 | // Done as in the Lodash compatibility build, but rejecting NaN as a number. 3 | var isNumeric = typeof value === 'number' || value && typeof value === 'object' && Object.prototype.toString.call( value ) === '[object Number]' || false; 4 | 5 | // Reject NaN before returning 6 | return isNumeric && value === +value; 7 | } 8 | 9 | function isString ( value ) { 10 | // Done as in the Lodash compatibility build 11 | return typeof value === 'string' || value && typeof value === 'object' && Object.prototype.toString.call(value) === '[object String]' || false; 12 | } 13 | 14 | function isUndefined( value ) { 15 | return typeof value === "undefined"; 16 | } 17 | 18 | function varExists ( variable ) { 19 | return !isUndefined( variable ); 20 | } 21 | 22 | 23 | function getTimestamp () { 24 | return Date.now && Date.now() || +new Date(); 25 | } 26 | 27 | 28 | function log ( message ) { 29 | "use strict"; 30 | if ( typeof console !== "undefined" && console.log ) console.log( message ); 31 | } 32 | 33 | function warn ( message ) { 34 | "use strict"; 35 | if ( typeof console !== "undefined" ) console.warn ? console.warn( message ) : log( "WARN " + message ); 36 | } 37 | 38 | 39 | /** 40 | * Makes sure a string ends in a semicolon, unless the string is empty. Useful for cssText strings. 41 | * 42 | * @param {string} cssString 43 | * @returns {string} 44 | */ 45 | function ensureTrailingSemicolon ( cssString ) { 46 | if ( cssString.length && cssString.slice( -1 ) !== ";" ) cssString += ";"; 47 | return cssString; 48 | } 49 | -------------------------------------------------------------------------------- /web-mocha/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test Framework Help 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Test Framework: Help and Reference

14 |

The test framework is based on Mocha and Chai. It includes these components:

15 | 38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /dist/marionette.handlebars.min.js: -------------------------------------------------------------------------------- 1 | // Marionette.Handlebars, v2.0.0 2 | // Copyright (c) 2015-2016 Michael Heim, Zeilenwechsel.de 3 | // Distributed under MIT license 4 | // http://github.com/hashchange/marionette.handlebars 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","handlebars","marionette"],b):c?b(exports,require("underscore"),require("backbone"),require("handlebars"),require("marionette")):b({},_,Backbone,Handlebars)}(this,function(a,_,Backbone,b){"use strict";function c(a){return _.isString(a)&&a.length>0}function d(a){return c(a)||f.TemplateCache.allowCompiledTemplatesOverHttp&&_.isFunction(a)}var e,f=Backbone.Marionette;if(!f)throw new Error("Load error: Backbone.Marionette is not available");e=f.TemplateCache.prototype.loadTemplate,f.TemplateCache.allowCompiledTemplatesOverHttp=!1,f.TemplateCache.MarionetteHandlebarsVersion="2.0.0",_.extend(f.TemplateCache.prototype,{loadTemplate:function(a,b){var f,g=this.getPrecompiledTemplate(a);if(!g||!_.isFunction(g)){try{f=e.call(this,a,b)}catch(a){}c(f)||(f=this.lazyLoadTemplate(a,b)),d(f)||this.throwTemplateError(a)}return f||g},compileTemplate:function(a,c){return _.isFunction(a)?a:b.compile(a,c)},getPrecompiledTemplate:function(a){return b.templates&&b.templates[a]},lazyLoadTemplate:function(a,b){},throwTemplateError:function(a){var b="NoTemplateError",c='Could not load template: "'+a+'". It does not exist, is of an illegal type, or has content which cannot be processed.';if(f.Error)throw new f.Error({name:b,message:c});if("function"!=typeof throwError)throw new Error(c);throwError(c,b)}}),a.info="Marionette.Handlebars has loaded. Don't use the exported value of the module. Its functionality is available inside the Backbone namespace."}); 8 | //# sourceMappingURL=marionette.handlebars.min.js.map -------------------------------------------------------------------------------- /dist/marionette.handlebars.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["marionette.handlebars.js"],"names":["root","factory","supportsExports","exports","nodeType","module","define","amd","require","_","Backbone","Handlebars","this","isValidTemplateHtml","templateData","isString","length","isValidTemplateReturnValue","Marionette","TemplateCache","allowCompiledTemplatesOverHttp","isFunction","origLoadTemplate","Error","prototype","loadTemplate","MarionetteHandlebarsVersion","extend","templateId","options","templateHtml","precompiledTemplate","getPrecompiledTemplate","call","err","lazyLoadTemplate","throwTemplateError","compileTemplate","template","compile","templates","errType","errMsg","name","message","throwError","info"],"mappings":";;;;;;CAKG,SAAWA,EAAMC,GAChB,YAQA,IAAIC,GAAqC,gBAAZC,UAAwBA,UAAYA,QAAQC,UAA8B,gBAAXC,SAAuBA,SAAWA,OAAOD,QAO9G,mBAAXE,SAA+C,gBAAfA,QAAOC,KAAoBD,OAAOC,IAG1ED,QAAU,UAAW,aAAc,WAAY,aAAc,cAAgBL,GAErEC,EAGRD,EAASE,QAASK,QAAS,cAAgBA,QAAS,YAAcA,QAAS,cAAgBA,QAAS,eAKpGP,KAAaQ,EAAGC,SAAUC,aAI/BC,KAAM,SAAWT,EAASM,EAAGC,SAAUC,GACtC,YAiIA,SAASE,GAAsBC,GAC3B,MAAOL,GAAEM,SAAUD,IAAkBA,EAAaE,OAAS,EAY/D,QAASC,GAA6BH,GAClC,MAAOD,GAAqBC,IAAkBI,EAAWC,cAAcC,gCAAkCX,EAAEY,WAAYP,GA7I3H,GAAIQ,GACAJ,EAAaR,SAASQ,UAE1B,KAAOA,EAAa,KAAM,IAAIK,OAAO,mDAErCD,GAAmBJ,EAAWC,cAAcK,UAAUC,aAGtDP,EAAWC,cAAcC,gCAAiC,EAE1DF,EAAWC,cAAcO,4BAA8B,QAEvDjB,EAAEkB,OAAQT,EAAWC,cAAcK,WAqB/BC,aAAc,SAAWG,EAAYC,GACjC,GAAIC,GACAC,EAAsBnB,KAAKoB,uBAAwBJ,EAEvD,KAAOG,IAAyBtB,EAAEY,WAAYU,GAAwB,CAClE,IACID,EAAeR,EAAiBW,KAAMrB,KAAMgB,EAAYC,GAC1D,MAAQK,IAEHrB,EAAqBiB,KAAiBA,EAAelB,KAAKuB,iBAAkBP,EAAYC,IAGxFZ,EAA4Ba,IAAiBlB,KAAKwB,mBAAoBR,GAGjF,MAAOE,IAAgBC,GAc3BM,gBAAiB,SAAWC,EAAUT,GAClC,MAAOpB,GAAEY,WAAYiB,GAAaA,EAAW3B,EAAW4B,QAASD,EAAUT,IAe/EG,uBAAwB,SAAWJ,GAC/B,MAAOjB,GAAW6B,WAAa7B,EAAW6B,UAAUZ,IAaxDO,iBAAkB,SAAWP,EAAYC,KASzCO,mBAAoB,SAAWR,GAE3B,GAAIa,GAAU,kBACVC,EAAS,6BAA+Bd,EAAa,wFAEzD,IAAKV,EAAWK,MAEZ,KAAM,IAAIL,GAAWK,OAASoB,KAAMF,EAASG,QAASF,GACnD,IAA2B,kBAAfG,YAKf,KAAM,IAAItB,OAAOmB,EAHjBG,YAAYH,EAAQD,MAuChCtC,EAAQ2C,KAAO","file":"marionette.handlebars.min.js"} -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Demo and Playground 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |

Demos

28 |
29 | 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /demo/amd/rjs/build-commands.md: -------------------------------------------------------------------------------- 1 | # Generating r.js builds 2 | 3 | ## Using a Grunt task 4 | 5 | Instead of individual r.js calls, the following command will create all builds: 6 | 7 | ``` 8 | grunt requirejs 9 | ``` 10 | 11 | The grunt task simply reads the build profiles described below, and feeds them to r.js. 12 | 13 | 14 | ## Split builds with two build files, for JS Bin demos 15 | 16 | The demo HTML files for JS Bin reference two concatenated build files: 17 | 18 | - `vendor.js` for the third-party dependencies. It includes Marionette.Handlebars. 19 | - `app.js` for the demo code, consisting of local modules. 20 | 21 | The code is not rolled up into a single file because that file would be massive, making it unnecessarily difficult to examine the demo code. The purpose of the demo is to see how Marionette.Handlebars is used, so it makes sense to keep the client code separate. 22 | 23 | ### Adjustments 24 | 25 | Care must be taken to avoid duplication. A module pulled into `vendor.js` must not be part of `app.js`, and vice versa. Update the module exclusions in **all** build config files when new modules are added to a demo. 26 | 27 | ### r.js calls 28 | 29 | Open a command prompt in the **project root** directory. 30 | 31 | ``` 32 | # For vendor.js: 33 | 34 | node node_modules/requirejs/bin/r.js -o demo/amd/rjs/config/jsbin-parts/vendor-config.js 35 | 36 | # For app.js: 37 | 38 | node node_modules/requirejs/bin/r.js -o demo/amd/rjs/config/jsbin-parts/app-config.js 39 | ``` 40 | 41 | ### Output files 42 | 43 | The output is written to the directory `demo/amd/rjs/output/parts`. 44 | 45 | 46 | ## Single-file builds, for local demos 47 | 48 | Builds for local demos are created to test that the setup continues to work after optimization with r.js. All modules of a demo end up in a single file. For easier examination, the file is not minified. 49 | 50 | For more info, see the comments in `index.html`. 51 | 52 | ### r.js calls 53 | 54 | For building the output file, open a command prompt in the **project root** directory, and run this command: 55 | 56 | ``` 57 | node node_modules/requirejs/bin/r.js -o demo/amd/rjs/config/unified/build-config.js 58 | ``` 59 | 60 | ### Output files 61 | 62 | The output is written to the directory `demo/amd/rjs/output/unified`. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marionette.handlebars", 3 | "version": "2.0.0", 4 | "homepage": "https://github.com/hashchange/marionette.handlebars", 5 | "bugs": "https://github.com/hashchange/marionette.handlebars/issues", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/hashchange/marionette.handlebars.git" 9 | }, 10 | "author": "Michael Heim (http://www.zeilenwechsel.de/)", 11 | "description": "Using Marionette with Handlebars and Mustache templates.", 12 | "main": "dist/marionette.handlebars.js", 13 | "keywords": [ 14 | "backbone", 15 | "marionette", 16 | "handlebars", 17 | "mustache", 18 | "templates", 19 | "views", 20 | "models", 21 | "collections" 22 | ], 23 | "license": "MIT", 24 | "scripts": { 25 | "cleanup": "del-cli bower_components demo/bower_demo_components node_modules -f", 26 | "setup": "npm install && bower install && cd demo && bower install && cd ..", 27 | "reinstall": "npm run cleanup -s && npm run setup || npm run setup" 28 | }, 29 | "dependencies": { 30 | "backbone": "^1.0.0 <1.4.0", 31 | "underscore": "^1.5.0 <1.9.0", 32 | "backbone.marionette": "^1.0.0 || ^2.0.0 || ^3.0.0", 33 | "handlebars": "~1.3.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" 34 | }, 35 | "devDependencies": { 36 | "bower": "^1.8.0", 37 | "connect-livereload": "~0.6.0", 38 | "del-cli": "~0.2.1", 39 | "grunt": "^1.0.1", 40 | "grunt-cli": "^1.2.0", 41 | "grunt-contrib-concat": "~1.0.1", 42 | "grunt-contrib-connect": "~1.0.2", 43 | "grunt-contrib-jshint": "~1.1.0", 44 | "grunt-contrib-requirejs": "^1.0.0", 45 | "grunt-contrib-uglify": "~2.2.0", 46 | "grunt-contrib-watch": "~1.0.0", 47 | "grunt-focus": "~1.0.0", 48 | "grunt-karma": "~2.0.0", 49 | "grunt-mocha": "~1.0.4", 50 | "grunt-preprocess": "~5.1.0", 51 | "grunt-sails-linker": "~1.0.4", 52 | "grunt-text-replace": "~0.4.0", 53 | "karma": "~1.5.0", 54 | "karma-chai-plugins": "~0.8.0", 55 | "karma-chrome-launcher": "~2.0.0", 56 | "karma-firefox-launcher": "~1.0.1", 57 | "karma-html2js-preprocessor": "~1.1.0", 58 | "karma-ie-launcher": "~1.0.0", 59 | "karma-mocha": "~1.3.0", 60 | "karma-mocha-reporter": "~2.2.3", 61 | "karma-opera-launcher": "~1.0.0", 62 | "karma-phantomjs-launcher": "~1.0.4", 63 | "karma-requirejs": "~1.1.0", 64 | "karma-safari-launcher": "1.0.0", 65 | "karma-script-launcher": "~1.0.0", 66 | "karma-slimerjs-launcher": "~1.1.0", 67 | "mocha": "~3.2.0", 68 | "phantomjs-prebuilt": "^2.1.14", 69 | "require-from-string": "^1.2.1", 70 | "requirejs": "^2.3.3" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /demo/amd/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | AMD setup - Demo and Playground 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 |
42 | 43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 58 | 59 | 67 | 68 | 69 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /karma.legacy.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 legacy versions here: jQuery 1, Handlebars 3, Marionette 2. Switch to modern versions as needed. 30 | 31 | 'bower_components/jquery-legacy-v1/dist/jquery.js', 32 | // 'bower_components/jquery-legacy-v2/dist/jquery.js', 33 | // 'bower_components/jquery/dist/jquery.js', 34 | 35 | 'bower_components/underscore/underscore.js', 36 | 'bower_components/backbone/backbone.js', 37 | 38 | // 'bower_components/handlebars-legacy-v2/handlebars.js', 39 | 'bower_components/handlebars-legacy-v3/handlebars.js', 40 | // 'bower_components/handlebars/handlebars.js', 41 | 42 | 'bower_components/marionette-legacy/lib/backbone.marionette.js', 43 | // 'bower_components/backbone.radio/build/backbone.radio.js', 44 | // 'bower_components/marionette/lib/backbone.marionette.js', 45 | 46 | // Component under test 47 | 'src/marionette.handlebars.js', 48 | 49 | // Test helpers 50 | 'spec/helpers/**/*.js', 51 | 52 | // Tests 53 | 'spec/**/*.+(spec|test|tests).js' 54 | ], 55 | 56 | 57 | // list of files to exclude 58 | exclude: [ 59 | 60 | ], 61 | 62 | 63 | // test results reporter to use 64 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage', 'mocha' 65 | reporters: ['progress'], 66 | 67 | 68 | // web server port 69 | port: 9876, 70 | 71 | 72 | // enable / disable colors in the output (reporters and logs) 73 | colors: true, 74 | 75 | 76 | // level of logging 77 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 78 | logLevel: config.LOG_INFO, 79 | 80 | 81 | // enable / disable watching file and executing tests whenever any file changes 82 | autoWatch: false, 83 | 84 | 85 | // Start these browsers, currently available: 86 | // - Chrome 87 | // - ChromeCanary 88 | // - Firefox 89 | // - Opera 90 | // - Safari 91 | // - PhantomJS 92 | // - SlimerJS 93 | // - IE (Windows only) 94 | // 95 | // ATTN Interactive debugging in PhpStorm/WebStorm doesn't work with PhantomJS. Use Firefox or Chrome instead. 96 | browsers: ['PhantomJS'], 97 | 98 | 99 | // If browser does not capture in given timeout [ms], kill it 100 | captureTimeout: 60000, 101 | 102 | 103 | // Continuous Integration mode 104 | // if true, it capture browsers, run tests and exit 105 | singleRun: false 106 | }); 107 | }; 108 | -------------------------------------------------------------------------------- /web-mocha/_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mocha Spec Runner 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 54 | 55 | 56 | 57 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /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. Likewise for Handlebars and Marionette. Switch to legacy versions as needed. 30 | // 31 | // NB Tests run through the interactive web interface use legacy versions: jQuery 1, Handlebars 3, Marionette 2. 32 | // Use `grunt interactive` or `grunt webtest` for them. 33 | 34 | // 'bower_components/jquery-legacy-v1/dist/jquery.js', 35 | // 'bower_components/jquery-legacy-v2/dist/jquery.js', 36 | 'bower_components/jquery/dist/jquery.js', 37 | 38 | 'bower_components/underscore/underscore.js', 39 | 'bower_components/backbone/backbone.js', 40 | 41 | // 'bower_components/handlebars-legacy-v2/handlebars.js', 42 | // 'bower_components/handlebars-legacy-v3/handlebars.js', 43 | 'bower_components/handlebars/handlebars.js', 44 | 45 | // 'bower_components/marionette-legacy/lib/backbone.marionette.js', 46 | 'bower_components/backbone.radio/build/backbone.radio.js', 47 | 'bower_components/marionette/lib/backbone.marionette.js', 48 | 49 | // Component under test 50 | 'src/marionette.handlebars.js', 51 | 52 | // Test helpers 53 | 'spec/helpers/**/*.js', 54 | 55 | // Tests 56 | 'spec/**/*.+(spec|test|tests).js' 57 | ], 58 | 59 | 60 | // list of files to exclude 61 | exclude: [ 62 | 63 | ], 64 | 65 | 66 | // test results reporter to use 67 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage', 'mocha' 68 | reporters: ['progress'], 69 | 70 | 71 | // web server port 72 | port: 9876, 73 | 74 | 75 | // enable / disable colors in the output (reporters and logs) 76 | colors: true, 77 | 78 | 79 | // level of logging 80 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 81 | logLevel: config.LOG_INFO, 82 | 83 | 84 | // enable / disable watching file and executing tests whenever any file changes 85 | autoWatch: false, 86 | 87 | 88 | // Start these browsers, currently available: 89 | // - Chrome 90 | // - ChromeCanary 91 | // - Firefox 92 | // - Opera 93 | // - Safari 94 | // - PhantomJS 95 | // - SlimerJS 96 | // - IE (Windows only) 97 | // 98 | // ATTN Interactive debugging in PhpStorm/WebStorm doesn't work with PhantomJS. Use Firefox or Chrome instead. 99 | browsers: ['PhantomJS'], 100 | 101 | 102 | // If browser does not capture in given timeout [ms], kill it 103 | captureTimeout: 60000, 104 | 105 | 106 | // Continuous Integration mode 107 | // if true, it capture browsers, run tests and exit 108 | singleRun: false 109 | }); 110 | }; 111 | -------------------------------------------------------------------------------- /spec/helpers/suite-utils.js: -------------------------------------------------------------------------------- 1 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 2 | // Depends on basic-utils.js 3 | // Depends on dom-utils.js > basic-utils.js 4 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 5 | 6 | function describe_noPhantom () { 7 | describeUnless.apply( undefined, [ 8 | isPhantomJs(), 9 | "Skipping tests in PhantomJS. Use another browser to run the full suite." 10 | ].concat( Array.prototype.slice.call( arguments ) ) ); 11 | } 12 | 13 | function it_noPhantom () { 14 | itUnless.apply( undefined, [ 15 | isPhantomJs(), 16 | "Skipping test in PhantomJS. Use another browser to run the full suite." 17 | ].concat( Array.prototype.slice.call( arguments ) ) ); 18 | } 19 | 20 | /** 21 | * Conditional describe. The condition can be a boolean, or a function which returns a boolean. Pass an optional warning 22 | * after the condition, before the suite description. 23 | * 24 | * Works for Mocha and Jasmine. Syntax: 25 | * 26 | * - without warning: describeIf( !isIE(), "My suite name", function () { ... } ); 27 | * - with warning: describeIf( !isIE(), "Skipping tests in IE.", "My suite name", function () { ... } ); 28 | */ 29 | function describeIf ( condition, warning ) { 30 | _conditionalSpec( { 31 | runFunc: describe, 32 | skipFunc: describe.skip || xdescribe, 33 | invertCondition: false, 34 | defaultWarning: "Skipping tests conditionally.", 35 | args: Array.prototype.slice.call( arguments ) 36 | } ); 37 | } 38 | 39 | /** 40 | * Conditional describe. See describeIf. 41 | */ 42 | function describeUnless ( condition, warning ) { 43 | _conditionalSpec( { 44 | runFunc: describe, 45 | skipFunc: describe.skip || xdescribe, 46 | invertCondition: true, 47 | defaultWarning: "Skipping tests conditionally.", 48 | args: Array.prototype.slice.call( arguments ) 49 | } ); 50 | } 51 | 52 | /** 53 | * Conditional spec. The condition can be a boolean, or a function which returns a boolean. Pass an optional warning 54 | * after the condition, before the spec description. 55 | * 56 | * Works for Mocha and Jasmine. Syntax: 57 | * 58 | * - without warning: itIf( !isIE(), "My spec name", function () { ... } ); 59 | * - with warning: itIf( !isIE(), "Skipping tests in IE.", "My spec name", function () { ... } ); 60 | */ 61 | function itIf ( condition, warning ) { 62 | _conditionalSpec( { 63 | runFunc: it, 64 | skipFunc: it.skip || xit, 65 | invertCondition: false, 66 | defaultWarning: "Skipping test conditionally.", 67 | args: Array.prototype.slice.call( arguments ) 68 | } ); 69 | } 70 | 71 | /** 72 | * Conditional spec. See itIf. 73 | */ 74 | function itUnless ( condition, warning ) { 75 | _conditionalSpec( { 76 | runFunc: it, 77 | skipFunc: it.skip || xit, 78 | invertCondition: true, 79 | defaultWarning: "Skipping test conditionally.", 80 | args: Array.prototype.slice.call( arguments ) 81 | } ); 82 | } 83 | 84 | /** 85 | * @private 86 | */ 87 | function _conditionalSpec( call ) { 88 | var originalArgs = call.args, 89 | condition = originalArgs[0], 90 | hasWarning = typeof originalArgs[2] === "string", 91 | warning = hasWarning ? originalArgs[1] : call.defaultWarning, 92 | specArgs = hasWarning ? originalArgs.slice( 2 ) : originalArgs.slice( 1 ); 93 | 94 | if ( typeof condition === "function" ) condition = condition(); 95 | if ( call.invertCondition ) condition = !condition; 96 | 97 | if ( condition ) { 98 | call.runFunc.apply( undefined, specArgs ); 99 | } else { 100 | warn( warning ); 101 | call.skipFunc.apply( undefined, specArgs ); 102 | } 103 | } 104 | 105 | 106 | -------------------------------------------------------------------------------- /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/require-config.js: -------------------------------------------------------------------------------- 1 | requirejs.config( { 2 | 3 | baseUrl: '../../', 4 | 5 | paths: { 6 | "jquery-legacy-v1": "bower_components/jquery-legacy-v1/dist/jquery", 7 | "jquery-legacy-v2": "bower_components/jquery-legacy-v2/dist/jquery", 8 | "jquery-modern": "bower_components/jquery/dist/jquery", 9 | 10 | "underscore": "bower_components/underscore/underscore", 11 | "backbone": "bower_components/backbone/backbone", 12 | "backbone.radio": "bower_components/backbone.radio/build/backbone.radio", 13 | "marionette-modern": "bower_components/marionette/lib/backbone.marionette", 14 | "marionette-legacy": "bower_components/marionette-legacy/lib/backbone.marionette", 15 | 16 | "handlebars-modern": "bower_components/handlebars/handlebars", 17 | "handlebars-legacy-v2": "bower_components/handlebars-legacy-v2/handlebars", 18 | "handlebars-legacy-v3": "bower_components/handlebars-legacy-v3/handlebars", 19 | 20 | "backbone.declarative.views": "demo/bower_demo_components/backbone.declarative.views/dist/backbone.declarative.views", 21 | "marionette.handlebars": "dist/marionette.handlebars", 22 | 23 | // Pulling in precompiled templates. The templates are compiled with the '--amd' switch, e.g. with 24 | // 25 | // handlebars --amd precompiled.handlebars -f precompiled.js 26 | // 27 | // A potential gotcha when compiling: If the file extension is `.handlebars`, the extension is dropped from the 28 | // template ID in the Handlebars cache (foo.bar.handlebars becomes "foo.bar"). That does _not_ happen if the 29 | // extension is `.hbs`: foo.bar.hbs becomes "foo.bar.hbs". 30 | // 31 | // For more on using precompiled templates, see http://goo.gl/rgNG2Y 32 | // 33 | // ATTN: 34 | // 35 | // Only the templates defined here, which are loaded by Require.js, are compiled with the '--amd' switch. 36 | // Compiled templates which are lazy-loaded later on, by the client code in main.js, must be compiled without it! 37 | // 38 | // A lazy loader implementation for Marionette.Handlebars must be synchronous. Hence, A(synchronous)MD does not 39 | // work for it. Granted, the template code could still be injected synchronously, with a synchronous AJAX call 40 | // (ok so far). But the actual template would be wrapped in a `define` function, which executes async - too late 41 | // for the loader. 42 | // 43 | // Compiling them without the '--amd' switch gets rid of the `define` wrapper. But it creates another problem: 44 | // Now the templates depend on a `Handlebars` global. Because Handlebars is loaded by Require.js, that global is 45 | // missing. The client code (in main.js) must expose Handlebars before compiled templates are lazy-loaded. A 46 | // simple `window.Handlebars = Handlebars;` statement does the trick. 47 | 48 | "precompiled.templates-legacy": "demo/amd/templates/precompiled/legacy/amd/precompiled", 49 | "precompiled.templates-modern": "demo/amd/templates/precompiled/modern/amd/precompiled", 50 | 51 | "local.main": "demo/amd/main" 52 | }, 53 | 54 | map: { 55 | "*": { 56 | // Picking the versions of jQuery and Marionette. 57 | "jquery": "jquery-modern", 58 | "marionette": "marionette-modern", 59 | 60 | // Picking the Handlebars version. ATTN: 61 | // - The handlebars.runtime mapping, below, must point to the same version. 62 | // - The templates mapping, also below, must be changed accordingly, too. 63 | "handlebars": "handlebars-modern", 64 | 65 | // Templates precompiled with the --amd switch require 'handlebars.runtime' rather than 'handlebars'. As we 66 | // don't use the runtime here, we need to map 'handlebars' to a 'handlebars.runtime' alias. 67 | "handlebars.runtime": "handlebars-modern", 68 | 69 | // Legacy templates, "precompiled.templates-legacy", work for Handlebars <=3. For Handlebars 4, map to 70 | // "precompiled.templates-modern". 71 | "precompiled.templates": "precompiled.templates-modern" 72 | } 73 | }, 74 | 75 | shim: { 76 | "jquery-legacy-v1": { 77 | exports: "jQuery" 78 | }, 79 | "jquery-legacy-v2": { 80 | exports: "jQuery" 81 | }, 82 | "jquery-modern": { 83 | exports: "jQuery" 84 | }, 85 | "handlebars-legacy-v2": { 86 | exports: "Handlebars" 87 | }, 88 | "handlebars-legacy-v3": { 89 | exports: "Handlebars" 90 | }, 91 | "handlebars-modern": { 92 | exports: "Handlebars" 93 | }, 94 | 95 | "marionette": ["backbone.declarative.views"] 96 | } 97 | 98 | } ); 99 | -------------------------------------------------------------------------------- /demo/amd/main.js: -------------------------------------------------------------------------------- 1 | // main.js 2 | 3 | require( [ 4 | 5 | 'jquery', 6 | 'underscore', 7 | 'backbone', 8 | 'handlebars', 9 | 'marionette', 10 | 'backbone.declarative.views', 11 | 'precompiled.templates', 12 | 'marionette.handlebars' 13 | 14 | ], function ( $, _, Backbone, Handlebars ) { 15 | 16 | var domView, precompiledView, lazyLoadedView, lazyLoadedPrecompiledView, 17 | 18 | Marionette = Backbone.Marionette, 19 | $container = $( ".content" ), 20 | 21 | MarionetteView = Marionette.ItemView || Marionette.View, 22 | BaseView = MarionetteView.extend( { 23 | tagName: "p" 24 | } ); 25 | 26 | // Allow lazy loading of compiled templates 27 | Backbone.Marionette.TemplateCache.allowCompiledTemplatesOverHttp = true; 28 | 29 | // Expose Handlebars as a browser global. Makes lazy-loading of *compiled* templates possible. 30 | // 31 | // See explanation in require-config.js. By contrast, raw HTML templates (ie strings, not code) can be lazy-loaded 32 | // without a global. 33 | window.Handlebars = Handlebars; 34 | 35 | // Implement a lazy template loader. 36 | _.extend( Backbone.Marionette.TemplateCache.prototype, { 37 | 38 | isPrecompiled: function ( templateId ) { 39 | return templateId.substr( -3 ) === ".js"; 40 | }, 41 | 42 | getTemplateUrl: function ( templateId, options ) { 43 | var isPrecompiled = this.isPrecompiled( templateId ), 44 | versionInfix = isLegacyHandlebars() ? "legacy" : "modern", 45 | prefix = isPrecompiled ? "templates/precompiled/" + versionInfix + "/non-amd/" : "templates/raw/", 46 | suffix = isPrecompiled ? "" : ".hbs"; 47 | 48 | return prefix + templateId + suffix; 49 | }, 50 | 51 | lazyLoadTemplate: function ( templateId, options ) { 52 | var templateHtml, compiledTemplate, 53 | isPrecompiled = this.isPrecompiled( templateId ), 54 | templateUrl = this.getTemplateUrl( templateId, options ); 55 | 56 | if ( isPrecompiled ) { 57 | 58 | this.loadResource( { url: templateUrl, isJavascript: true } ); 59 | 60 | // The $.ajax call returns a precompiled template as a string, not as Javascript code. We simply throw 61 | // the string away. 62 | // 63 | // But the code has also been executed, which means that it has been added to the Handlebars cache. We 64 | // must read it from the cache now. 65 | // 66 | // Our template IDs for precompiled templates end in ".js" because we needed to fetch the actual files. 67 | // In the Handlebars cache, the ".js" file extension not part of the ID. We need to remove it before 68 | // querying the cache. 69 | templateId = templateId.slice( 0, -3 ); 70 | compiledTemplate = this.getPrecompiledTemplate( templateId ); 71 | 72 | } else { 73 | 74 | // Loading a raw HTML template (ie, a string). 75 | templateHtml = this.loadResource( { url: templateUrl, isJavascript: false } ); 76 | 77 | } 78 | 79 | return templateHtml || compiledTemplate; 80 | }, 81 | 82 | loadResource: function ( config ) { 83 | var content; 84 | 85 | Backbone.$.ajax( { 86 | url: config.url, 87 | success: function ( data ) { content = data; }, 88 | 89 | async: false, 90 | cache: true, 91 | dataType: config.isJavascript ? "script" : "text" 92 | } ); 93 | 94 | return content; 95 | } 96 | 97 | } ); 98 | 99 | // Load templates in various ways 100 | 101 | // Load a template from the DOM. 102 | // 103 | // The el is defined by the template in this case, using Backbone.Declarative.Views. Hence, we use a plain ItemView 104 | // (or View, in Marionette 3), rather than the local BaseView which also defines the el. 105 | domView = new MarionetteView( { 106 | model: new Backbone.Model( { origin: "DOM-based" } ), 107 | template: "#dom-template" 108 | } ); 109 | 110 | // Load a precompiled template 111 | precompiledView = new BaseView( { 112 | model: new Backbone.Model( { origin: "precompiled" } ), 113 | template: "precompiled" 114 | } ); 115 | 116 | // Lazy-load a template 117 | lazyLoadedView= new BaseView( { 118 | model: new Backbone.Model( { origin: "lazy-loaded" } ), 119 | template: "lazy-loaded" 120 | } ); 121 | 122 | // Lazy-load a template asynchronously 123 | createViewWithAsyncTemplate( { 124 | ViewClass: BaseView, 125 | model: new Backbone.Model( { origin: "lazy-loaded" } ), 126 | templateId: "lazy-loaded-async" 127 | } ); 128 | 129 | // Lazy-load a precompiled template 130 | lazyLoadedPrecompiledView = new BaseView( { 131 | model: new Backbone.Model( { origin: "lazy-loaded" } ), 132 | template: "lazy-loaded-precompiled.js" 133 | } ); 134 | 135 | // Lazy-load a precompiled template async 136 | createViewWithAsyncTemplate( { 137 | ViewClass: BaseView, 138 | model: new Backbone.Model( { origin: "lazy-loaded" } ), 139 | templateId: "lazy-loaded-precompiled-async.js" 140 | } ); 141 | 142 | // Show the synchronous views (the async ones have been handled inside createViewWithAsyncTemplate()). 143 | addHeadline( "Preloaded" ); 144 | show( domView ); 145 | show( precompiledView ); 146 | addHeadline( "Lazy-loaded" ); 147 | show( lazyLoadedView ); 148 | show( lazyLoadedPrecompiledView ); 149 | addHeadline( "Async lazy-loaded" ); 150 | 151 | function addHeadline ( text ) { 152 | $( "

" ).text( text ).wrapInner( "").appendTo( $container ); 153 | } 154 | 155 | function show ( view ) { 156 | view.render(); 157 | view.$el.appendTo( $container ); 158 | } 159 | 160 | function preloadTemplate ( templateId, deferred ) { 161 | Marionette.TemplateCache.get( templateId ); 162 | deferred.resolve(); 163 | } 164 | 165 | function createViewWithAsyncTemplate ( config ) { 166 | // Preload the template before using it in a view. Do it async. Delay the creation of the view until the 167 | // template has arrived in the cache. 168 | // 169 | // The templateLoaded promise triggers view creation. The helper function preloadTemplate() receives the promise 170 | // and resolves it when the template is ready. 171 | var templateLoaded = new Backbone.$.Deferred( function ( deferred ) { 172 | setTimeout( _.partial( preloadTemplate, config.templateId, deferred ), 0 ); 173 | } ); 174 | 175 | templateLoaded.done( function () { 176 | 177 | var view = new config.ViewClass( { 178 | model: config.model, 179 | template: config.templateId 180 | } ); 181 | 182 | show( view ); 183 | } ); 184 | } 185 | 186 | function getHandlebarsVersion () { 187 | return +Handlebars.VERSION[0] 188 | } 189 | 190 | function isLegacyHandlebars () { 191 | return getHandlebarsVersion() < 4; 192 | } 193 | 194 | } ); 195 | -------------------------------------------------------------------------------- /demo/amd/rjs/output/parts/app.js: -------------------------------------------------------------------------------- 1 | define('precompiled.templates-modern',['handlebars.runtime'], function(Handlebars) { 2 | Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 3 | return templates['precompiled'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 4 | var helper; 5 | 6 | return "This paragraph is served with a " 7 | + container.escapeExpression(((helper = (helper = helpers.origin || (depth0 != null ? depth0.origin : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"origin","hash":{},"data":data}) : helper))) 8 | + " template."; 9 | },"useData":true}); 10 | }); 11 | // main.js 12 | 13 | require( [ 14 | 15 | 'jquery', 16 | 'underscore', 17 | 'backbone', 18 | 'handlebars', 19 | 'marionette', 20 | 'backbone.declarative.views', 21 | 'precompiled.templates', 22 | 'marionette.handlebars' 23 | 24 | ], function ( $, _, Backbone, Handlebars ) { 25 | 26 | var domView, precompiledView, lazyLoadedView, lazyLoadedPrecompiledView, 27 | 28 | Marionette = Backbone.Marionette, 29 | $container = $( ".content" ), 30 | 31 | MarionetteView = Marionette.ItemView || Marionette.View, 32 | BaseView = MarionetteView.extend( { 33 | tagName: "p" 34 | } ); 35 | 36 | // Allow lazy loading of compiled templates 37 | Backbone.Marionette.TemplateCache.allowCompiledTemplatesOverHttp = true; 38 | 39 | // Expose Handlebars as a browser global. Makes lazy-loading of *compiled* templates possible. 40 | // 41 | // See explanation in require-config.js. By contrast, raw HTML templates (ie strings, not code) can be lazy-loaded 42 | // without a global. 43 | window.Handlebars = Handlebars; 44 | 45 | // Implement a lazy template loader. 46 | _.extend( Backbone.Marionette.TemplateCache.prototype, { 47 | 48 | isPrecompiled: function ( templateId ) { 49 | return templateId.substr( -3 ) === ".js"; 50 | }, 51 | 52 | getTemplateUrl: function ( templateId, options ) { 53 | var isPrecompiled = this.isPrecompiled( templateId ), 54 | versionInfix = isLegacyHandlebars() ? "legacy" : "modern", 55 | prefix = isPrecompiled ? "templates/precompiled/" + versionInfix + "/non-amd/" : "templates/raw/", 56 | suffix = isPrecompiled ? "" : ".hbs"; 57 | 58 | return prefix + templateId + suffix; 59 | }, 60 | 61 | lazyLoadTemplate: function ( templateId, options ) { 62 | var templateHtml, compiledTemplate, 63 | isPrecompiled = this.isPrecompiled( templateId ), 64 | templateUrl = this.getTemplateUrl( templateId, options ); 65 | 66 | if ( isPrecompiled ) { 67 | 68 | this.loadResource( { url: templateUrl, isJavascript: true } ); 69 | 70 | // The $.ajax call returns a precompiled template as a string, not as Javascript code. We simply throw 71 | // the string away. 72 | // 73 | // But the code has also been executed, which means that it has been added to the Handlebars cache. We 74 | // must read it from the cache now. 75 | // 76 | // Our template IDs for precompiled templates end in ".js" because we needed to fetch the actual files. 77 | // In the Handlebars cache, the ".js" file extension not part of the ID. We need to remove it before 78 | // querying the cache. 79 | templateId = templateId.slice( 0, -3 ); 80 | compiledTemplate = this.getPrecompiledTemplate( templateId ); 81 | 82 | } else { 83 | 84 | // Loading a raw HTML template (ie, a string). 85 | templateHtml = this.loadResource( { url: templateUrl, isJavascript: false } ); 86 | 87 | } 88 | 89 | return templateHtml || compiledTemplate; 90 | }, 91 | 92 | loadResource: function ( config ) { 93 | var content; 94 | 95 | Backbone.$.ajax( { 96 | url: config.url, 97 | success: function ( data ) { content = data; }, 98 | 99 | async: false, 100 | cache: true, 101 | dataType: config.isJavascript ? "script" : "text" 102 | } ); 103 | 104 | return content; 105 | } 106 | 107 | } ); 108 | 109 | // Load templates in various ways 110 | 111 | // Load a template from the DOM. 112 | // 113 | // The el is defined by the template in this case, using Backbone.Declarative.Views. Hence, we use a plain ItemView 114 | // (or View, in Marionette 3), rather than the local BaseView which also defines the el. 115 | domView = new MarionetteView( { 116 | model: new Backbone.Model( { origin: "DOM-based" } ), 117 | template: "#dom-template" 118 | } ); 119 | 120 | // Load a precompiled template 121 | precompiledView = new BaseView( { 122 | model: new Backbone.Model( { origin: "precompiled" } ), 123 | template: "precompiled" 124 | } ); 125 | 126 | // Lazy-load a template 127 | lazyLoadedView= new BaseView( { 128 | model: new Backbone.Model( { origin: "lazy-loaded" } ), 129 | template: "lazy-loaded" 130 | } ); 131 | 132 | // Lazy-load a template asynchronously 133 | createViewWithAsyncTemplate( { 134 | ViewClass: BaseView, 135 | model: new Backbone.Model( { origin: "lazy-loaded" } ), 136 | templateId: "lazy-loaded-async" 137 | } ); 138 | 139 | // Lazy-load a precompiled template 140 | lazyLoadedPrecompiledView = new BaseView( { 141 | model: new Backbone.Model( { origin: "lazy-loaded" } ), 142 | template: "lazy-loaded-precompiled.js" 143 | } ); 144 | 145 | // Lazy-load a precompiled template async 146 | createViewWithAsyncTemplate( { 147 | ViewClass: BaseView, 148 | model: new Backbone.Model( { origin: "lazy-loaded" } ), 149 | templateId: "lazy-loaded-precompiled-async.js" 150 | } ); 151 | 152 | // Show the synchronous views (the async ones have been handled inside createViewWithAsyncTemplate()). 153 | addHeadline( "Preloaded" ); 154 | show( domView ); 155 | show( precompiledView ); 156 | addHeadline( "Lazy-loaded" ); 157 | show( lazyLoadedView ); 158 | show( lazyLoadedPrecompiledView ); 159 | addHeadline( "Async lazy-loaded" ); 160 | 161 | function addHeadline ( text ) { 162 | $( "

" ).text( text ).wrapInner( "").appendTo( $container ); 163 | } 164 | 165 | function show ( view ) { 166 | view.render(); 167 | view.$el.appendTo( $container ); 168 | } 169 | 170 | function preloadTemplate ( templateId, deferred ) { 171 | Marionette.TemplateCache.get( templateId ); 172 | deferred.resolve(); 173 | } 174 | 175 | function createViewWithAsyncTemplate ( config ) { 176 | // Preload the template before using it in a view. Do it async. Delay the creation of the view until the 177 | // template has arrived in the cache. 178 | // 179 | // The templateLoaded promise triggers view creation. The helper function preloadTemplate() receives the promise 180 | // and resolves it when the template is ready. 181 | var templateLoaded = new Backbone.$.Deferred( function ( deferred ) { 182 | setTimeout( _.partial( preloadTemplate, config.templateId, deferred ), 0 ); 183 | } ); 184 | 185 | templateLoaded.done( function () { 186 | 187 | var view = new config.ViewClass( { 188 | model: config.model, 189 | template: config.templateId 190 | } ); 191 | 192 | show( view ); 193 | } ); 194 | } 195 | 196 | function getHandlebarsVersion () { 197 | return +Handlebars.VERSION[0] 198 | } 199 | 200 | function isLegacyHandlebars () { 201 | return getHandlebarsVersion() < 4; 202 | } 203 | 204 | } ); 205 | 206 | define("local.main", function(){}); 207 | 208 | -------------------------------------------------------------------------------- /src/marionette.handlebars.js: -------------------------------------------------------------------------------- 1 | ;( function ( root, factory ) { 2 | "use strict"; 3 | 4 | // UMD for a Marionette plugin. Supports AMD, Node.js, CommonJS and globals. 5 | // 6 | // - Code lives in the Marionette namespace. 7 | // - The module does not export a meaningful value. 8 | // - The module does not create a global. 9 | 10 | var supportsExports = typeof exports === "object" && exports && !exports.nodeType && typeof module === "object" && module && !module.nodeType; 11 | 12 | // AMD: 13 | // - Some AMD build optimizers like r.js check for condition patterns like the AMD check below, so keep it as is. 14 | // - Check for `exports` after `define` in case a build optimizer adds an `exports` object. 15 | // - The AMD spec requires the dependencies to be an array **literal** of module IDs. Don't use a variable there, 16 | // or optimizers may fail. 17 | if ( typeof define === "function" && typeof define.amd === "object" && define.amd ) { 18 | 19 | // AMD module 20 | define( [ "exports", "underscore", "backbone", "handlebars", "marionette" ], factory ); 21 | 22 | } else if ( supportsExports ) { 23 | 24 | // Node module, CommonJS module 25 | factory( exports, require( "underscore" ), require( "backbone" ), require( "handlebars" ), require( "marionette" ) ); 26 | 27 | } else { 28 | 29 | // Global (browser or Rhino) 30 | factory( {}, _, Backbone, Handlebars ); 31 | 32 | } 33 | 34 | }( this, function ( exports, _, Backbone, Handlebars ) { 35 | "use strict"; 36 | 37 | var origLoadTemplate, 38 | Marionette = Backbone.Marionette; 39 | 40 | if ( ! Marionette ) throw new Error( "Load error: Backbone.Marionette is not available" ); 41 | 42 | origLoadTemplate = Marionette.TemplateCache.prototype.loadTemplate; 43 | 44 | /** @type {boolean} flag allowing the lazy loading of compiled templates */ 45 | Marionette.TemplateCache.allowCompiledTemplatesOverHttp = false; 46 | 47 | Marionette.TemplateCache.MarionetteHandlebarsVersion = "__COMPONENT_VERSION_PLACEHOLDER__"; 48 | 49 | _.extend( Marionette.TemplateCache.prototype, { 50 | 51 | /** 52 | * Loads and returns a template from the Handlebars cache, the DOM, or the server (if set up to do so). Throws 53 | * an error if the template can't be found. 54 | * 55 | * Unlike the original Marionette implementation, this version of loadTemplate does not always return the raw 56 | * template HTML. If the template is fetched from the Handlebars cache of precompiled templates, it is returned 57 | * as is, ie as a compiled template function. 58 | * 59 | * That does not cause issues in Marionette.TemplateCache, as long as the compileTemplate function can handle it 60 | * correctly. (And here, it obviously does.) 61 | * 62 | * If a template can't be found in either the Handlebars cache or the DOM, the job is passed on to the 63 | * lazyLoadTemplate() method. It is a noop by default. To use an actual loader and make it work for your needs, 64 | * assign your own implementation to Marionette.TemplateCache.prototype.lazyLoadTemplate. 65 | * 66 | * @param {string} templateId a selector, usually, or the file ID if the Handlebars cache is used 67 | * @param {Object|undefined} options 68 | * @returns {string|Function} 69 | */ 70 | loadTemplate: function ( templateId, options ) { 71 | var templateHtml, 72 | precompiledTemplate = this.getPrecompiledTemplate( templateId ); 73 | 74 | if ( ! precompiledTemplate || ! _.isFunction( precompiledTemplate ) ) { 75 | try { 76 | templateHtml = origLoadTemplate.call( this, templateId, options ); 77 | } catch ( err ) {} 78 | 79 | if ( ! isValidTemplateHtml( templateHtml ) ) templateHtml = this.lazyLoadTemplate( templateId, options ); 80 | 81 | // Throw an error if the template is missing, just like the original implementation. 82 | if ( ! isValidTemplateReturnValue( templateHtml ) ) this.throwTemplateError( templateId ); 83 | } 84 | 85 | return templateHtml || precompiledTemplate; 86 | }, 87 | 88 | /** 89 | * Returns the compiled template. 90 | * 91 | * Unlike the original Marionette implementation, the method does not just accept a raw template HTML string as 92 | * its first argument, but an existing precompiled template as well. Such a template function is simply returned 93 | * as it is. 94 | * 95 | * @param {string|Function} template 96 | * @param {Object|undefined} options 97 | * @returns {Function} 98 | */ 99 | compileTemplate: function ( template, options ) { 100 | return _.isFunction( template ) ? template : Handlebars.compile( template, options ); 101 | }, 102 | 103 | /** 104 | * Returns the precompiled Handlebars template for a given template ID, if it exists. 105 | * 106 | * NB In this case, the template ID is not a selector, but derived from the file name of the original template. 107 | * See http://handlebarsjs.com/precompilation.html 108 | * 109 | * Override it if you have to perform some special magic for matching a Marionette templateId to the templateId 110 | * of the Handlebars cache. 111 | * 112 | * @param {string} templateId 113 | * @returns {Function|undefined} 114 | */ 115 | getPrecompiledTemplate: function ( templateId ) { 116 | return Handlebars.templates && Handlebars.templates[templateId]; 117 | }, 118 | 119 | /** 120 | * Lazy-load the template. 121 | * 122 | * Noop by default. Provide your own loader by implementing this method. It must return the templateHtml if 123 | * successful, or undefined otherwise. And it MUST NOT be async (set async: false in an $.ajax() call). 124 | * 125 | * @param {string} templateId 126 | * @param {Object|undefined} options 127 | * @returns {string|undefined} 128 | */ 129 | lazyLoadTemplate: function ( templateId, options ) { 130 | return undefined; 131 | }, 132 | 133 | /** 134 | * Throws a NoTemplateError in a way which is compatible with any Marionette version. 135 | * 136 | * @param {string} templateId 137 | */ 138 | throwTemplateError: function ( templateId ) { 139 | 140 | var errType = 'NoTemplateError', 141 | errMsg = 'Could not load template: "' + templateId + '". It does not exist, is of an illegal type, or has content which cannot be processed.'; 142 | 143 | if ( Marionette.Error ) { 144 | // Error handling in Marionette 2.x 145 | throw new Marionette.Error( { name: errType, message: errMsg } ); 146 | } else if ( typeof throwError === "function" ) { 147 | // Error handling in Marionette 1.x 148 | throwError( errMsg, errType ); // jshint ignore:line 149 | } else { 150 | // Being future proof, we throw our own errors if all else has failed 151 | throw new Error( errMsg ); 152 | } 153 | 154 | } 155 | 156 | } ); 157 | 158 | /** 159 | * Checks if the template data is a non-empty string. 160 | * 161 | * @param {*} templateData 162 | * @returns {boolean} 163 | */ 164 | function isValidTemplateHtml ( templateData ) { 165 | return _.isString( templateData ) && templateData.length > 0; 166 | } 167 | 168 | /** 169 | * Checks if the template data is a valid return value for loadTemplate(). 170 | * 171 | * - A non-empty string always passes the test. This is the format of raw template HTML. 172 | * - A function may or may not be acceptable, depending on the allowCompiledTemplatesOverHttp flag. 173 | * 174 | * @param {*} templateData 175 | * @returns {boolean} 176 | */ 177 | function isValidTemplateReturnValue ( templateData ) { 178 | return isValidTemplateHtml( templateData ) || Marionette.TemplateCache.allowCompiledTemplatesOverHttp && _.isFunction( templateData ); 179 | } 180 | 181 | 182 | // Module return value 183 | // ------------------- 184 | // 185 | // A return value may be necessary for AMD to detect that the module is loaded. It ony exists for that reason and is 186 | // purely symbolic. Don't use it in client code. The functionality of this module lives in the Backbone namespace. 187 | exports.info = "Marionette.Handlebars has loaded. Don't use the exported value of the module. Its functionality is available inside the Backbone namespace."; 188 | 189 | } ) ); 190 | -------------------------------------------------------------------------------- /dist/marionette.handlebars.js: -------------------------------------------------------------------------------- 1 | // Marionette.Handlebars, v2.0.0 2 | // Copyright (c) 2015-2016 Michael Heim, Zeilenwechsel.de 3 | // Distributed under MIT license 4 | // http://github.com/hashchange/marionette.handlebars 5 | 6 | ;( function ( root, factory ) { 7 | "use strict"; 8 | 9 | // UMD for a Marionette plugin. Supports AMD, Node.js, CommonJS and globals. 10 | // 11 | // - Code lives in the Marionette namespace. 12 | // - The module does not export a meaningful value. 13 | // - The module does not create a global. 14 | 15 | var supportsExports = typeof exports === "object" && exports && !exports.nodeType && typeof module === "object" && module && !module.nodeType; 16 | 17 | // AMD: 18 | // - Some AMD build optimizers like r.js check for condition patterns like the AMD check below, so keep it as is. 19 | // - Check for `exports` after `define` in case a build optimizer adds an `exports` object. 20 | // - The AMD spec requires the dependencies to be an array **literal** of module IDs. Don't use a variable there, 21 | // or optimizers may fail. 22 | if ( typeof define === "function" && typeof define.amd === "object" && define.amd ) { 23 | 24 | // AMD module 25 | define( [ "exports", "underscore", "backbone", "handlebars", "marionette" ], factory ); 26 | 27 | } else if ( supportsExports ) { 28 | 29 | // Node module, CommonJS module 30 | factory( exports, require( "underscore" ), require( "backbone" ), require( "handlebars" ), require( "marionette" ) ); 31 | 32 | } else { 33 | 34 | // Global (browser or Rhino) 35 | factory( {}, _, Backbone, Handlebars ); 36 | 37 | } 38 | 39 | }( this, function ( exports, _, Backbone, Handlebars ) { 40 | "use strict"; 41 | 42 | var origLoadTemplate, 43 | Marionette = Backbone.Marionette; 44 | 45 | if ( ! Marionette ) throw new Error( "Load error: Backbone.Marionette is not available" ); 46 | 47 | origLoadTemplate = Marionette.TemplateCache.prototype.loadTemplate; 48 | 49 | /** @type {boolean} flag allowing the lazy loading of compiled templates */ 50 | Marionette.TemplateCache.allowCompiledTemplatesOverHttp = false; 51 | 52 | Marionette.TemplateCache.MarionetteHandlebarsVersion = "2.0.0"; 53 | 54 | _.extend( Marionette.TemplateCache.prototype, { 55 | 56 | /** 57 | * Loads and returns a template from the Handlebars cache, the DOM, or the server (if set up to do so). Throws 58 | * an error if the template can't be found. 59 | * 60 | * Unlike the original Marionette implementation, this version of loadTemplate does not always return the raw 61 | * template HTML. If the template is fetched from the Handlebars cache of precompiled templates, it is returned 62 | * as is, ie as a compiled template function. 63 | * 64 | * That does not cause issues in Marionette.TemplateCache, as long as the compileTemplate function can handle it 65 | * correctly. (And here, it obviously does.) 66 | * 67 | * If a template can't be found in either the Handlebars cache or the DOM, the job is passed on to the 68 | * lazyLoadTemplate() method. It is a noop by default. To use an actual loader and make it work for your needs, 69 | * assign your own implementation to Marionette.TemplateCache.prototype.lazyLoadTemplate. 70 | * 71 | * @param {string} templateId a selector, usually, or the file ID if the Handlebars cache is used 72 | * @param {Object|undefined} options 73 | * @returns {string|Function} 74 | */ 75 | loadTemplate: function ( templateId, options ) { 76 | var templateHtml, 77 | precompiledTemplate = this.getPrecompiledTemplate( templateId ); 78 | 79 | if ( ! precompiledTemplate || ! _.isFunction( precompiledTemplate ) ) { 80 | try { 81 | templateHtml = origLoadTemplate.call( this, templateId, options ); 82 | } catch ( err ) {} 83 | 84 | if ( ! isValidTemplateHtml( templateHtml ) ) templateHtml = this.lazyLoadTemplate( templateId, options ); 85 | 86 | // Throw an error if the template is missing, just like the original implementation. 87 | if ( ! isValidTemplateReturnValue( templateHtml ) ) this.throwTemplateError( templateId ); 88 | } 89 | 90 | return templateHtml || precompiledTemplate; 91 | }, 92 | 93 | /** 94 | * Returns the compiled template. 95 | * 96 | * Unlike the original Marionette implementation, the method does not just accept a raw template HTML string as 97 | * its first argument, but an existing precompiled template as well. Such a template function is simply returned 98 | * as it is. 99 | * 100 | * @param {string|Function} template 101 | * @param {Object|undefined} options 102 | * @returns {Function} 103 | */ 104 | compileTemplate: function ( template, options ) { 105 | return _.isFunction( template ) ? template : Handlebars.compile( template, options ); 106 | }, 107 | 108 | /** 109 | * Returns the precompiled Handlebars template for a given template ID, if it exists. 110 | * 111 | * NB In this case, the template ID is not a selector, but derived from the file name of the original template. 112 | * See http://handlebarsjs.com/precompilation.html 113 | * 114 | * Override it if you have to perform some special magic for matching a Marionette templateId to the templateId 115 | * of the Handlebars cache. 116 | * 117 | * @param {string} templateId 118 | * @returns {Function|undefined} 119 | */ 120 | getPrecompiledTemplate: function ( templateId ) { 121 | return Handlebars.templates && Handlebars.templates[templateId]; 122 | }, 123 | 124 | /** 125 | * Lazy-load the template. 126 | * 127 | * Noop by default. Provide your own loader by implementing this method. It must return the templateHtml if 128 | * successful, or undefined otherwise. And it MUST NOT be async (set async: false in an $.ajax() call). 129 | * 130 | * @param {string} templateId 131 | * @param {Object|undefined} options 132 | * @returns {string|undefined} 133 | */ 134 | lazyLoadTemplate: function ( templateId, options ) { 135 | return undefined; 136 | }, 137 | 138 | /** 139 | * Throws a NoTemplateError in a way which is compatible with any Marionette version. 140 | * 141 | * @param {string} templateId 142 | */ 143 | throwTemplateError: function ( templateId ) { 144 | 145 | var errType = 'NoTemplateError', 146 | errMsg = 'Could not load template: "' + templateId + '". It does not exist, is of an illegal type, or has content which cannot be processed.'; 147 | 148 | if ( Marionette.Error ) { 149 | // Error handling in Marionette 2.x 150 | throw new Marionette.Error( { name: errType, message: errMsg } ); 151 | } else if ( typeof throwError === "function" ) { 152 | // Error handling in Marionette 1.x 153 | throwError( errMsg, errType ); // jshint ignore:line 154 | } else { 155 | // Being future proof, we throw our own errors if all else has failed 156 | throw new Error( errMsg ); 157 | } 158 | 159 | } 160 | 161 | } ); 162 | 163 | /** 164 | * Checks if the template data is a non-empty string. 165 | * 166 | * @param {*} templateData 167 | * @returns {boolean} 168 | */ 169 | function isValidTemplateHtml ( templateData ) { 170 | return _.isString( templateData ) && templateData.length > 0; 171 | } 172 | 173 | /** 174 | * Checks if the template data is a valid return value for loadTemplate(). 175 | * 176 | * - A non-empty string always passes the test. This is the format of raw template HTML. 177 | * - A function may or may not be acceptable, depending on the allowCompiledTemplatesOverHttp flag. 178 | * 179 | * @param {*} templateData 180 | * @returns {boolean} 181 | */ 182 | function isValidTemplateReturnValue ( templateData ) { 183 | return isValidTemplateHtml( templateData ) || Marionette.TemplateCache.allowCompiledTemplatesOverHttp && _.isFunction( templateData ); 184 | } 185 | 186 | 187 | // Module return value 188 | // ------------------- 189 | // 190 | // A return value may be necessary for AMD to detect that the module is loaded. It ony exists for that reason and is 191 | // purely symbolic. Don't use it in client code. The functionality of this module lives in the Backbone namespace. 192 | exports.info = "Marionette.Handlebars has loaded. Don't use the exported value of the module. Its functionality is available inside the Backbone namespace."; 193 | 194 | } ) ); 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Marionette.Handlebars 2 | 3 | Marionette.Handlebars does exactly what its name suggests: it adds support for Handlebars and Mustache templates to Marionette. 4 | 5 | Marionette.Handlebars supports [precompiled templates][hlb-precompiled] as well. It does its job entirely behind the scenes – load it, and you are all set. 6 | 7 | There really isn't much in terms of an API – nothing, in fact, except for an extension point in case you want to [lazy-load some templates][lazy-loading]. 8 | 9 | ## Dependencies and setup 10 | 11 | Marionette.Handlebars, somewhat unsurprisingly, depends on the Marionette stack ([Underscore][], [Backbone][], [Marionette][]) and [Handlebars][]. Include marionette.handlebars.js after those are loaded. 12 | 13 | When loaded as a module (e.g. AMD, Node), Marionette.Handlebars does not export a meaningful value. It solely lives in the Marionette namespace. 14 | 15 | The stable version of Marionette.Handlebars is available in the `dist` directory ([dev][dist-dev], [prod][dist-prod]). If you use Bower, fetch the files with `bower install marionette.handlebars`. With npm, it is `npm install marionette.handlebars`. 16 | 17 | ## Precompiled templates 18 | 19 | If you have [precompiled your templates][hlb-precompiled], Marionette.Handlebars retrieves them from the Handlebars cache. You just need to set the `template` property of a Marionette view to the ID of the compiled template (derived from the name of the template file). 20 | 21 | ## Lazy loading of templates 22 | 23 | If you want to lazy-load some of your templates, you must implement a loader which works for your requirements – such as your URL scheme, for instance. Marionette.Handlebars just provides an extension point for you. 24 | 25 | A word of caution: Loading templates on demand is _terrible_ for the performance of an application. The additional overhead of an HTTP request, particularly on mobile, is way beyond acceptable levels for UI elements which the user is already waiting for. Lazy template loading can make sense, though, for elements which are pre-rendered, but not yet exposed to the user. Use it judiciously. 26 | 27 | ### How to implement a loader 28 | 29 | You need to override `Marionette.TemplateCache.prototype.lazyLoadTemplate`. Out of the box, the method doesn't do anything. Replace it with your own implementation. Here is what you need to know. 30 | 31 | - Your loader is called with the template ID as the first argument. 32 | 33 | The template ID is the value of the `template` property, or `template` constructor option, in your Marionette views. The template ID _would_ be a selector if your templates were read from the DOM. But because you provide your own loader, it can be any string you choose. 34 | 35 | - Your loader also receives an options argument which is passed around by Marionette. 36 | 37 | That argument [can initially be provided to][Marionette.TemplateCache-basic-usage] `Marionette.TemplateCache.get()`, but is usually `undefined`. Have a look at the few bits of information [in the Marionette documentation][Marionette.TemplateCache-basic-usage] to get started. 38 | 39 | Overall, though, the `options` argument is not very useful. Marionette views never pass it in when requesting a template. At a minimum, you'd have to override `Marionette.Renderer.render()` to make it work at all. 40 | 41 | - The loader must return the raw template HTML if successful, or `undefined` if it fails to fetch the template. 42 | 43 | If you want to fetch and return compiled templates, you need to [enable it explicitly][lazy-load-compiled-templates]. 44 | 45 | - Your loader **must not** be async. 46 | 47 | Yes, synchronous loading is terribly inefficient. But asynchronous template loading is beyond what a generic Handlebars integration can provide to Marionette views. 48 | 49 | That said, you can certainly fill in the blanks. If the templates are lazy-loaded asynchronously, then the views must be rendered async as well. Also, there should probably be a mechanism to prevent multiple, simultaneous requests for the same template URL. (I have seen [Traffic Cop][traffic-cop] being [mentioned][los-techies-traffic-cop] as a helpful tool, but it seems abandoned. [More here.][traffic-cop-docs]) 50 | 51 | A _very_ basic implementation of async loading can be seen [in the AMD demo][amd-demo-async-loading] of Marionette.Handlebars. 52 | 53 | Please be aware that the lazy loader is only called as a last resort. The search for a matching template begins in the Handlebars cache of precompiled templates, then moves on to the DOM. Only if the template is not found in any of these places, the lazy loader gets its turn. 54 | 55 | ### Allowing compiled templates to be lazy-loaded 56 | 57 | For security reasons, you cannot lazy-load compiled templates, ie executable Javascript code. If you want to go down that route, you must explicitly enable it: 58 | 59 | ```javascript 60 | Marionette.TemplateCache.allowCompiledTemplatesOverHttp = true; 61 | ``` 62 | 63 | This is a global setting. The `lazyLoadTemplate()` method is allowed to return a function then, in addition to strings. 64 | 65 | ### An example 66 | 67 | A basic loader might look like this: 68 | 69 | ```javascript 70 | Marionette.TemplateCache.prototype.lazyLoadTemplate = function ( templateId, options ) { 71 | var templateHtml, 72 | templateUrl = "templates/" + templateId + ".hbs"; 73 | 74 | Backbone.$.ajax( { 75 | url: templateUrl, 76 | success: function( data ) { 77 | templateHtml = data; 78 | }, 79 | async: false 80 | } ); 81 | 82 | return templateHtml; 83 | }; 84 | ``` 85 | 86 | The loader in the [AMD demo][amd-demo] might give you some inspiration, too. 87 | 88 | ## Build process and tests 89 | 90 | If you'd like to fix, customize or otherwise improve the project: here are your tools. 91 | 92 | ### Setup 93 | 94 | [npm][] sets up the environment for you. 95 | 96 | - The only thing you've got to have on your machine (besides Git) is [Node.js]. Download the installer [here][Node.js]. 97 | - Clone the project and open a command prompt in the project directory. 98 | - Run the setup with `npm run setup`. 99 | - Make sure the Grunt CLI is installed as a global Node module. If not, or if you are not sure, run `npm install -g grunt-cli` from the command prompt. 100 | 101 | Your test and build environment is ready now. If you want to test against specific versions of Backbone, edit `bower.json` first. 102 | 103 | ### Running tests, creating a new build 104 | 105 | #### Considerations for testing 106 | 107 | To run the tests on remote clients (e.g. mobile devices), start a web server with `grunt interactive` and visit `http://[your-host-ip]:9400/web-mocha/` with the client browser. Running the tests in a browser like this is slow, so it might make sense to disable the power-save/sleep/auto-lock timeout on mobile devices. Use `grunt test` (see below) for faster local testing. 108 | 109 | #### Tool chain and commands 110 | 111 | The test tool chain: [Grunt][] (task runner), [Karma][] (test runner), [Mocha][] (test framework), [Chai][] (assertion library), [Sinon][] (mocking framework). The good news: you don't need to worry about any of this. 112 | 113 | A handful of commands manage everything for you: 114 | 115 | - Run the tests in a terminal with `grunt test`. 116 | - Run the tests in a browser interactively, live-reloading the page when the source or the tests change: `grunt interactive`. 117 | - If the live reload bothers you, you can also run the tests in a browser without it: `grunt webtest`. 118 | - Run the linter only with `grunt lint` or `grunt hint`. (The linter is part of `grunt test` as well.) 119 | - Build the dist files (also running tests and linter) with `grunt build`, or just `grunt`. 120 | - Build continuously on every save with `grunt ci`. 121 | - Change the version number throughout the project with `grunt setver --to=1.2.3`. Or just increment the revision with `grunt setver --inc`. (Remember to rebuild the project with `grunt` afterwards.) 122 | - `grunt getver` will quickly tell you which version you are at. 123 | 124 | Finally, if need be, you can set up a quick demo page to play with the code. First, edit the files in the `demo` directory. Then display `demo/index.html`, live-reloading your changes to the code or the page, with `grunt demo`. Libraries needed for the demo/playground should go into the Bower dev dependencies – in the project-wide `bower.json` – or else be managed by the dedicated `bower.json` in the demo directory. 125 | 126 | _The `grunt interactive` and `grunt demo` commands spin up a web server, opening up the **whole project** to access via http._ So please be aware of the security implications. You can restrict that access to localhost in `Gruntfile.js` if you just use browsers on your machine. 127 | 128 | ### Changing the tool chain configuration 129 | 130 | In case anything about the test and build process needs to be changed, have a look at the following config files: 131 | 132 | - `karma.conf.js` (changes to dependencies, additional test frameworks) 133 | - `Gruntfile.js` (changes to the whole process) 134 | - `web-mocha/_index.html` (changes to dependencies, additional test frameworks) 135 | 136 | New test files in the `spec` directory are picked up automatically, no need to edit the configuration for that. 137 | 138 | ## Release Notes 139 | 140 | ### v2.0.0 141 | 142 | - Removed the separate AMD/Node builds in `dist/amd`. Module systems and browser globals are now supported by the same file, `dist/marionette.handlebars.js` (or `.min.js`) 143 | - Updated Marionette dependency to include Marionette 3 144 | - Exposed version in `Marionette.TemplateCache.MarionetteHandlebarsVersion` 145 | 146 | ### v1.0.2 147 | 148 | - Updated Backbone and Handlebars dependencies 149 | 150 | ### v1.0.1 151 | 152 | - Updated Backbone dependency 153 | 154 | ### v1.0.0 155 | 156 | - Added support for lazy-loading precompiled templates 157 | - Exposed getPrecompiledTemplate 158 | - Added lazy-loaded, precompiled templates to demo 159 | 160 | ### v0.1.0 - 0.1.1 161 | 162 | - Initial development, tests, documentation 163 | 164 | ## License 165 | 166 | MIT. 167 | 168 | Copyright (c) 2015-2025 Michael Heim. 169 | 170 | [dist-dev]: https://raw.github.com/hashchange/marionette.handlebars/master/dist/marionette.handlebars.js "marionette.handlebars.js" 171 | [dist-prod]: https://raw.github.com/hashchange/marionette.handlebars/master/dist/marionette.handlebars.min.js "marionette.handlebars.min.js" 172 | 173 | [amd-demo]: https://github.com/hashchange/marionette.handlebars/blob/master/demo/amd/main.js "Marionette.Handlebars: AMD demo" 174 | [amd-demo-async-loading]: https://github.com/hashchange/marionette.handlebars/blob/master/demo/amd/main.js#L165-L184 "Marionette.Handlebars: AMD demo – Async view creation" 175 | 176 | [lazy-loading]: #lazy-loading-of-templates 177 | [lazy-load-compiled-templates]: #allowing-compiled-templates-to-be-lazy-loaded 178 | 179 | [Backbone]: http://backbonejs.org/ "Backbone.js" 180 | [Underscore]: http://underscorejs.org/ "Underscore.js" 181 | [Marionette]: http://marionettejs.com/ "Marionette.js – The Backbone Framework" 182 | [Handlebars]: http://handlebarsjs.com/ "Handlebars.js – Minimal Templating on Steroids" 183 | [hlb-precompiled]: http://handlebarsjs.com/precompilation.html "Handlebars.js: Precompiling templates" 184 | [Marionette.TemplateCache-basic-usage]: http://marionettejs.com/docs/marionette.templatecache.html#basic-usage "Marionette.TemplateCache: Basic Usage" 185 | [traffic-cop]: https://github.com/ifandelse/TrafficCop "Traffic Cop – Simple js lib to prevent multiple simultaneous client requests to same HTTP endpoint" 186 | [traffic-cop-docs]: http://web.archive.org/web/20130224223736/http://freshbrewedcode.com/jimcowart/2011/11/25/traffic-cop/ "Jim Cowart: Traffic Cop" 187 | [los-techies-traffic-cop]: http://lostechies.com/derickbailey/2012/03/20/trafficcop-a-jquery-plugin-to-limit-ajax-requests-for-a-resource/ " Derick Bailey – TrafficCop: A jQuery Plugin To Limit AJAX Requests For A Resource" 188 | 189 | [Node.js]: http://nodejs.org/ "Node.js" 190 | [Bower]: http://bower.io/ "Bower: a package manager for the web" 191 | [npm]: https://npmjs.org/ "npm: Node Packaged Modules" 192 | [Grunt]: http://gruntjs.com/ "Grunt: The JavaScript Task Runner" 193 | [Karma]: http://karma-runner.github.io/ "Karma – Spectacular Test Runner for Javascript" 194 | [Mocha]: http://mochajs.org/ "Mocha – the fun, simple, flexible JavaScript test framework" 195 | [Chai]: http://chaijs.com/ "Chai: a BDD / TDD assertion library" 196 | [Sinon]: http://sinonjs.org/ "Sinon.JS – Versatile standalone test spies, stubs and mocks for JavaScript" 197 | [JSHint]: http://www.jshint.com/ "JSHint, a JavaScript Code Quality Tool" 198 | 199 | [license]: #license "License" 200 | [hashchange-projects-overview]: http://hashchange.github.io/ "Hacking the front end: Backbone, Marionette, jQuery and the DOM. An overview of open-source projects by @hashchange." -------------------------------------------------------------------------------- /spec/helpers/dom-utils.js: -------------------------------------------------------------------------------- 1 | // !!!!!!!!!!!!!!!!!!!!!!!!!! 2 | // Depends on basic-utils.js 3 | // !!!!!!!!!!!!!!!!!!!!!!!!!! 4 | 5 | /** 6 | * Creates a child window, including a document with an HTML 5 doctype, UFT-8 charset, head, title, and body tags. 7 | * Returns the handle, or undefined if window creation fails. 8 | * 9 | * Optionally accepts a jQuery Deferred. The deferred is resolved when the document in the child window is ready and the 10 | * window has expanded to its intended size. If the child window can't be created, the deferred is rejected. (For this, 11 | * jQuery needs to be loaded, obviously.) 12 | * 13 | * The size can also be specified. If so, the new window is opened with minimal browser chrome (no menu, location, and 14 | * status bars) and is positioned at the top left of the viewport. By default, the window is opened with the default 15 | * settings of the browser (usually with browser chrome, and not exactly in the top left corner). 16 | * 17 | * If the child window can't be created, a pop-up blocker usually prevents it. Pop-up blockers are active by default in 18 | * most browsers - Chrome, for instance. 19 | * 20 | * @param {jQuery.Deferred} [readyDfd] 21 | * @param {Object|string} [size] "parent" (same size as parent window), or size object 22 | * @param {number} [size.width] 23 | * @param {number} [size.height] 24 | * 25 | * @returns {Window|undefined} the window handle 26 | */ 27 | function createChildWindow ( readyDfd, size ) { 28 | var childWindow, width, height, 29 | sizedDefaultProps = ",top=0,left=0,location=no,menubar=no,status=no,toolbar=no,resizeable=yes,scrollbars=yes"; 30 | 31 | if ( size ) { 32 | 33 | width = size === "parent" ? window.document.documentElement.clientWidth : size.width; 34 | height = size === "parent" ? window.document.documentElement.clientHeight : size.height; 35 | 36 | childWindow = window.open( "", "", "width=" + width + ",height=" + height + sizedDefaultProps ); 37 | 38 | } else { 39 | 40 | childWindow = window.open(); 41 | 42 | } 43 | 44 | if ( childWindow ) { 45 | 46 | // Setting the document content (using plain JS - jQuery can't write an entire HTML document, including the 47 | // doctype and tags). 48 | childWindow.document.open(); 49 | childWindow.document.write( '\n\n\n\n\n\n\n\n' ); 50 | childWindow.document.close(); 51 | 52 | } 53 | 54 | if ( readyDfd ) { 55 | if ( ! varExists( $ ) ) throw new Error( "`$` variable is not available. For using a readyDfd, jQuery (or a compatible library) must be loaded" ); 56 | 57 | if ( childWindow && childWindow.document ) { 58 | $( childWindow.document ).ready ( function () { 59 | windowSizeReady( childWindow, readyDfd ); 60 | } ); 61 | } else { 62 | readyDfd.reject(); 63 | } 64 | } 65 | 66 | return childWindow; 67 | } 68 | 69 | /** 70 | * Creates an iframe with an HTML5 doctype and UTF-8 encoding. Appends it to the body, or to another specified parent 71 | * element. Alternatively, the iframe can be prepended to the parent. 72 | * 73 | * The iframe element can be styled as it is created, before it is added to the DOM, e.g. to keep it out of view. 74 | * Likewise, styles can be written into the iframe document as it is created, providing it with defaults from the 75 | * get-go. 76 | * 77 | * @param {Object} [opts] 78 | * @param {HTMLElement|jQuery} [opts.parent=document.body] the parent element to which the iframe is appended 79 | * @param {boolean} [opts.prepend=false] if true, the iframe gets prepended to the parent, rather than appended 80 | * @param {string} [opts.elementStyles] cssText string, styles the iframe _element_ 81 | * @param {string} [opts.documentStyles] cssText string of entire rules, styles the iframe document, e.g. 82 | * "html, body { overflow: hidden; } div.foo { margin: 2em; }" 83 | * @returns {HTMLIFrameElement} 84 | */ 85 | function createIframe ( opts ) { 86 | var parent = ( opts && opts.parent ) ? ( varExists( $ ) && opts.parent instanceof $ ) ? opts.parent[0] : opts.parent : document.body, 87 | _document = parent.ownerDocument, 88 | iframe = _document.createElement( "iframe" ); 89 | 90 | opts || ( opts = {} ); 91 | 92 | if ( opts.elementStyles ) iframe.style.cssText = ensureTrailingSemicolon( opts.elementStyles ); 93 | iframe.frameborder = "0"; 94 | 95 | if ( opts.prepend ) { 96 | parent.insertBefore( iframe, parent.firstChild ); 97 | } else { 98 | parent.appendChild( iframe ); 99 | } 100 | 101 | iframe.src = 'about:blank'; 102 | createIframeDocument( iframe, opts.documentStyles ); 103 | 104 | return iframe; 105 | } 106 | 107 | /** 108 | * Creates an iframe document with an HTML5 doctype and UTF-8 encoding. 109 | * 110 | * The iframe element MUST have been appended to the DOM by the time this function is called, and it must be a 111 | * descendant of the body element. A document inside an iframe can only be created when these conditions are met. 112 | * 113 | * @param {HTMLIFrameElement|jQuery} iframe 114 | * @param {string} [documentStyles] cssText string of CSS rules, styles the iframe document, e.g. 115 | * "html, body { overflow: hidden; } div.foo { margin: 2em; }" 116 | * @returns {Document} 117 | */ 118 | function createIframeDocument ( iframe, documentStyles ) { 119 | if ( varExists( $ ) && iframe instanceof $ ) iframe = iframe[0]; 120 | 121 | if ( ! iframe.ownerDocument.body.contains( iframe ) ) throw new Error( "The iframe has not been appended to the DOM, or is not a descendant of the body element. Can't create an iframe content document." ); 122 | if ( ! iframe.contentDocument ) throw new Error( "Cannot access the iframe content document. Check for cross-domain policy restrictions." ); 123 | 124 | documentStyles = documentStyles ? '\n' : ""; 125 | iframe.contentDocument.write( '\n\n\n\n\n' + documentStyles + '\n\n\n' ); 126 | 127 | return iframe.contentDocument; 128 | } 129 | 130 | /** 131 | * Waits for the size of a window to become stable, in case it is undergoing a change. Returns a deferred which resolves 132 | * when the window size is stable. 133 | * 134 | * Optionally accepts an external jQuery deferred to act on, which is then returned instead. 135 | * 136 | * This check can be used to determine when the process of resizing a window has ended. It should also be used when a 137 | * new window is created. 138 | * 139 | * Technique 140 | * --------- 141 | * 142 | * In most cases, it would be enough to set a timeout of 0 and then resolve the deferred. The timeout frees the UI and 143 | * allows the window to assume its eventual size before the deferred is resolved. 144 | * 145 | * Unfortunately, the success rate of this approach is close to, but not quite, 100%. So instead, we check the reported 146 | * window size in regular intervals, so we know for sure when it is stable. 147 | * 148 | * @param {Window|jQuery} queriedWindow the window to observe, also accepted inside a jQuery `$( window )` wrapper 149 | * @param {jQuery.Deferred} [readyDfd] 150 | * @param {number} [interval=100] the interval for checking the window size, in ms 151 | * 152 | * @returns {jQuery.Deferred} 153 | */ 154 | function windowSizeReady ( queriedWindow, readyDfd, interval ) { 155 | 156 | if ( !varExists( $ ) ) throw new Error( "This method uses jQuery deferreds, but the $ variable is not available" ); 157 | 158 | if ( queriedWindow instanceof $ ) queriedWindow = queriedWindow[0]; 159 | 160 | readyDfd || ( readyDfd = $.Deferred() ); 161 | 162 | $( queriedWindow.document ).ready( function () { 163 | 164 | var documentElement = queriedWindow.document.documentElement, 165 | lastSize = { width: documentElement.clientWidth, height: documentElement.clientHeight }, 166 | 167 | repeater = setInterval( function () { 168 | 169 | var width = documentElement.clientWidth, 170 | height = documentElement.clientHeight, 171 | isStable = width > 0 && height > 0 && width === lastSize.width && height === lastSize.height; 172 | 173 | if ( isStable ) { 174 | clearInterval( repeater ); 175 | readyDfd.resolve(); 176 | } else { 177 | lastSize = { width: width, height: height }; 178 | } 179 | 180 | }, interval || 100 ); 181 | 182 | } ); 183 | 184 | return readyDfd; 185 | 186 | } 187 | 188 | /** 189 | * Makes sure a window is as at least as large as the specified minimum. If the window is too small, an error 190 | * is thrown. 191 | * 192 | * Optionally, it can be validated that the window matches the expected size exactly. 193 | * 194 | * @param {Object} expected 195 | * @param {number} expected.width 196 | * @param {number} expected.height 197 | * @param {Object} [opts] 198 | * @param {Object} [opts.window=window] a window handle, defaults to the global `window` 199 | * @param {boolean} [opts.exactly=false] 200 | */ 201 | function validateWindowSize ( expected, opts ) { 202 | var msg = "", 203 | documentElement = ( opts && opts.window || window ).document.documentElement, 204 | width = documentElement.clientWidth, 205 | height = documentElement.clientHeight; 206 | 207 | if ( opts && opts.exactly ) { 208 | if ( width !== expected.width ) msg = " Window width is " + width + "px (expected: " + expected.width + "px)."; 209 | if ( height !== expected.height ) msg += " Window height is " + height + "px (expected: " + expected.height + "px)."; 210 | } else { 211 | if ( width < expected.width ) msg = " Window width is " + width + "px (expected minimum: " + expected.width + "px)."; 212 | if ( height < expected.height ) msg += " Window height is " + height + "px (expected minimum: " + expected.height + "px)."; 213 | } 214 | 215 | if ( msg !== "" ) throw new Error( "The browser window does not match the expected size." + msg ); 216 | } 217 | 218 | /** 219 | * Feature-tests that an iframe expands to show its content, even if given an explicit width and height. This is the 220 | * case on iOS. 221 | * 222 | * For some background on expanding iframes in iOS, see 223 | * http://dev.magnolia-cms.com/blog/2012/05/strategies-for-the-iframe-on-the-ipad-problem/ 224 | * 225 | * @returns {boolean} 226 | */ 227 | function testIframeExpands () { 228 | var _document, _documentElement, expands, 229 | iframe = createIframe(); 230 | 231 | iframe.style.cssText = "width: 50px; height: 50px; padding: 0px; border: none; margin: 0px;"; 232 | 233 | _document = iframe.contentDocument; 234 | _documentElement = _document.documentElement; 235 | 236 | _document.body.style.cssText = "width: 100px; height: 100px;"; 237 | 238 | expands = parseFloat( _documentElement.clientWidth ) > 50 || parseFloat( _documentElement.clientHeight ) > 50; 239 | 240 | document.body.removeChild( iframe ); 241 | 242 | return expands; 243 | } 244 | 245 | /** 246 | * Forces a reflow for a given element, in case it doesn't happen automatically. 247 | * 248 | * For the technique, see http://stackoverflow.com/a/14382251/508355 249 | * 250 | * For some background, see e.g. http://apmblog.dynatrace.com/2009/12/12/understanding-internet-explorer-rendering-behaviour/ 251 | * 252 | * @param {HTMLElement|jQuery} element 253 | */ 254 | function forceReflow ( element ) { 255 | if ( !varExists( $ ) ) throw new Error( "This method uses jQuery, but the $ variable is not available" ); 256 | 257 | var $element = element instanceof $ ? element : $( element ); 258 | 259 | $element.css( { display: "none" } ).height(); 260 | $element.css( { display: "block" } ); 261 | } 262 | 263 | /** 264 | * Detects if the browser is on iOS. Works for Safari as well as other browsers, say, Chrome on iOS. 265 | * 266 | * Required for some iOS behaviour which can't be feature-detected in any way. 267 | * 268 | * @returns {boolean} 269 | */ 270 | function isIOS () { 271 | return /iPad|iPhone|iPod/g.test( navigator.userAgent ); 272 | } 273 | 274 | /** 275 | * Detects IE. 276 | * 277 | * Can use a version requirement. A range can also be specified, e.g. with an option like { gte: 8, lt: 11 }. 278 | * 279 | * @param {Object} [opts] 280 | * @param {number} [opts.eq] the IE version must be as specified 281 | * @param {number} [opts.lt] the IE version must be less than the one specified 282 | * @param {number} [opts.lte] the IE version must be less than or equal to the one specified 283 | * @param {number} [opts.gt] the IE version must be greater than the one specified 284 | * @param {number} [opts.gte] the IE version must be greater than or equal to the one specified 285 | */ 286 | function isIE ( opts ) { 287 | var ver = getIEVersion(), 288 | isMatch = ver !== 0; 289 | 290 | opts || ( opts = {} ); 291 | 292 | if ( isMatch && opts.eq ) isMatch = ver === opts.eq; 293 | if ( isMatch && opts.lt ) isMatch = ver < opts.lt; 294 | if ( isMatch && opts.lte ) isMatch = ver <= opts.lte; 295 | if ( isMatch && opts.gt ) isMatch = ver > opts.gt; 296 | if ( isMatch && opts.gte ) isMatch = ver >= opts.gte; 297 | 298 | return isMatch; 299 | } 300 | 301 | /** 302 | * Detects the IE version. Returns the major version number, or 0 if the browser is not IE. 303 | * 304 | * Simple solution, solely based on UA sniffing. In a better implementation, conditional comments would be used to 305 | * detect IE6 to IE9 - see https://gist.github.com/cowboy/542301 for an example. UA sniffing would only serve as a 306 | * fallback to detect IE > 9. There are also other solutions to infer the version of IE > 9. For inspiration, see 307 | * http://stackoverflow.com/q/17907445/508355. 308 | */ 309 | function getIEVersion () { 310 | var ieMatch = /MSIE (\d+)/.exec( navigator.userAgent ) || /Trident\/.+? rv:(\d+)/.exec( navigator.userAgent ); 311 | return ( ieMatch && ieMatch.length ) ? parseFloat( ieMatch[1] ) : 0; 312 | } 313 | 314 | function isSlimerJs () { 315 | return /SlimerJS/.test( navigator.userAgent ); 316 | } 317 | 318 | function isPhantomJs () { 319 | return /PhantomJS/.test( navigator.userAgent ); 320 | } 321 | 322 | // Legacy name, still around in old test code. 323 | function inPhantomJs () { 324 | return isPhantomJs(); 325 | } 326 | 327 | -------------------------------------------------------------------------------- /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: '// Marionette.Handlebars, v<%= meta.version %>\n' + 81 | '// Copyright (c) 2015-<%= grunt.template.today("yyyy") %> Michael Heim, Zeilenwechsel.de\n' + 82 | '// Distributed under MIT license\n' + 83 | '// http://github.com/hashchange/marionette.handlebars\n' + 84 | '\n' 85 | }, 86 | 87 | preprocess: { 88 | build: { 89 | files: { 90 | 'dist/marionette.handlebars.js': 'src/marionette.handlebars.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/marionette.handlebars.js', 115 | dest: 'dist/marionette.handlebars.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/marionette.handlebars.js', 129 | dest: 'dist/marionette.handlebars.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/**/marionette.handlebars.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 | unifiedBuild : { 226 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/unified/build-config.js', false ) 227 | }, 228 | splitBuildVendor : { 229 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/vendor-config.js', false ) 230 | }, 231 | splitBuildApp : { 232 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/app-config.js', false ) 233 | } 234 | }, 235 | 236 | // Use focus to run Grunt watch with a hand-picked set of simultaneous watch targets. 237 | focus: { 238 | demo: { 239 | include: ['livereloadDemo'] 240 | }, 241 | demoCi: { 242 | include: ['build', 'livereloadDemo'] 243 | }, 244 | demoCiDirty: { 245 | include: ['buildDirty', 'livereloadDemo'] 246 | } 247 | }, 248 | 249 | // Use watch to monitor files for changes, and to kick off a task then. 250 | watch: { 251 | options: { 252 | nospawn: false 253 | }, 254 | // Live-reloads the web page when the source files or the spec files change. Meant for test pages. 255 | livereloadTest: { 256 | options: { 257 | livereload: LIVERELOAD_PORT 258 | }, 259 | files: WATCHED_FILES_SRC.concat( WATCHED_FILES_SPEC ) 260 | }, 261 | // Live-reloads the web page when the dist files or the demo files change. Meant for demo pages. 262 | livereloadDemo: { 263 | options: { 264 | livereload: LIVERELOAD_PORT 265 | }, 266 | files: WATCHED_FILES_DEMO.concat( WATCHED_FILES_DIST ) 267 | }, 268 | // Runs the "build" task (ie, runs linter and tests, then compiles the dist files) when the source files or the 269 | // spec files change. Meant for continuous integration tasks ("ci", "demo-ci"). 270 | build: { 271 | tasks: ['build'], 272 | files: WATCHED_FILES_SRC.concat( WATCHED_FILES_SPEC ) 273 | }, 274 | // Runs the "build-dirty" task (ie, compiles the dist files without running linter and tests) when the source 275 | // files change. Meant for "dirty" continuous integration tasks ("ci-dirty", "demo-ci-dirty"). 276 | buildDirty: { 277 | tasks: ['build-dirty'], 278 | files: WATCHED_FILES_SRC 279 | } 280 | }, 281 | 282 | // Spins up a web server. 283 | connect: { 284 | options: { 285 | port: HTTP_PORT, 286 | // For restricting access to localhost only, change the hostname from '*' to 'localhost' 287 | hostname: '*', 288 | open: true, 289 | base: '.' 290 | }, 291 | livereload: { 292 | livereload: LIVERELOAD_PORT 293 | }, 294 | test: { 295 | options: { 296 | open: 'http://localhost:<%= connect.options.port %>/web-mocha/', 297 | livereload: LIVERELOAD_PORT 298 | } 299 | }, 300 | testNoReload: { 301 | options: { 302 | open: 'http://localhost:<%= connect.options.port %>/web-mocha/', 303 | keepalive: true 304 | } 305 | }, 306 | demo: { 307 | options: { 308 | open: 'http://localhost:<%= connect.options.port %>/demo/', 309 | livereload: LIVERELOAD_PORT 310 | } 311 | } 312 | }, 313 | 314 | replace: { 315 | version: { 316 | src: ['bower.json', 'package.json'], 317 | overwrite: true, 318 | replacements: [{ 319 | from: /"version"\s*:\s*"((\d+\.\d+\.)(\d+))"\s*,/, 320 | to: function (matchedWord, index, fullText, regexMatches) { 321 | var version = grunt.option('inc') ? regexMatches[1] + (parseInt(regexMatches[2], 10) + 1) : grunt.option('to'); 322 | 323 | 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'); 324 | if (typeof version !== "string") grunt.fail.fatal('Version number is not a string. Provide a semantic version number, e.g. --to=1.2.3'); 325 | 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'); 326 | 327 | grunt.log.writeln('Modifying file: Changing the version number from ' + regexMatches[0] + ' to ' + version); 328 | return '"version": "' + version + '",'; 329 | } 330 | }] 331 | } 332 | }, 333 | getver: { 334 | files: ['bower.json', 'package.json'] 335 | } 336 | }); 337 | 338 | grunt.loadNpmTasks('grunt-preprocess'); 339 | grunt.loadNpmTasks('grunt-contrib-concat'); 340 | grunt.loadNpmTasks('grunt-contrib-jshint'); 341 | grunt.loadNpmTasks('grunt-contrib-uglify'); 342 | grunt.loadNpmTasks('grunt-karma'); 343 | grunt.loadNpmTasks('grunt-contrib-watch'); 344 | grunt.loadNpmTasks('grunt-contrib-connect'); 345 | grunt.loadNpmTasks('grunt-sails-linker'); 346 | grunt.loadNpmTasks('grunt-text-replace'); 347 | grunt.loadNpmTasks('grunt-contrib-requirejs'); 348 | grunt.loadNpmTasks('grunt-focus'); 349 | 350 | grunt.registerTask('lint', ['jshint:components']); 351 | grunt.registerTask('hint', ['jshint:components']); // alias 352 | grunt.registerTask('test', ['jshint:components', 'karma:test']); 353 | grunt.registerTask('test-legacy', ['jshint:components', 'karma:test-legacy']); 354 | grunt.registerTask('webtest', ['preprocess:interactive', 'sails-linker:interactive_sinon', 'sails-linker:interactive_spec', 'connect:testNoReload']); 355 | grunt.registerTask('interactive', ['preprocess:interactive', 'sails-linker:interactive_sinon', 'sails-linker:interactive_spec', 'connect:test', 'watch:livereloadTest']); 356 | grunt.registerTask('demo', ['connect:demo', 'focus:demo']); 357 | grunt.registerTask('build', ['jshint:components', 'karma:build', 'preprocess:build', 'concat', 'uglify', 'jshint:concatenated', 'requirejs']); 358 | grunt.registerTask('ci', ['build', 'watch:build']); 359 | grunt.registerTask('setver', ['replace:version']); 360 | grunt.registerTask('getver', function () { 361 | grunt.config.get('getver.files').forEach(function (file) { 362 | var config = grunt.file.readJSON(file); 363 | grunt.log.writeln('Version number in ' + file + ': ' + config.version); 364 | }); 365 | }); 366 | 367 | // Special tasks, not mentioned in Readme documentation: 368 | // 369 | // - test-legacy: 370 | // runs the unit tests with legacy Marionette (version 2.x) 371 | // - requirejs: 372 | // creates build files for the AMD demo with r.js 373 | // - build-dirty: 374 | // builds the project without running checks (no linter, no tests) 375 | // - ci-dirty: 376 | // builds the project without running checks (no linter, no tests) on every source change 377 | // - demo-ci: 378 | // Runs the demo (= "demo" task), and also rebuilds the project on every source change (= "ci" task) 379 | // - demo-ci-dirty: 380 | // Runs the demo (= "demo" task), and also rebuilds the project "dirty", without tests or linter, on every source 381 | // change (= "ci-dirty" task) 382 | grunt.registerTask('build-dirty', ['preprocess:build', 'concat', 'uglify', 'requirejs']); 383 | grunt.registerTask('ci-dirty', ['build-dirty', 'watch:buildDirty']); 384 | grunt.registerTask('demo-ci', ['build', 'connect:demo', 'focus:demoCi']); 385 | grunt.registerTask('demo-ci-dirty', ['build-dirty', 'connect:demo', 'focus:demoCiDirty']); 386 | 387 | // Make 'build' the default task. 388 | grunt.registerTask('default', ['build']); 389 | 390 | 391 | }; 392 | -------------------------------------------------------------------------------- /spec/full.spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, $ */ 2 | (function () { 3 | "use strict"; 4 | 5 | function isMarionette1x () { 6 | return ! Backbone.Marionette.VERSION; 7 | } 8 | 9 | describe( 'Marionette.Handlebars', function () { 10 | 11 | var $template, domTemplateHtml, precompiledTemplateHtml, precompiled, MarionetteView; 12 | 13 | beforeEach( function () { 14 | MarionetteView = getMarionetteView(); 15 | 16 | // Template in the DOM. 17 | domTemplateHtml = "A template with {{content}} in the DOM."; 18 | $template = $( '' ) 19 | .html( domTemplateHtml ) 20 | .appendTo( "body" ); 21 | 22 | // Faking a precompiled template here. 23 | Handlebars.templates || ( Handlebars.templates = {} ); 24 | 25 | precompiledTemplateHtml = "A fake precompiled template with {{content}}, stored in the cache of Handlebars."; 26 | precompiled = Handlebars.templates[ "fake.precompiled" ] = Handlebars.compile( precompiledTemplateHtml ); 27 | 28 | // Spies 29 | sinon.spy( Backbone.Marionette.TemplateCache.prototype, "loadTemplate" ); 30 | sinon.spy( Backbone.Marionette.TemplateCache.prototype, "compileTemplate" ); 31 | sinon.spy( Handlebars, "compile" ); 32 | } ); 33 | 34 | afterEach( function () { 35 | $template.remove(); 36 | Backbone.Marionette.TemplateCache.clear(); 37 | delete Handlebars.templates; 38 | 39 | Backbone.Marionette.TemplateCache.prototype.loadTemplate.restore(); 40 | Backbone.Marionette.TemplateCache.prototype.compileTemplate.restore(); 41 | Handlebars.compile.restore(); 42 | } ); 43 | 44 | describe( 'Templates in the DOM', function () { 45 | 46 | describe( 'If a template is defined by a selector which matches a template in the DOM', function () { 47 | 48 | var returnedFromCache; 49 | 50 | beforeEach( function () { 51 | returnedFromCache = Backbone.Marionette.TemplateCache.get( "#template" ); 52 | } ); 53 | 54 | it( 'it is returned by loadTemplate()', function () { 55 | expect( Backbone.Marionette.TemplateCache.prototype.loadTemplate ).to.have.returned( domTemplateHtml ); 56 | } ); 57 | 58 | it( 'it is passed to the compileTemplate() method as the template argument', function () { 59 | expect( Backbone.Marionette.TemplateCache.prototype.compileTemplate ).to.have.been.calledWith( domTemplateHtml ); 60 | } ); 61 | 62 | it( 'it is passed to the Handlebars.compile() as the template argument', function () { 63 | expect( Handlebars.compile ).to.have.been.calledWith( domTemplateHtml ); 64 | } ); 65 | 66 | it( 'shows up in the compiled template returned by Marionette.TemplateCache.get()', function () { 67 | var compiled = Handlebars.compile( domTemplateHtml ), 68 | templateVars = { content: "amazing content" }; 69 | 70 | expect( returnedFromCache( templateVars ) ).to.equal( compiled( templateVars ) ); 71 | } ); 72 | 73 | it( 'it is used by a Marionette view if its template property has been set to the selector of the template', function () { 74 | var compiled = Handlebars.compile( domTemplateHtml ), 75 | content = { content: "amazing content" }, 76 | view = new MarionetteView( { 77 | template: "#template", 78 | model: new Backbone.Model( content ) 79 | } ); 80 | 81 | view.render(); 82 | expect( view.$el.html() ).to.equal( compiled( content ) ); 83 | } ); 84 | 85 | } ); 86 | 87 | describe( 'If the template is requested with an options object', function () { 88 | 89 | var options; 90 | 91 | beforeEach( function () { 92 | options = { 93 | data: { level: Handlebars.logger.WARN } 94 | }; 95 | Backbone.Marionette.TemplateCache.get( "#template", _.clone( options ) ); 96 | } ); 97 | 98 | itUnless( isMarionette1x(), "Skipped test. Options are not supported in Marionette 1.x", 99 | 'the options object is passed on to Handlebars when the template is being compiled', function () { 100 | expect( Handlebars.compile ).to.have.been.calledWithExactly( domTemplateHtml, options ); 101 | } 102 | ); 103 | 104 | } ); 105 | 106 | } ); 107 | 108 | describe( 'Precompiled templates', function () { 109 | 110 | describe( 'If a template is in the Handlebars template cache', function () { 111 | 112 | var returnedFromCache; 113 | 114 | beforeEach( function () { 115 | returnedFromCache = Backbone.Marionette.TemplateCache.get( "fake.precompiled" ); 116 | } ); 117 | 118 | it( 'it is returned by loadTemplate()', function () { 119 | expect( Backbone.Marionette.TemplateCache.prototype.loadTemplate ).to.have.returned( precompiled ); 120 | } ); 121 | 122 | it( 'it is passed to the compileTemplate() method as the template argument', function () { 123 | expect( Backbone.Marionette.TemplateCache.prototype.compileTemplate ).to.have.been.calledWith( precompiled ); 124 | } ); 125 | 126 | it( 'it is returned by compileTemplate(), without a recompilation having taken place', function () { 127 | // Ie, compileTemplate must return the exact same function object 128 | expect( Backbone.Marionette.TemplateCache.prototype.compileTemplate ).to.have.returned( precompiled ); 129 | } ); 130 | 131 | it( 'it is returned by Marionette.TemplateCache.get()', function () { 132 | expect( returnedFromCache ).to.equal( precompiled ); 133 | } ); 134 | 135 | it( 'it is used by a Marionette view if its template property has been set to the ID of the cached template', function () { 136 | var content = { content: "amazing content" }, 137 | view = new MarionetteView( { 138 | template: "fake.precompiled", 139 | model: new Backbone.Model( content ) 140 | } ); 141 | 142 | view.render(); 143 | expect( view.$el.html() ).to.equal( precompiled( content ) ); 144 | } ); 145 | 146 | it( 'it takes precedence over a template in the DOM with the same template ID', function () { 147 | Handlebars.templates[ "#template" ] = Handlebars.compile( precompiledTemplateHtml ); 148 | expect( Backbone.Marionette.TemplateCache.get( "#template" ) ).to.equal( Handlebars.templates[ "#template" ] ); 149 | } ); 150 | 151 | } ); 152 | 153 | } ); 154 | 155 | describe( 'Custom lazy loader', function () { 156 | 157 | var defaultNoopLazyLoader; 158 | 159 | beforeEach( function () { 160 | defaultNoopLazyLoader = Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate; 161 | } ); 162 | 163 | afterEach( function () { 164 | Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate = defaultNoopLazyLoader; 165 | } ); 166 | 167 | describe( 'The lazyLoadTemplate() method is not called', function () { 168 | 169 | beforeEach( function () { 170 | sinon.spy( Backbone.Marionette.TemplateCache.prototype, "lazyLoadTemplate" ); 171 | } ); 172 | 173 | afterEach( function () { 174 | Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate.restore(); 175 | } ); 176 | 177 | it( 'if a matching template is found in the cache of Handlebars for precompiled templates', function () { 178 | Backbone.Marionette.TemplateCache.get( "fake.precompiled" ); 179 | expect( Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate ).not.to.have.been.called; 180 | } ); 181 | 182 | it( 'if the template is found in the DOM', function () { 183 | Backbone.Marionette.TemplateCache.get( "#template" ); 184 | expect( Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate ).not.to.have.been.called; 185 | } ); 186 | 187 | } ); 188 | 189 | describe( 'The lazyLoadTemplate() method receives', function () { 190 | 191 | beforeEach( function () { 192 | sinon.spy( Backbone.Marionette.TemplateCache.prototype, "lazyLoadTemplate" ); 193 | } ); 194 | 195 | afterEach( function () { 196 | Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate.restore(); 197 | } ); 198 | 199 | it( 'the template ID as the first argument', function () { 200 | // We ask for a template which doesn't exist in either the Handlebars template cache or the DOM, 201 | // so that the query is forwarded to the lazy loader. Because we use the default no-op loader, 202 | // we need to catch the resulting no-template error. 203 | try { 204 | Backbone.Marionette.TemplateCache.get( "fictional.id" ); 205 | } catch ( err ) {} 206 | 207 | expect( Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate ).to.have.been.calledWith( "fictional.id" ); 208 | } ); 209 | 210 | itUnless( isMarionette1x(), "Skipped test. Options are not supported in Marionette 1.x", 211 | 'an options object as second argument, if passed to Marionette.TemplateCache.get()', function () { 212 | var options = { foo: "bar" }; 213 | try { 214 | Backbone.Marionette.TemplateCache.get( "fictional.id", _.clone( options ) ); 215 | } catch ( err ) {} 216 | 217 | expect( Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate ).to.have.been.calledWithExactly( "fictional.id", options ); 218 | } 219 | ); 220 | 221 | } ); 222 | 223 | describe( 'The lazyLoadTemplate() method triggers a friendly error in subsequent processing', function () { 224 | 225 | var initialCompiledTemplatesFlag, lazyLoadedTemplateId, expectedError; 226 | 227 | beforeEach( function () { 228 | initialCompiledTemplatesFlag = Marionette.TemplateCache.allowCompiledTemplatesOverHttp; 229 | lazyLoadedTemplateId = "lazyLoaded"; 230 | expectedError = 'Could not load template: "' + lazyLoadedTemplateId + '"'; 231 | } ); 232 | 233 | afterEach( function () { 234 | Marionette.TemplateCache.allowCompiledTemplatesOverHttp = initialCompiledTemplatesFlag; 235 | } ); 236 | 237 | it( 'if its return value is undefined', function () { 238 | Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate = function () { return undefined; }; 239 | expect( function () { Backbone.Marionette.TemplateCache.get( lazyLoadedTemplateId ); } ).to.throw( expectedError ); 240 | } ); 241 | 242 | it( 'if its return value is false', function () { 243 | Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate = function () { return false; }; 244 | expect( function () { Backbone.Marionette.TemplateCache.get( lazyLoadedTemplateId ); } ).to.throw( expectedError ); 245 | } ); 246 | 247 | it( 'if its return value is an object', function () { 248 | Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate = function () { return { foo: "bar" }; }; 249 | expect( function () { Backbone.Marionette.TemplateCache.get( lazyLoadedTemplateId ); } ).to.throw( expectedError ); 250 | } ); 251 | 252 | it( 'if its return value is a function, and the allowCompiledTemplatesOverHttp flag has not actively been enabled', function () { 253 | Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate = function () { return { foo: "bar" }; }; 254 | expect( function () { Backbone.Marionette.TemplateCache.get( lazyLoadedTemplateId ); } ).to.throw( expectedError ); 255 | } ); 256 | 257 | it( 'if its return value is a function, and the allowCompiledTemplatesOverHttp flag been set to false explicitly', function () { 258 | Marionette.TemplateCache.allowCompiledTemplatesOverHttp = false; 259 | Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate = function () { return { foo: "bar" }; }; 260 | expect( function () { Backbone.Marionette.TemplateCache.get( lazyLoadedTemplateId ); } ).to.throw( expectedError ); 261 | } ); 262 | 263 | it( 'if its return value is an empty string', function () { 264 | Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate = function () { return ""; }; 265 | expect( function () { Backbone.Marionette.TemplateCache.get( lazyLoadedTemplateId ); } ).to.throw( expectedError ); 266 | } ); 267 | 268 | } ); 269 | 270 | describe( 'The lazyLoadTemplate() method is allowed to return a function', function () { 271 | 272 | var initialCompiledTemplatesFlag, lazyLoadedTemplateId; 273 | 274 | beforeEach( function () { 275 | initialCompiledTemplatesFlag = Marionette.TemplateCache.allowCompiledTemplatesOverHttp; 276 | lazyLoadedTemplateId = "lazyLoaded"; 277 | } ); 278 | 279 | afterEach( function () { 280 | Marionette.TemplateCache.allowCompiledTemplatesOverHttp = initialCompiledTemplatesFlag; 281 | } ); 282 | 283 | it( 'if the allowCompiledTemplatesOverHttp flag has been enabled', function () { 284 | var compiled = Handlebars.compile( precompiledTemplateHtml ); 285 | Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate = function () { return compiled; }; 286 | 287 | Marionette.TemplateCache.allowCompiledTemplatesOverHttp = true; 288 | expect( Backbone.Marionette.TemplateCache.get( lazyLoadedTemplateId ) ).to.equal( compiled ); 289 | } ); 290 | 291 | } ); 292 | 293 | describe( 'The return value of lazyLoadTemplate()', function () { 294 | 295 | var lazyLoadedTemplateHtml, returnedFromCache; 296 | 297 | beforeEach( function () { 298 | lazyLoadedTemplateHtml = "A fake lazy-loaded template with {{content}}."; 299 | Backbone.Marionette.TemplateCache.prototype.lazyLoadTemplate = function () { return lazyLoadedTemplateHtml; }; 300 | 301 | returnedFromCache = Backbone.Marionette.TemplateCache.get( "lazyLoaded" ); 302 | } ); 303 | 304 | it( 'is passed to the compileTemplate() method as the template argument', function () { 305 | expect( Backbone.Marionette.TemplateCache.prototype.compileTemplate ).to.have.been.calledWith( lazyLoadedTemplateHtml ); 306 | } ); 307 | 308 | it( 'shows up in the compiled template returned by Marionette.TemplateCache.get()', function () { 309 | var compiled = Handlebars.compile( lazyLoadedTemplateHtml ), 310 | templateVars = { content: "amazing content" }; 311 | 312 | expect( returnedFromCache( templateVars ) ).to.equal( compiled( templateVars ) ); 313 | } ); 314 | 315 | it( 'is used by a Marionette view if its template property has been set to the ID of a lazy-loaded template', function () { 316 | // Along the way, we check that the Marionette view can deal with an entirely arbitrary ID string 317 | // without any hiccups. 318 | var compiled = Handlebars.compile( lazyLoadedTemplateHtml ), 319 | content = { content: "amazing content" }, 320 | view = new MarionetteView( { 321 | template: "a! very! weird! template! id!", 322 | model: new Backbone.Model( content ) 323 | } ); 324 | 325 | view.render(); 326 | expect( view.$el.html() ).to.equal( compiled( content ) ); 327 | } ); 328 | 329 | } ); 330 | 331 | } ); 332 | 333 | describe( 'Load errors', function () { 334 | 335 | describe( 'A friendly error is thrown', function () { 336 | 337 | it( 'if the template cannot be found', function () { 338 | var templateId = "nonexistent"; 339 | expect( function () { Backbone.Marionette.TemplateCache.get( templateId ); } ).to.throw( 'Could not load template: "' + templateId + '"' ); 340 | } ); 341 | 342 | } ); 343 | 344 | } ); 345 | 346 | } ); 347 | 348 | })(); --------------------------------------------------------------------------------