├── demo ├── .bowerrc ├── demo.js ├── amd │ ├── rjs │ │ ├── config │ │ │ ├── unified │ │ │ │ └── build-config.js │ │ │ └── jsbin-parts │ │ │ │ ├── vendor-config.js │ │ │ │ └── app-config.js │ │ ├── build-commands.md │ │ └── output │ │ │ └── parts │ │ │ └── app.js │ ├── amd.css │ ├── inline-el.css │ ├── require-config.js │ ├── views-marionette.js │ ├── main.js │ ├── base.js │ ├── views-backbone.js │ └── index.html ├── demo.css ├── about.txt ├── common.css ├── bower.json ├── close-build-info.js ├── index.html └── bower-check.js ├── .gitignore ├── .jshintrc ├── LICENSE ├── bower.json ├── web-mocha ├── test-framework.css ├── help.html └── _index.html ├── package.json ├── karma.conf.js ├── dist ├── backbone.inline.template.min.js ├── backbone.inline.template.min.js.map └── backbone.inline.template.js ├── spec ├── helpers │ ├── suite-utils.js │ ├── various-utils.js │ ├── browser-utils.js │ ├── data-provider.js │ └── fixture-utils.js ├── template.selection.spec.js ├── matcher.spec.js ├── chai-helpers │ └── backbone-el.js └── special.cases.spec.js ├── src └── backbone.inline.template.js └── Gruntfile.js /demo/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_demo_components" 3 | } 4 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | ( function( Backbone, _ ) { 2 | "use strict"; 3 | 4 | }( Backbone, _ )); -------------------------------------------------------------------------------- /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 | }) -------------------------------------------------------------------------------- /.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/demo.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin: 0.25rem; 5 | } 6 | 7 | #header { 8 | margin: 1.25rem 0; 9 | } 10 | 11 | ul { 12 | list-style: none; 13 | margin-left: 0; 14 | } 15 | li { 16 | padding: 0; 17 | margin: 0; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "local.base", 8 | "local.views-backbone", 9 | "local.views-marionette" 10 | ], 11 | out: "../../output/parts/vendor.js" 12 | }) -------------------------------------------------------------------------------- /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 | "usertiming", 7 | "jquery", 8 | "underscore", 9 | "backbone", 10 | "marionette", 11 | "backbone.declarative.views", 12 | 'backbone.inline.template' 13 | ], 14 | out: "../../output/parts/app.js" 15 | }) -------------------------------------------------------------------------------- /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 | .list { 12 | list-style: none; 13 | margin: 0; 14 | } 15 | 16 | li.item { 17 | display: block; 18 | box-sizing: border-box; 19 | border: 1px dotted red; 20 | padding: 0; 21 | text-align: center; 22 | line-height: 3rem; 23 | height: 3rem; 24 | vertical-align: middle; 25 | } 26 | 27 | h1 { 28 | font-size: 2.25rem; 29 | } 30 | 31 | h2 { 32 | font-size: 1.75rem; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /.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 | 29 | "JSON": false, 30 | "require": false, 31 | "define": false, 32 | "module": false, 33 | "exports": true 34 | } 35 | } -------------------------------------------------------------------------------- /demo/common.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin: 0.25rem; 5 | } 6 | 7 | #header { 8 | margin-bottom: 2rem; 9 | } 10 | 11 | #header h1, #header h2 { 12 | margin-bottom: 1rem; 13 | } 14 | 15 | h1 { 16 | font-size: 2.5rem; 17 | } 18 | 19 | h2 { 20 | font-size: 1.75rem; 21 | } 22 | 23 | #build-info { 24 | position: relative; 25 | border: 1px solid #d8d8d8; 26 | border-radius: 3px; 27 | padding: 1.25rem; 28 | margin-bottom: 1.25rem; 29 | background-color: #ecfaff; 30 | } 31 | 32 | #build-info a { 33 | display: block; 34 | position: absolute; 35 | top: 0.15rem; 36 | right: 0.6rem; 37 | color: lightgrey; 38 | } 39 | 40 | #build-info a:hover, #build-info a:active { 41 | color: #007b9f; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /demo/amd/inline-el.css: -------------------------------------------------------------------------------- 1 | h2 { 2 | font-size: 1.25rem; 3 | padding-top: 1.5rem; 4 | padding-bottom: 1rem; 5 | } 6 | 7 | li.item { 8 | height: 4rem; 9 | line-height: 2rem; 10 | } 11 | 12 | /* Very specific selectors, show that all template `el`s have been recognized and are set up correctly */ 13 | .ok, 14 | .container>ul#unordered-list-backbone.list.small-block-grid-4.medium-block-grid-8.large-block-grid-10>li.item[contenteditable="true"][lang="en"], 15 | .container>ul#unordered-list-marionette.list.small-block-grid-4.medium-block-grid-8.large-block-grid-10>li.item[contenteditable="true"][lang="en"], 16 | .report>p.conventional { 17 | background-color: green; 18 | } 19 | 20 | .failure, 21 | li.item, 22 | .report p { 23 | background-color: red; 24 | } 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /demo/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "main": "index.html", 4 | "version": "0.0.0", 5 | "homepage": "https://github.com/hashchange/backbone.inline.template", 6 | "authors": [ 7 | "hashchange " 8 | ], 9 | "description": "Backbone.Inline.Template 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 | "foundation": "~5.5.3", 25 | "jquery": "^1", 26 | "modernizr": "^2 || ~3.3.1", 27 | "requirejs": "~2.2.0", 28 | "usertiming": "~0.1.8" 29 | }, 30 | "resolutions": { 31 | "jquery": "^1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.inline.template", 3 | "version": "1.0.1", 4 | "homepage": "https://github.com/hashchange/backbone.inline.template", 5 | "authors": [ 6 | "Michael Heim " 7 | ], 8 | "description": "Fully-formed templates for Backbone. Put the DOM element of a Backbone view into the template content.", 9 | "main": "dist/backbone.inline.template.js", 10 | "keywords": [ 11 | "backbone", 12 | "views", 13 | "backbone.view", 14 | "el", 15 | "html", 16 | "template", 17 | "tag", 18 | "tagName", 19 | "className", 20 | "attributes", 21 | "marionette", 22 | "backbone.declarative.views" 23 | ], 24 | "license": "MIT", 25 | "ignore": [ 26 | "**/.*", 27 | "node_modules", 28 | "bower_components", 29 | "test", 30 | "tests", 31 | "public", 32 | "reports", 33 | "demo", 34 | "lib-other", 35 | "web-mocha", 36 | "spec", 37 | "src", 38 | "Gruntfile.js", 39 | "karma.conf.js", 40 | "package.json" 41 | ], 42 | "devDependencies": { 43 | "marionette": "^1.1.0 <1.9.0 || ^2.0.0" 44 | }, 45 | "dependencies": { 46 | "backbone": "^1.0.0 <1.4.0", 47 | "backbone.declarative.views": "^3.1.0", 48 | "underscore": "^1.5.0 <1.9.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /demo/close-build-info.js: -------------------------------------------------------------------------------- 1 | // Closes the #build-info box when clicking on the close button. Hides the box immediately on JS Bin and Codepen. 2 | // 3 | // Works in any browser. 4 | ( function ( window, document ) { 5 | "use strict"; 6 | 7 | var isLocalDemo =!window.location.hostname.match( /jsbin\.|codepen\./i ), 8 | 9 | box = document.getElementById( "build-info" ), 10 | closeButton = document.getElementById( "close-build-info" ), 11 | 12 | close = function ( event ) { 13 | if ( event ) event.preventDefault(); 14 | box.style.display = "none"; 15 | }; 16 | 17 | addEventHandler( closeButton, "click", close ); 18 | if ( !isLocalDemo ) close(); 19 | 20 | function addEventHandler( element, event, handler ) { 21 | 22 | if ( element ) { 23 | 24 | if ( element.addEventListener ) { 25 | element.addEventListener( event, handler, false ); 26 | } else if ( element.attachEvent ) { 27 | element.attachEvent( "on" + event, handler ) 28 | } else { 29 | element["on" + event] = handler; 30 | } 31 | 32 | } else if ( window.console && window.console.log ) { 33 | window.console.log( "close-build-info.js: Build info box (or its close button) not found" ); 34 | } 35 | 36 | } 37 | 38 | } ( window, document ) ); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/amd/require-config.js: -------------------------------------------------------------------------------- 1 | requirejs.config( { 2 | 3 | // Base URL: project root 4 | baseUrl: '../../', 5 | 6 | paths: { 7 | 'usertiming': 'demo/bower_demo_components/usertiming/src/usertiming', 8 | 9 | // Using a different jQuery here than elsewhere (1.x, instead of 3.x in node_modules and bower_components). 10 | // Makes the demo work in oldIE, too. 11 | 'jquery': 'demo/bower_demo_components/jquery/dist/jquery', 12 | 13 | // Use this path for switching to jQuery 3.x 14 | // 'jquery': 'bower_components/jquery/dist/jquery', 15 | 16 | 'underscore': 'bower_components/underscore/underscore', 17 | 'backbone': 'bower_components/backbone/backbone', 18 | 'marionette': 'bower_components/marionette/lib/backbone.marionette', 19 | 'backbone.declarative.views': 'bower_components/backbone.declarative.views/dist/backbone.declarative.views', 20 | 21 | 'backbone.inline.template': 'dist/backbone.inline.template', 22 | 23 | 'local.main': 'demo/amd/main', 24 | 'local.base': 'demo/amd/base', 25 | 'local.views-backbone': 'demo/amd/views-backbone', 26 | 'local.views-marionette': 'demo/amd/views-marionette' 27 | }, 28 | 29 | shim: { 30 | 'backbone': { 31 | deps: ['underscore', 'jquery'], 32 | exports: 'Backbone' 33 | }, 34 | 'underscore': { 35 | exports: '_' 36 | }, 37 | 'marionette': { 38 | deps: ['jquery', 'underscore', 'backbone'], 39 | exports: 'Marionette' 40 | }, 41 | 42 | // Required for the Marionette part of the demo 43 | 'backbone.declarative.views': ['marionette'] 44 | } 45 | } ); 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/amd/views-marionette.js: -------------------------------------------------------------------------------- 1 | // views-marionette.js 2 | 3 | define( [ 4 | 5 | 'underscore', 6 | 'backbone', 7 | 'marionette', 8 | 'usertiming', 9 | 'local.base', 10 | 'backbone.declarative.views' 11 | 12 | ], function ( _, Backbone, Marionette, performance, base ) { 13 | 14 | var ItemView = Marionette.ItemView.extend( { 15 | 16 | appendTo: function ( $parent ) { 17 | if ( !( $parent instanceof Backbone.$ ) ) $parent = Backbone.$( $parent ); 18 | $parent.append( this.$el ); 19 | return this; 20 | } 21 | 22 | } ), 23 | 24 | ListView = Marionette.CollectionView.extend( { 25 | 26 | initialize: function ( options ) { 27 | options || ( options = {} ); // jshint ignore:line 28 | 29 | if ( options.parent ) this.parent = options.parent; 30 | this.$parent = Backbone.$( this.parent ); 31 | }, 32 | 33 | onBeforeRender: function () { 34 | // Start timer 35 | performance.clearMarks(); 36 | performance.clearMeasures(); 37 | performance.mark( "create-itemViews-start" ); 38 | }, 39 | 40 | onRender: function () { 41 | var duration; 42 | 43 | // Measure the time it took to create the itemViews 44 | performance.measure( "create-itemViews", "create-itemViews-start" ); 45 | duration = performance.getEntriesByName( "create-itemViews" )[0].duration; 46 | 47 | if ( ! this.$el.parent().length ) this.$el.appendTo( this.$parent ); 48 | 49 | base.eventBus.trigger( "createStats", { itemViewCount : this.children.length, duration: Math.round( duration ) } ); 50 | } 51 | 52 | } ); 53 | 54 | return { 55 | ItemView: ItemView, 56 | ListView: ListView 57 | } 58 | 59 | } ); 60 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Backbone.Inline.Template: Demo and Playground 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 |
32 |
33 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/amd/main.js: -------------------------------------------------------------------------------- 1 | // main.js 2 | 3 | require( [ 4 | 5 | 'backbone', 6 | 'local.base', 7 | 'local.views-backbone', 8 | 'local.views-marionette', 9 | 10 | 'backbone.inline.template' 11 | 12 | ], function ( Backbone, base, backboneViews, marionetteViews ) { 13 | 14 | // To make the inline `el` magic work with Marionette, the original template must be updated (modified). 15 | // Backbone-only views don't need this. Try it by commenting it out. 16 | Backbone.InlineTemplate.updateTemplateSource = true; 17 | 18 | var count = 10, 19 | 20 | BackboneListItemView = backboneViews.ItemView.extend( { template: "#item-template" } ), 21 | MarionetteListItemView = marionetteViews.ItemView.extend( { template: "#item-template" } ), 22 | 23 | backboneListView = new backboneViews.ListView( { 24 | template: "#list-template-backbone", 25 | parent: ".container.backbone", 26 | ItemView: BackboneListItemView, 27 | collection: base.Collection.create( count ) 28 | } ), 29 | 30 | marionetteListView = new marionetteViews.ListView( { 31 | template: "#list-template-marionette", 32 | childView: MarionetteListItemView, 33 | parent: ".container.marionette", 34 | collection: base.Collection.create( count ) 35 | } ), 36 | 37 | backboneReportView = new backboneViews.ItemView( { 38 | model: new Backbone.Model( { 39 | itemViewCount: count, 40 | framework: "Backbone" 41 | } ), 42 | template: "#report-template-backbone" 43 | } ), 44 | 45 | marionetteReportView = new marionetteViews.ItemView( { 46 | model: new Backbone.Model( { 47 | itemViewCount: count, 48 | framework: "Marionette" 49 | } ), 50 | template: "#report-template-marionette" 51 | } ); 52 | 53 | backboneListView.render(); 54 | marionetteListView.render(); 55 | 56 | backboneReportView.render().appendTo( ".report.backbone" ); 57 | marionetteReportView.render().appendTo( ".report.marionette" ); 58 | 59 | } ); 60 | -------------------------------------------------------------------------------- /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 Backbone.Inline.Template. 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 Backbone.Inline.Template 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": "backbone.inline.template", 3 | "version": "1.0.1", 4 | "homepage": "https://github.com/hashchange/backbone.inline.template", 5 | "bugs": "https://github.com/hashchange/backbone.inline.template/issues", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/hashchange/backbone.inline.template.git" 9 | }, 10 | "author": "Michael Heim (http://www.zeilenwechsel.de/)", 11 | "description": "Fully-formed templates for Backbone. Put the DOM element of a Backbone view into the template content.", 12 | "main": "dist/backbone.inline.template.js", 13 | "keywords": [ 14 | "backbone", 15 | "views", 16 | "backbone.view", 17 | "el", 18 | "html", 19 | "template", 20 | "tag", 21 | "tagName", 22 | "className", 23 | "attributes", 24 | "marionette", 25 | "backbone.declarative.views" 26 | ], 27 | "license": "MIT", 28 | "dependencies": { 29 | "backbone": "^1.0.0 <1.4.0", 30 | "backbone.declarative.views": "^3.1.0", 31 | "underscore": "^1.5.0 <1.9.0" 32 | }, 33 | "devDependencies": { 34 | "bower": "^1.7.9", 35 | "chai-subset": "~1.3.0", 36 | "connect-livereload": "~0.5.4", 37 | "grunt": "^1.0.1", 38 | "grunt-cli": "^1.2.0", 39 | "grunt-contrib-concat": "~1.0.1", 40 | "grunt-contrib-connect": "~1.0.2", 41 | "grunt-contrib-jshint": "~1.0.0", 42 | "grunt-contrib-requirejs": "^1.0.0", 43 | "grunt-contrib-uglify": "~2.0.0", 44 | "grunt-contrib-watch": "~1.0.0", 45 | "grunt-focus": "~1.0.0", 46 | "grunt-karma": "~2.0.0", 47 | "grunt-mocha": "~1.0.2", 48 | "grunt-preprocess": "~5.1.0", 49 | "grunt-sails-linker": "~0.10.1", 50 | "grunt-text-replace": "~0.4.0", 51 | "karma": "~1.2.0", 52 | "karma-chai-plugins": "~0.7.0", 53 | "karma-chrome-launcher": "~1.0.1", 54 | "karma-firefox-launcher": "~1.0.0", 55 | "karma-html2js-preprocessor": "~1.0.0", 56 | "karma-ie-launcher": "~1.0.0", 57 | "karma-mocha": "~1.1.1", 58 | "karma-mocha-reporter": "~2.1.0", 59 | "karma-opera-launcher": "~1.0.0", 60 | "karma-phantomjs-launcher": "~1.0.1", 61 | "karma-requirejs": "~1.0.0", 62 | "karma-safari-launcher": "1.0.0", 63 | "karma-script-launcher": "~1.0.0", 64 | "karma-slimerjs-launcher": "~1.1.0", 65 | "matchdep": "~1.0.1", 66 | "mocha": "~3.0.2", 67 | "phantomjs-prebuilt": "^2.1.12", 68 | "require-from-string": "^1.2.0", 69 | "requirejs": "^2.2.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /demo/amd/base.js: -------------------------------------------------------------------------------- 1 | // base.js 2 | 3 | define( [ 4 | 5 | 'underscore', 6 | 'backbone', 7 | 'usertiming', 8 | 'backbone.declarative.views' 9 | 10 | ], function ( _, Backbone, performance ) { 11 | 12 | var eventBus = _.extend( {}, Backbone.Events ), 13 | 14 | Model = Backbone.Model.extend(), 15 | 16 | Collection = Backbone.Collection.extend( 17 | { model: Model }, 18 | { 19 | create: function ( modelCount ) { 20 | var i, 21 | collection = new Collection(), 22 | models = []; 23 | 24 | for ( i = 0; i < modelCount; i++ ) models.push( new collection.model( { number: i + 1 } ) ); 25 | collection.reset( models ); 26 | 27 | return collection; 28 | } 29 | } 30 | ), 31 | 32 | StatsView = Backbone.View.extend( { 33 | 34 | template: "#stats-template", 35 | 36 | parent: ".stats", 37 | 38 | initialize: function ( options ) { 39 | var compiledTemplate = this.inlineTemplate.getCachedTemplate().compiled; 40 | this.template = compiledTemplate || _.template( this.inlineTemplate.getCachedTemplate(). html ); 41 | 42 | options || ( options = {} ); 43 | 44 | if ( options.parent ) this.parent = options.parent; 45 | this.$parent = Backbone.$( this.parent ); 46 | this.$el.appendTo( this.$parent ); 47 | 48 | this.listenTo( eventBus, "createStats", this.render ); 49 | }, 50 | 51 | render: function ( stats ) { 52 | stats.totalDuration = Math.round( this.getTotalDuration() ); 53 | this.$el.html( this.template( stats ) ); 54 | }, 55 | 56 | getTotalDuration: function () { 57 | // Query the document height. This is to assess the true total render time, including the painting. 58 | // The calculation of the document height should be blocked by the browser until all item elements have 59 | // been painted. 60 | var docHeight = $( document ).height(); 61 | 62 | performance.measure( "paint", "create-itemViews-start" ); 63 | return performance.getEntriesByName( "paint" )[0].duration; 64 | } 65 | 66 | } ); 67 | 68 | Backbone.DeclarativeViews.custom.compiler = _.template; 69 | 70 | return { 71 | Model: Model, 72 | Collection: Collection, 73 | StatsView: StatsView, 74 | eventBus: eventBus 75 | } 76 | 77 | } ); 78 | -------------------------------------------------------------------------------- /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 | 'bower_components/jquery/dist/jquery.js', 29 | 'bower_components/underscore/underscore.js', 30 | 'bower_components/backbone/backbone.js', 31 | 'bower_components/backbone.declarative.views/dist/backbone.declarative.views.js', 32 | 'bower_components/marionette/lib/backbone.marionette.js', 33 | 34 | // Component under test 35 | 'src/backbone.inline.template.js', 36 | 37 | // Test helpers 38 | 'node_modules/chai-subset/lib/chai-subset.js', 39 | 'spec/chai-helpers/**/*.js', 40 | 'spec/helpers/**/*.js', 41 | 42 | // Tests 43 | 'spec/**/*.+(spec|test|tests).js' 44 | ], 45 | 46 | 47 | // list of files to exclude 48 | exclude: [ 49 | 50 | ], 51 | 52 | 53 | // test results reporter to use 54 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage', 'mocha' 55 | reporters: ['progress'], 56 | 57 | 58 | // web server port 59 | port: 9876, 60 | 61 | 62 | // enable / disable colors in the output (reporters and logs) 63 | colors: true, 64 | 65 | 66 | // level of logging 67 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 68 | logLevel: config.LOG_INFO, 69 | 70 | 71 | // enable / disable watching file and executing tests whenever any file changes 72 | autoWatch: false, 73 | 74 | 75 | // Start these browsers, currently available: 76 | // - Chrome 77 | // - ChromeCanary 78 | // - Firefox 79 | // - Opera 80 | // - Safari 81 | // - PhantomJS 82 | // - SlimerJS 83 | // - IE (Windows only) 84 | // 85 | // ATTN Interactive debugging in PhpStorm/WebStorm doesn't work with PhantomJS. Use Firefox or Chrome instead. 86 | browsers: ['PhantomJS'], 87 | 88 | 89 | // If browser does not capture in given timeout [ms], kill it 90 | captureTimeout: 60000, 91 | 92 | 93 | // Continuous Integration mode 94 | // if true, it capture browsers, run tests and exit 95 | singleRun: false 96 | }); 97 | }; 98 | -------------------------------------------------------------------------------- /web-mocha/_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mocha Spec Runner 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 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /demo/amd/views-backbone.js: -------------------------------------------------------------------------------- 1 | // views-backbone.js 2 | 3 | define( [ 4 | 5 | 'underscore', 6 | 'backbone', 7 | 'usertiming', 8 | 'local.base', 9 | 'backbone.declarative.views' 10 | 11 | ], function ( _, Backbone, performance, base ) { 12 | 13 | var ItemView = Backbone.View.extend( { 14 | 15 | initialize: function ( options ) { 16 | this.template = this.inlineTemplate.getCachedTemplate().compiled; 17 | }, 18 | 19 | render: function () { 20 | this.$el.html( this.template( this.model.attributes ) ); 21 | return this; 22 | }, 23 | 24 | appendTo: function ( $parent ) { 25 | if ( !( $parent instanceof Backbone.$ ) ) $parent = Backbone.$( $parent ); 26 | $parent.append( this.$el ); 27 | return this; 28 | } 29 | 30 | } ), 31 | 32 | ListView = Backbone.View.extend( { 33 | 34 | initialize: function ( options ) { 35 | options || ( options = {} ); // jshint ignore:line 36 | 37 | if ( options.ItemView ) this.ItemView = options.ItemView; 38 | if ( options.parent ) this.parent = options.parent; 39 | this.$parent = Backbone.$( this.parent ); 40 | }, 41 | 42 | render: function () { 43 | var duration, renderDuration, 44 | els = []; 45 | 46 | this.itemViews = []; 47 | 48 | // Start timer 49 | performance.clearMarks(); 50 | performance.clearMeasures(); 51 | performance.mark( "create-itemViews-start" ); 52 | 53 | this.collection.each( function ( model ) { 54 | // Backbone.InlineTemplate.clearCache(); 55 | var itemView = new this.ItemView( { model: model } ); 56 | itemView.render(); 57 | 58 | this.itemViews.push( itemView ); 59 | els.push( itemView.el ); 60 | }, this ); 61 | 62 | // Measure itemView creation time 63 | performance.measure( "create-itemViews", "create-itemViews-start" ); 64 | duration = performance.getEntriesByName( "create-itemViews" )[0].duration; 65 | 66 | this.$el.append( els ); 67 | this.$el.appendTo( this.$parent ); 68 | 69 | // Measure render duration time (total from beginning of itemView creation) 70 | performance.measure( "render", "create-itemViews-start" ); 71 | renderDuration = performance.getEntriesByName( "render" )[0].duration; 72 | 73 | base.eventBus.trigger( "createStats", { 74 | itemViewCount : this.itemViews.length, 75 | duration: Math.round( duration ), 76 | renderDuration: Math.round( renderDuration ) 77 | } ); 78 | }, 79 | 80 | destroy: function () { 81 | _.each( this.itemViews, function ( itemView ) { 82 | itemView.remove(); 83 | } ); 84 | 85 | this.remove(); 86 | } 87 | 88 | } ); 89 | 90 | return { 91 | ItemView: ItemView, 92 | ListView: ListView 93 | } 94 | 95 | } ); 96 | -------------------------------------------------------------------------------- /dist/backbone.inline.template.min.js: -------------------------------------------------------------------------------- 1 | // Backbone.Inline.Template, v1.0.1 2 | // Copyright (c) 2016 Michael Heim, Zeilenwechsel.de 3 | // Distributed under MIT license 4 | // http://github.com/hashchange/backbone.inline.template 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","backbone.declarative.views"],b):c?b(exports,require("underscore"),require("backbone"),require("backbone.declarative.views")):b({},_,Backbone)}(this,function(a,_,Backbone){"use strict";function b(a){return"inline"===a.data("el-definition")}function c(a){a.attr("data-bbit-internal-template-status","updated")}function d(a){return"updated"===a.data("bbit-internal-template-status")}function e(a){var b,c,d="",e=a.replace(k,"").replace(l,""),f=m.exec(e)||n.exec(e);if(!f)throw new Backbone.DeclarativeViews.TemplateError('Backbone.Inline.Template: Failed to parse template with inline `el` definition. No matching content found.\nTemplate text is "'+a+'"');f[3]?(b=f[1]+f[3],d=f[2]):b=f[0];try{c=h(b)}catch(c){throw new Backbone.DeclarativeViews.TemplateError('Backbone.Inline.Template: Failed to parse template with inline `el` definition. Extracted `el` could not be turned into a sample node.\nExtracted `el` definition string is "'+b+'", full template text is "'+a+'"')}return{templateContent:d,$elSample:c}}function f(a,b){var c={tagName:a.prop("tagName").toLowerCase(),className:h.trim(a.attr("class")),id:h.trim(a.attr("id")),otherAttributes:{}};_.each(a[0].attributes,function(a){var b=a.nodeName,d=a.nodeValue,e=void 0!==d&&"class"!==b&&"id"!==b;e&&(c.otherAttributes[a.nodeName]=d)}),"div"!==c.tagName&&b.attr("data-tag-name",c.tagName),c.className&&b.attr("data-class-name",c.className),c.id&&b.attr("data-id",c.id),_.size(c.otherAttributes)&&b.attr("data-attributes",JSON.stringify(c.otherAttributes))}function g(a){try{return 0!==i.find(a).length}catch(a){return!1}}var h=Backbone.$,i=h(document),j=Backbone.InlineTemplate={hasInlineEl:b,updateTemplateSource:!1,version:"1.0.1"},k=/^(\s*)+/,l=/(\s*)+$/,m=/(<\s*[a-zA-Z][\s\S]*?>)([\s\S]*)(<\s*\/\s*[a-zA-Z]+\s*>)/,n=/<\s*[a-zA-Z][\s\S]*?\/?\s*>/,o=Backbone.DeclarativeViews.defaults.loadTemplate;Backbone.DeclarativeViews.plugins.registerDataAttribute("el-definition"),Backbone.DeclarativeViews.plugins.registerDataAttribute("bbit-internal-template-status"),Backbone.DeclarativeViews.plugins.registerCacheAlias(j,"inlineTemplate"),Backbone.DeclarativeViews.plugins.enforceTemplateLoading(),Backbone.DeclarativeViews.defaults.loadTemplate=function(a){var i,k,l=j.custom.hasInlineEl||j.hasInlineEl||b,m=j.updateTemplateSource,n=o(a);if(d(n)||!l(n))k=n;else{try{i=e(n.html())}catch(b){throw b.message+='\nThe template was requested for template property "'+a+'"',b}if(m){if(!g(a))throw new Backbone.DeclarativeViews.TemplateError("Backbone.Inline.Template: Can't update the template container because it doesn't exist in the DOM. The template property must be a valid selector (and not, for instance, a raw HTML string). Instead, we got \""+a+'"');k=n,c(k)}else k=h(" 25 | 26 | 34 | 35 | 45 | 46 | 47 | 54 | 55 | 59 | 62 | 63 | 64 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 88 | 89 |
90 |

Content generated with Backbone and an inline el

91 |
92 |

Content generated with Marionette and an inline el

93 |
94 |

Content generated with Backbone and an el defined with data attributes

95 |
96 |

Content generated with Marionette and an el defined with data attributes

97 |
98 |
99 | 100 | 101 | 102 | 103 | 104 | 105 | 112 | 113 | 120 | 121 | 122 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /spec/helpers/data-provider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Code lifted from Leche because I didn't get it to work with Karma. 3 | * 4 | * See https://github.com/box/leche#mocha-data-provider for usage instructions. 5 | * 6 | * LIMITATION: 7 | * 8 | * - You can't use data in your dataSets which is created or populated in an outer before() or beforeEach() hook. The 9 | * hooks run _after_ withData() has been invoked. 10 | * 11 | * Tests contained in the withData callback are executed in order, though, so you can use data from outer before() and 12 | * beforeEach() hooks in there. 13 | * 14 | * To clarify the execution order, consider this setup: 15 | * 16 | * var foo=0; 17 | * 18 | * beforeEach( function () { foo = 10 } ); 19 | * 20 | * withData( [foo], function ( importedFoo ) { 21 | * 22 | * console.log( foo ); // => prints 0, beforeEach hasn't run yet 23 | * console.log( importedFoo ); // => prints 0, beforeEach hadn't run when the data set for withData 24 | * // was created 25 | * 26 | * describe( "a suite", function () { 27 | * 28 | * console.log( foo ); // => prints 0, beforeEach hasn't run yet 29 | * 30 | * beforeEach( function () { 31 | * console.log( foo ); // => prints 10, beforeEach has run 32 | * } ); 33 | * 34 | * it( "is a test", function () { 35 | * console.log( foo ); // => prints 10, beforeEach has run 36 | * } ); 37 | * 38 | * } ); 39 | * 40 | * } ); 41 | * 42 | * tldr; before/beforeEach cannot be used for withData itself, or its data sets. Refer to it only inside tests or 43 | * hooks. 44 | * 45 | * Legal 46 | * ----- 47 | * 48 | * Original file available at https://github.com/box/leche 49 | * 50 | * Copyright 51 | * (c) 2014 Box, Inc. 52 | * (c) 2015-2016 Michael Heim, Zeilenwechsel.de 53 | * 54 | * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 55 | * except in compliance with the License. You may obtain a copy of the License at 56 | * http://www.apache.org/licenses/LICENSE-2.0 57 | * 58 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 59 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 60 | * specific language governing permissions and limitations under the License. 61 | */ 62 | 63 | /** 64 | * A data provider for use with Mocha. Generates describe() blocks. Use this around a call to it() to run the test over 65 | * a series of data. 66 | * 67 | * @throws {Error} if dataset is missing or an empty array 68 | * @param {Object|Array} dataset the data to test 69 | * @param {Function} testFunction the function to call for each piece of data 70 | */ 71 | function describeWithData ( dataset, testFunction ) { 72 | _withData( describe, dataset, testFunction ); 73 | } 74 | 75 | /** 76 | * A data provider for use with Mocha. Generates it() blocks. Use this around test code which would normally be placed 77 | * into it(). Each key in the data set becomes the name of a test. 78 | * 79 | * @throws {Error} if dataset is missing or an empty array 80 | * @param {Object|Array} dataset the data to test 81 | * @param {Function} testFunction the function to call for each piece of data 82 | */ 83 | function itWithData ( dataset, testFunction ) { 84 | _withData( it, dataset, testFunction ); 85 | } 86 | 87 | /** 88 | * A data provider for use with Mocha. Use this to run tests over a series of data. 89 | * 90 | * @throws {Error} if dataset is missing or an empty array 91 | * @param {Function} mochaFunction the Mocha function to use for generating the block, ie `define` or `it` 92 | * @param {Object|Array} dataset the data to test 93 | * @param {Function} testFunction the function to call for each piece of data 94 | * @param {string} [prefix=""] prefix for each dataset name (when turned into the test name) 95 | * @param {string} [suffix=""] prefix for each dataset name (when turned into the test name) 96 | */ 97 | function _withData ( mochaFunction, dataset, testFunction, prefix, suffix ) { 98 | 99 | prefix || ( prefix = "" ); 100 | suffix || ( suffix = "" ); 101 | 102 | // check for missing or null argument 103 | if ( typeof dataset !== 'object' || dataset === null ) { 104 | throw new Error( 'First argument must be an object or non-empty array.' ); 105 | } 106 | 107 | /* 108 | * The dataset needs to be normalized so it looks like: 109 | * { 110 | * "name1": [ "data1", "data2" ], 111 | * "name2": [ "data3", "data4" ], 112 | * } 113 | */ 114 | var namedDataset = dataset; 115 | if ( dataset instanceof Array ) { 116 | 117 | // arrays must have at least one item 118 | if ( dataset.length ) { 119 | namedDataset = createNamedDataset( dataset ); 120 | } else { 121 | throw new Error( 'First argument must be an object or non-empty array.' ); 122 | } 123 | } 124 | 125 | /* 126 | * For each name, create a new describe() or it() block containing the name. 127 | * This causes the dataset info to be output into the console, making 128 | * it easier to determine which dataset caused a problem when there's an 129 | * error. 130 | */ 131 | for ( var name in namedDataset ) { 132 | if ( namedDataset.hasOwnProperty( name ) ) { 133 | //jshint loopfunc:true 134 | 135 | mochaFunction( prefix + name + suffix, (function ( name ) { 136 | return function () { 137 | 138 | var args = namedDataset[name]; 139 | 140 | if ( !(args instanceof Array) ) { 141 | args = [args]; 142 | } 143 | 144 | testFunction.apply( this, args ); 145 | }; 146 | }( name )) ); 147 | } 148 | } 149 | } 150 | 151 | /** 152 | * Converts an array into an object whose keys are a string representation of the each array item and whose values are 153 | * each array item. This is to normalize the information into an object so that other operations can assume objects are 154 | * always used. For an array like this: 155 | * 156 | * [ "foo", "bar" ] 157 | * 158 | * It creates an object like this: 159 | * 160 | * { "foo": "foo", "bar": "bar" } 161 | * 162 | * If there are duplicate values in the array, only the last value is represented in the resulting object. 163 | * 164 | * @param {Array} array the array to convert 165 | * @returns {Object} an object representing the array 166 | * @private 167 | */ 168 | function createNamedDataset ( array ) { 169 | var result = {}; 170 | 171 | for ( var i = 0, len = array.length; i < len; i++ ) { 172 | result[array[i].toString()] = array[i]; 173 | } 174 | 175 | return result; 176 | } 177 | 178 | 179 | -------------------------------------------------------------------------------- /spec/helpers/fixture-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates the inner content of a template, for use _inside of_ the `el`. 3 | * 4 | * @param {string} varStartDelimiter start delimiter of a template variable (e.g. "{{" for Mustache) 5 | * @param {string} varEndDelimiter end delimiter of a template variable (e.g. "}}" for Mustache) 6 | * @param {object} [options] 7 | * @param {string} [options.indentation=""] a string of whitespace, e.g. " " or "" (no indentation) 8 | * @param {string} [options.insertion=""] an additional string which is inserted somewhere in the middle of the 9 | * content (if left undefined, a blank line, plus insertion, appears instead) 10 | * @returns {string} 11 | */ 12 | function createInnerContent ( varStartDelimiter, varEndDelimiter, options ) { 13 | var indent = options && options.indentation || "", 14 | insert = options && options.insertion || "", 15 | lines = [ 16 | '

This is a %%header&&

', 17 | 'Some random %%text&& with different line breaks.


', 18 | insert, 19 | '
', 20 | '
%%dd_name&&
', 21 | '
%%dd_content&&
', 22 | '
' 23 | ], 24 | 25 | innerContent = _.map( lines, function ( line ) { 26 | return indent + line; 27 | } ).join( "\n" ); 28 | 29 | return innerContent.replace( /%%/g, varStartDelimiter ).replace( /&&/g, varEndDelimiter ); 30 | } 31 | 32 | /** 33 | * Creates a template node for a given ID and content and returns it as a jQuery object. 34 | * 35 | * Does not append it to the DOM. 36 | * 37 | * @param {string} id template ID 38 | * @param {string} content content of the template node 39 | * @param {Object} [attributes] additional attributes of the template node, e.g. { "data-el-definition": "inline" } 40 | * @param {string} [tag="script"] template tag, can be "script" or "template" 41 | * @returns {jQuery} 42 | */ 43 | function createTemplateNode ( id, content, attributes, tag ) { 44 | 45 | var templateTag = tag || "script", 46 | templateAttributes = _.extend( { 47 | id: id, 48 | type: "text/x-template" 49 | }, attributes || {} ); 50 | 51 | return $( "<" + templateTag + " />" ) 52 | .attr( templateAttributes ) 53 | .text( content ); 54 | } 55 | 56 | /** 57 | * Creates the HTML of an inline template and returns the characteristics of the template, including the full HTML, in a 58 | * hash. 59 | * 60 | * A note on boolean attributes: 61 | * 62 | * A boolean attribute can be passed in as part of elDefinition.attributes if it is supposed to appear in the source as 63 | * a key-value assignment. In accordance with the spec, a boolean attribute can be defined with an empty string value, 64 | * or with the attribute name repeated as the value:

") or "" (for style "

") 91 | * @param {string} [elFormatting.quoteStyle='"'] the quote style used for enclosing attribute values in the HTML, can be "'", '"', or "" (no quotes around attribute values!) 92 | * @param {string} [elFormatting.extraSpace=""] redundant whitespace to be inserted into the el tag itself (e.g. between attributes), and outside of it 93 | * (before the tag is opened, and after it is closed) 94 | * 95 | * @returns {TestInlineTemplateCharacteristics} 96 | */ 97 | function createInlineTemplate ( elDefinition, elContent, elFormatting ) { 98 | var _elContent = elContent || "", 99 | _elFormatting = elFormatting || {}, 100 | 101 | tagName = elDefinition.tagName, 102 | attrs = elDefinition.attributes || {}, 103 | className = elDefinition.className || attrs.className, 104 | id = elDefinition.id || attrs.id, 105 | 106 | isSelfClosing = _elFormatting.isSelfClosing, 107 | selfClosingChar = isSelfClosing && _elFormatting.selfClosingChar || "", 108 | 109 | quoteStyle = _elFormatting.quoteStyle !== undefined ? _elFormatting.quoteStyle : '"', 110 | extraSpace = _elFormatting.extraSpace || "", 111 | 112 | customKeyValueAttributes = _.omit( attrs, "className", "id" ), 113 | allKeyValueAttributes = _.extend( {}, customKeyValueAttributes, { className: className, id: id } ), 114 | booleanAttributes = elDefinition.booleanAttributes || [], 115 | 116 | keyValueAttributeString = _.reduce( allKeyValueAttributes, function ( reduced, attrValue, attrName ) { 117 | var name = attrName === "className" ? "class" : attrName; 118 | if ( attrValue !== undefined ) { 119 | reduced += " " + extraSpace + name + extraSpace + "=" + extraSpace + quoteStyle + attrValue + quoteStyle; 120 | } 121 | return reduced; 122 | }, "" ), 123 | 124 | attributeString = _.reduce( booleanAttributes, function ( reduced, booleanAttribute ) { 125 | return reduced + " " + extraSpace + booleanAttribute; 126 | }, keyValueAttributeString ), 127 | 128 | elStartTag = extraSpace + "<" + tagName + attributeString + extraSpace + selfClosingChar + ">", 129 | elEndTag = isSelfClosing ? extraSpace : "" + extraSpace, 130 | templateNodeInnerHtml = elStartTag + _elContent + elEndTag, 131 | 132 | booleanAttributesKeyValue = _.reduce( booleanAttributes, function ( hash, booleanAttribute ) { 133 | hash[booleanAttribute] = ""; 134 | return hash; 135 | }, {} ), 136 | allCustomAttributes = _.extend( {}, customKeyValueAttributes, booleanAttributesKeyValue ), 137 | fullAttributes = _.extend( {}, allKeyValueAttributes, booleanAttributesKeyValue ); 138 | 139 | if ( isSelfClosing && _elContent ) throw new Error( "createInlineTemplate: Inconsistent setup. A self-closing el tag can't have inner content, but both have been passed as arguments" ); 140 | 141 | return { 142 | html: { 143 | fullContent: templateNodeInnerHtml, 144 | elStartTag: elStartTag, 145 | elEndTag: elEndTag 146 | }, 147 | 148 | el: { 149 | tagName: tagName, 150 | className: className, 151 | id: id, 152 | attributes: _.size( allCustomAttributes ) ? allCustomAttributes : undefined, 153 | fullAttributes: _.size( fullAttributes ) ? fullAttributes : undefined 154 | } 155 | } 156 | } 157 | 158 | /** 159 | * @name TestInlineTemplateCharacteristics 160 | * @type {Object} 161 | * 162 | * @property {TestTemplateHtml} html 163 | * @property {TestTemplateEl} el 164 | */ 165 | 166 | /** 167 | * @name TestTemplateHtml 168 | * @type {Object} 169 | * 170 | * @property {string} fullContent 171 | * @property {string} elStartTag 172 | * @property {string} elEndTag empty string for self-closing tags 173 | */ 174 | 175 | /** 176 | * @name TestTemplateEl 177 | * @type {Object} 178 | * 179 | * @property {string} tagName 180 | * @property {string|undefined} className 181 | * @property {string|undefined} id 182 | * @property {Object|undefined} attributes 183 | * @property {Object|undefined} fullAttributes 184 | */ 185 | -------------------------------------------------------------------------------- /spec/template.selection.spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it */ 2 | (function () { 3 | "use strict"; 4 | 5 | describe( 'Template selection', function () { 6 | 7 | var expected, view, 8 | $head = $( "head" ); 9 | 10 | afterEach( function () { 11 | expected = view = undefined; 12 | Backbone.InlineTemplate.clearCache(); 13 | } ); 14 | 15 | var templateIds, elDataAttributes, elDefinition, inlineTemplateA, inlineTemplateB, $templates; 16 | 17 | beforeEach( function () { 18 | var elContentA = createInnerContent( "{{", "}}" ), 19 | elContentB = createInnerContent( "<%= ", " %>"), 20 | 21 | noElDefinition = { 22 | tagName: undefined, 23 | className: undefined, 24 | id: undefined, 25 | attributes: undefined 26 | }; 27 | 28 | elDefinition = { 29 | tagName: "p", 30 | className: "fooClass barClass", 31 | id: "fooId", 32 | attributes: { lang: "fr", contenteditable: "true" } 33 | }; 34 | 35 | elDataAttributes = propertiesToDataAttributes( elDefinition ); 36 | 37 | templateIds = { 38 | defaultMarker: { 39 | A: _.uniqueId( "template_" ), 40 | B: _.uniqueId( "template_" ) 41 | }, 42 | customMarker: { 43 | A: _.uniqueId( "template_" ), 44 | B: _.uniqueId( "template_" ) 45 | }, 46 | noMarker: { 47 | A: _.uniqueId( "template_" ), 48 | B: _.uniqueId( "template_" ) 49 | } 50 | }; 51 | 52 | inlineTemplateA = createInlineTemplate( elDefinition, elContentA ); 53 | inlineTemplateB = createInlineTemplate( elDefinition, elContentB ); 54 | 55 | $templates = { 56 | defaultMarker: { 57 | A: createTemplateNode( templateIds.defaultMarker.A, inlineTemplateA.html.fullContent, { "data-el-definition": "inline" } ) 58 | .appendTo( $head ), 59 | B: createTemplateNode( templateIds.defaultMarker.B, inlineTemplateB.html.fullContent, { "data-el-definition": "inline" } ) 60 | .appendTo( $head ) 61 | }, 62 | customMarker: { 63 | A: createTemplateNode( templateIds.customMarker.A, inlineTemplateA.html.fullContent, { type: "text/x-inline-template" } ) 64 | .appendTo( $head ), 65 | B: createTemplateNode( templateIds.customMarker.B, inlineTemplateB.html.fullContent, { type: "text/x-inline-template" } ) 66 | .appendTo( $head ) 67 | }, 68 | noMarker: { 69 | A: createTemplateNode( templateIds.noMarker.A, inlineTemplateA.html.fullContent ) 70 | .appendTo( $head ), 71 | B: createTemplateNode( templateIds.noMarker.B, inlineTemplateB.html.fullContent ) 72 | .appendTo( $head ) 73 | } 74 | }; 75 | 76 | expected = { 77 | processed: { 78 | cacheA: _.extend( {}, elDefinition, { html: elContentA, compiled: undefined } ), 79 | cacheB: _.extend( {}, elDefinition, { html: elContentB, compiled: undefined } ) 80 | }, 81 | unprocessed: { 82 | cacheA: _.extend( {}, noElDefinition, { html: inlineTemplateA.html.fullContent, compiled: undefined } ), 83 | cacheB: _.extend( {}, noElDefinition, { html: inlineTemplateB.html.fullContent, compiled: undefined } ) 84 | } 85 | }; 86 | } ); 87 | 88 | afterEach( function () { 89 | if ( $templates ) { 90 | if ( $templates.defaultMarker ) _.invoke( $templates.defaultMarker, "remove" ); 91 | if ( $templates.customMarker ) _.invoke( $templates.customMarker, "remove" ); 92 | if ( $templates.noMarker ) _.invoke( $templates.noMarker, "remove" ); 93 | } 94 | 95 | templateIds = elDataAttributes = elDefinition = inlineTemplateA = inlineTemplateB = $templates = undefined; 96 | } ); 97 | 98 | describe( 'When the default marker is used, Backbone.Inline.Template', function () { 99 | 100 | it( 'processes all template nodes which are marked for processing with the default marker', function () { 101 | var normalizedCache; 102 | 103 | // Template A 104 | view = new Backbone.View( { template: "#" + templateIds.defaultMarker.A } ); 105 | normalizedCache = normalizeCacheEntry( view.inlineTemplate.getCachedTemplate() ); 106 | 107 | expect( view ).to.have.exactElProperties( elDefinition ); 108 | expect( normalizedCache ).to.eql( expected.processed.cacheA ); 109 | 110 | // Template B 111 | view = new Backbone.View( { template: "#" + templateIds.defaultMarker.B } ); 112 | normalizedCache = normalizeCacheEntry( view.inlineTemplate.getCachedTemplate() ); 113 | 114 | expect( view ).to.have.exactElProperties( elDefinition ); 115 | expect( normalizedCache ).to.eql( expected.processed.cacheB ); 116 | } ); 117 | 118 | it( 'does not process template nodes which are not marked for processing', function () { 119 | var normalizedCache; 120 | 121 | // Template A 122 | view = new Backbone.View( { template: "#" + templateIds.noMarker.A } ); 123 | normalizedCache = normalizeCacheEntry( view.inlineTemplate.getCachedTemplate() ); 124 | 125 | expect( view ).to.have.defaultEl; 126 | expect( normalizedCache ).to.eql( expected.unprocessed.cacheA ); 127 | 128 | // Template B 129 | view = new Backbone.View( { template: "#" + templateIds.noMarker.B } ); 130 | normalizedCache = normalizeCacheEntry( view.inlineTemplate.getCachedTemplate() ); 131 | 132 | expect( view ).to.have.defaultEl; 133 | expect( normalizedCache ).to.eql( expected.unprocessed.cacheB ); 134 | } ); 135 | 136 | } ); 137 | 138 | describe( 'When a custom marker is used, Backbone.Inline.Template', function () { 139 | 140 | var _hasInlineEl; 141 | 142 | before( function () { 143 | _hasInlineEl = Backbone.InlineTemplate.hasInlineEl; 144 | 145 | Backbone.InlineTemplate.hasInlineEl = function ( $template ) { 146 | return $template.attr( "type" ) === "text/x-inline-template"; 147 | }; 148 | } ); 149 | 150 | after( function () { 151 | Backbone.InlineTemplate.hasInlineEl = _hasInlineEl; 152 | } ); 153 | 154 | it( 'processes all template nodes which are marked for processing with the custom marker', function () { 155 | var normalizedCache; 156 | 157 | // Template A 158 | view = new Backbone.View( { template: "#" + templateIds.customMarker.A } ); 159 | normalizedCache = normalizeCacheEntry( view.inlineTemplate.getCachedTemplate() ); 160 | 161 | expect( view ).to.have.exactElProperties( elDefinition ); 162 | expect( normalizedCache ).to.eql( expected.processed.cacheA ); 163 | 164 | // Template B 165 | view = new Backbone.View( { template: "#" + templateIds.customMarker.B } ); 166 | normalizedCache = normalizeCacheEntry( view.inlineTemplate.getCachedTemplate() ); 167 | 168 | expect( view ).to.have.exactElProperties( elDefinition ); 169 | expect( normalizedCache ).to.eql( expected.processed.cacheB ); 170 | } ); 171 | 172 | it( 'does not process template nodes which are marked with the default marker', function () { 173 | var normalizedCache; 174 | 175 | // Template A 176 | view = new Backbone.View( { template: "#" + templateIds.defaultMarker.A } ); 177 | normalizedCache = normalizeCacheEntry( view.inlineTemplate.getCachedTemplate() ); 178 | 179 | expect( view ).to.have.defaultEl; 180 | expect( normalizedCache ).to.eql( expected.unprocessed.cacheA ); 181 | 182 | // Template B 183 | view = new Backbone.View( { template: "#" + templateIds.defaultMarker.B } ); 184 | normalizedCache = normalizeCacheEntry( view.inlineTemplate.getCachedTemplate() ); 185 | 186 | expect( view ).to.have.defaultEl; 187 | expect( normalizedCache ).to.eql( expected.unprocessed.cacheB ); 188 | } ); 189 | 190 | it( 'does not process template nodes which are not marked for processing', function () { 191 | var normalizedCache; 192 | 193 | // Template A 194 | view = new Backbone.View( { template: "#" + templateIds.noMarker.A } ); 195 | normalizedCache = normalizeCacheEntry( view.inlineTemplate.getCachedTemplate() ); 196 | 197 | expect( view ).to.have.defaultEl; 198 | expect( normalizedCache ).to.eql( expected.unprocessed.cacheA ); 199 | 200 | // Template B 201 | view = new Backbone.View( { template: "#" + templateIds.noMarker.B } ); 202 | normalizedCache = normalizeCacheEntry( view.inlineTemplate.getCachedTemplate() ); 203 | 204 | expect( view ).to.have.defaultEl; 205 | expect( normalizedCache ).to.eql( expected.unprocessed.cacheB ); 206 | } ); 207 | 208 | } ); 209 | 210 | } ); 211 | 212 | })(); -------------------------------------------------------------------------------- /demo/amd/rjs/output/parts/app.js: -------------------------------------------------------------------------------- 1 | // base.js 2 | 3 | define( 'local.base',[ 4 | 5 | 'underscore', 6 | 'backbone', 7 | 'usertiming', 8 | 'backbone.declarative.views' 9 | 10 | ], function ( _, Backbone, performance ) { 11 | 12 | var eventBus = _.extend( {}, Backbone.Events ), 13 | 14 | Model = Backbone.Model.extend(), 15 | 16 | Collection = Backbone.Collection.extend( 17 | { model: Model }, 18 | { 19 | create: function ( modelCount ) { 20 | var i, 21 | collection = new Collection(), 22 | models = []; 23 | 24 | for ( i = 0; i < modelCount; i++ ) models.push( new collection.model( { number: i + 1 } ) ); 25 | collection.reset( models ); 26 | 27 | return collection; 28 | } 29 | } 30 | ), 31 | 32 | StatsView = Backbone.View.extend( { 33 | 34 | template: "#stats-template", 35 | 36 | parent: ".stats", 37 | 38 | initialize: function ( options ) { 39 | var compiledTemplate = this.inlineTemplate.getCachedTemplate().compiled; 40 | this.template = compiledTemplate || _.template( this.inlineTemplate.getCachedTemplate(). html ); 41 | 42 | options || ( options = {} ); 43 | 44 | if ( options.parent ) this.parent = options.parent; 45 | this.$parent = Backbone.$( this.parent ); 46 | this.$el.appendTo( this.$parent ); 47 | 48 | this.listenTo( eventBus, "createStats", this.render ); 49 | }, 50 | 51 | render: function ( stats ) { 52 | stats.totalDuration = Math.round( this.getTotalDuration() ); 53 | this.$el.html( this.template( stats ) ); 54 | }, 55 | 56 | getTotalDuration: function () { 57 | // Query the document height. This is to assess the true total render time, including the painting. 58 | // The calculation of the document height should be blocked by the browser until all item elements have 59 | // been painted. 60 | var docHeight = $( document ).height(); 61 | 62 | performance.measure( "paint", "create-itemViews-start" ); 63 | return performance.getEntriesByName( "paint" )[0].duration; 64 | } 65 | 66 | } ); 67 | 68 | Backbone.DeclarativeViews.custom.compiler = _.template; 69 | 70 | return { 71 | Model: Model, 72 | Collection: Collection, 73 | StatsView: StatsView, 74 | eventBus: eventBus 75 | } 76 | 77 | } ); 78 | 79 | // views-backbone.js 80 | 81 | define( 'local.views-backbone',[ 82 | 83 | 'underscore', 84 | 'backbone', 85 | 'usertiming', 86 | 'local.base', 87 | 'backbone.declarative.views' 88 | 89 | ], function ( _, Backbone, performance, base ) { 90 | 91 | var ItemView = Backbone.View.extend( { 92 | 93 | initialize: function ( options ) { 94 | this.template = this.inlineTemplate.getCachedTemplate().compiled; 95 | }, 96 | 97 | render: function () { 98 | this.$el.html( this.template( this.model.attributes ) ); 99 | return this; 100 | }, 101 | 102 | appendTo: function ( $parent ) { 103 | if ( !( $parent instanceof Backbone.$ ) ) $parent = Backbone.$( $parent ); 104 | $parent.append( this.$el ); 105 | return this; 106 | } 107 | 108 | } ), 109 | 110 | ListView = Backbone.View.extend( { 111 | 112 | initialize: function ( options ) { 113 | options || ( options = {} ); // jshint ignore:line 114 | 115 | if ( options.ItemView ) this.ItemView = options.ItemView; 116 | if ( options.parent ) this.parent = options.parent; 117 | this.$parent = Backbone.$( this.parent ); 118 | }, 119 | 120 | render: function () { 121 | var duration, renderDuration, 122 | els = []; 123 | 124 | this.itemViews = []; 125 | 126 | // Start timer 127 | performance.clearMarks(); 128 | performance.clearMeasures(); 129 | performance.mark( "create-itemViews-start" ); 130 | 131 | this.collection.each( function ( model ) { 132 | // Backbone.InlineTemplate.clearCache(); 133 | var itemView = new this.ItemView( { model: model } ); 134 | itemView.render(); 135 | 136 | this.itemViews.push( itemView ); 137 | els.push( itemView.el ); 138 | }, this ); 139 | 140 | // Measure itemView creation time 141 | performance.measure( "create-itemViews", "create-itemViews-start" ); 142 | duration = performance.getEntriesByName( "create-itemViews" )[0].duration; 143 | 144 | this.$el.append( els ); 145 | this.$el.appendTo( this.$parent ); 146 | 147 | // Measure render duration time (total from beginning of itemView creation) 148 | performance.measure( "render", "create-itemViews-start" ); 149 | renderDuration = performance.getEntriesByName( "render" )[0].duration; 150 | 151 | base.eventBus.trigger( "createStats", { 152 | itemViewCount : this.itemViews.length, 153 | duration: Math.round( duration ), 154 | renderDuration: Math.round( renderDuration ) 155 | } ); 156 | }, 157 | 158 | destroy: function () { 159 | _.each( this.itemViews, function ( itemView ) { 160 | itemView.remove(); 161 | } ); 162 | 163 | this.remove(); 164 | } 165 | 166 | } ); 167 | 168 | return { 169 | ItemView: ItemView, 170 | ListView: ListView 171 | } 172 | 173 | } ); 174 | 175 | // views-marionette.js 176 | 177 | define( 'local.views-marionette',[ 178 | 179 | 'underscore', 180 | 'backbone', 181 | 'marionette', 182 | 'usertiming', 183 | 'local.base', 184 | 'backbone.declarative.views' 185 | 186 | ], function ( _, Backbone, Marionette, performance, base ) { 187 | 188 | var ItemView = Marionette.ItemView.extend( { 189 | 190 | appendTo: function ( $parent ) { 191 | if ( !( $parent instanceof Backbone.$ ) ) $parent = Backbone.$( $parent ); 192 | $parent.append( this.$el ); 193 | return this; 194 | } 195 | 196 | } ), 197 | 198 | ListView = Marionette.CollectionView.extend( { 199 | 200 | initialize: function ( options ) { 201 | options || ( options = {} ); // jshint ignore:line 202 | 203 | if ( options.parent ) this.parent = options.parent; 204 | this.$parent = Backbone.$( this.parent ); 205 | }, 206 | 207 | onBeforeRender: function () { 208 | // Start timer 209 | performance.clearMarks(); 210 | performance.clearMeasures(); 211 | performance.mark( "create-itemViews-start" ); 212 | }, 213 | 214 | onRender: function () { 215 | var duration; 216 | 217 | // Measure the time it took to create the itemViews 218 | performance.measure( "create-itemViews", "create-itemViews-start" ); 219 | duration = performance.getEntriesByName( "create-itemViews" )[0].duration; 220 | 221 | if ( ! this.$el.parent().length ) this.$el.appendTo( this.$parent ); 222 | 223 | base.eventBus.trigger( "createStats", { itemViewCount : this.children.length, duration: Math.round( duration ) } ); 224 | } 225 | 226 | } ); 227 | 228 | return { 229 | ItemView: ItemView, 230 | ListView: ListView 231 | } 232 | 233 | } ); 234 | 235 | // main.js 236 | 237 | require( [ 238 | 239 | 'backbone', 240 | 'local.base', 241 | 'local.views-backbone', 242 | 'local.views-marionette', 243 | 244 | 'backbone.inline.template' 245 | 246 | ], function ( Backbone, base, backboneViews, marionetteViews ) { 247 | 248 | // To make the inline `el` magic work with Marionette, the original template must be updated (modified). 249 | // Backbone-only views don't need this. Try it by commenting it out. 250 | Backbone.InlineTemplate.updateTemplateSource = true; 251 | 252 | var count = 10, 253 | 254 | BackboneListItemView = backboneViews.ItemView.extend( { template: "#item-template" } ), 255 | MarionetteListItemView = marionetteViews.ItemView.extend( { template: "#item-template" } ), 256 | 257 | backboneListView = new backboneViews.ListView( { 258 | template: "#list-template-backbone", 259 | parent: ".container.backbone", 260 | ItemView: BackboneListItemView, 261 | collection: base.Collection.create( count ) 262 | } ), 263 | 264 | marionetteListView = new marionetteViews.ListView( { 265 | template: "#list-template-marionette", 266 | childView: MarionetteListItemView, 267 | parent: ".container.marionette", 268 | collection: base.Collection.create( count ) 269 | } ), 270 | 271 | backboneReportView = new backboneViews.ItemView( { 272 | model: new Backbone.Model( { 273 | itemViewCount: count, 274 | framework: "Backbone" 275 | } ), 276 | template: "#report-template-backbone" 277 | } ), 278 | 279 | marionetteReportView = new marionetteViews.ItemView( { 280 | model: new Backbone.Model( { 281 | itemViewCount: count, 282 | framework: "Marionette" 283 | } ), 284 | template: "#report-template-marionette" 285 | } ); 286 | 287 | backboneListView.render(); 288 | marionetteListView.render(); 289 | 290 | backboneReportView.render().appendTo( ".report.backbone" ); 291 | marionetteReportView.render().appendTo( ".report.marionette" ); 292 | 293 | } ); 294 | 295 | define("local.main", function(){}); 296 | 297 | -------------------------------------------------------------------------------- /src/backbone.inline.template.js: -------------------------------------------------------------------------------- 1 | ;( function ( root, factory ) { 2 | "use strict"; 3 | 4 | // UMD for a Backbone plugin. Supports AMD, Node.js, CommonJS and globals. 5 | // 6 | // - Code lives in the Backbone 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", "backbone.declarative.views" ], factory ); 21 | 22 | } else if ( supportsExports ) { 23 | 24 | // Node module, CommonJS module 25 | factory( exports, require( "underscore" ), require( "backbone" ), require( "backbone.declarative.views" ) ); 26 | 27 | } else { 28 | 29 | // Global (browser or Rhino) 30 | factory( {}, _, Backbone ); 31 | 32 | } 33 | 34 | }( this, function ( exports, _, Backbone ) { 35 | "use strict"; 36 | 37 | var $ = Backbone.$, 38 | $document = $( document ), 39 | pluginNamespace = Backbone.InlineTemplate = { 40 | hasInlineEl: _hasInlineEl, 41 | updateTemplateSource: false, 42 | version: "__COMPONENT_VERSION_PLACEHOLDER__" 43 | }, 44 | 45 | rxLeadingComments = /^(\s*)+/, 46 | rxTrailingComments = /(\s*)+$/, 47 | rxOutermostHtmlTagWithContent = /(<\s*[a-zA-Z][\s\S]*?>)([\s\S]*)(<\s*\/\s*[a-zA-Z]+\s*>)/, 48 | rxSelfClosingHtmlTag = /<\s*[a-zA-Z][\s\S]*?\/?\s*>/, 49 | 50 | bbdvLoadTemplate = Backbone.DeclarativeViews.defaults.loadTemplate; 51 | 52 | // 53 | // Initialization 54 | // -------------- 55 | 56 | Backbone.DeclarativeViews.plugins.registerDataAttribute( "el-definition" ); 57 | Backbone.DeclarativeViews.plugins.registerDataAttribute( "bbit-internal-template-status" ); 58 | Backbone.DeclarativeViews.plugins.registerCacheAlias( pluginNamespace, "inlineTemplate" ); 59 | Backbone.DeclarativeViews.plugins.enforceTemplateLoading(); 60 | 61 | // 62 | // Template loader 63 | //---------------- 64 | 65 | Backbone.DeclarativeViews.defaults.loadTemplate = function ( templateProperty ) { 66 | var parsedTemplateData, $resultTemplate, 67 | 68 | // Check Backbone.InlineTemplate.custom.hasInlineEl first, even though it is undocumented, to catch 69 | // accidental assignments. 70 | hasInlineEl = pluginNamespace.custom.hasInlineEl || pluginNamespace.hasInlineEl || _hasInlineEl, 71 | updateTemplateContainer = pluginNamespace.updateTemplateSource, 72 | 73 | $inputTemplate = bbdvLoadTemplate( templateProperty ); 74 | 75 | if ( _isMarkedAsUpdated( $inputTemplate ) || !hasInlineEl( $inputTemplate ) ) { 76 | 77 | // No inline el definition. Just return the template as is. 78 | $resultTemplate = $inputTemplate; 79 | 80 | } else { 81 | 82 | // Parse the template data. 83 | // 84 | // NB Errors are not handled here and bubble up further. Try-catch is just used to enhance the error message 85 | // for easier debugging. 86 | try { 87 | 88 | parsedTemplateData = _parseTemplateHtml( $inputTemplate.html() ); 89 | 90 | } catch ( err ) { 91 | err.message += '\nThe template was requested for template property "' + templateProperty + '"'; 92 | throw err; 93 | } 94 | 95 | if ( updateTemplateContainer ) { 96 | // For updating the template container, it has to be a node in the DOM. Throw an error if it has been 97 | // passed in as a raw HTML string. 98 | if ( !existsInDOM( templateProperty ) ) throw new Backbone.DeclarativeViews.TemplateError( "Backbone.Inline.Template: Can't update the template container because it doesn't exist in the DOM. The template property must be a valid selector (and not, for instance, a raw HTML string). Instead, we got \"" + templateProperty + '"' ); 99 | 100 | $resultTemplate = $inputTemplate; 101 | 102 | // The template is updated and the inline `el` removed. Set a flag on the template to make sure the 103 | // template is never processed again as having an inline `el`. 104 | _markAsUpdated( $resultTemplate ); 105 | } else { 106 | // No updating of the input template. Create a new template node which will stay out of the DOM, but is 107 | // passed to the cache. 108 | $resultTemplate = $( "', 180 | // relative doesn't seem to have any effect, ever 181 | relative: true, 182 | // appRoot is a misnomer for "strip out this prefix from the file path before inserting", 183 | // should be stripPrefix 184 | appRoot: '' 185 | }, 186 | interactive_spec: { 187 | options: { 188 | startTag: '', 189 | endTag: '' 190 | }, 191 | files: { 192 | // the target file is changed in place; for generating copies, run preprocess first 193 | 'web-mocha/index.html': ['spec/**/*.+(spec|test|tests).js'] 194 | } 195 | }, 196 | interactive_sinon: { 197 | options: { 198 | startTag: '', 199 | endTag: '' 200 | }, 201 | files: { 202 | // the target file is changed in place; for generating copies, run preprocess first 203 | // 204 | // The util/core.js file must be loaded first, and typeof.js must be loaded before match.js. 205 | // 206 | // mock.js must be loaded last (specifically, after spy.js). For the pattern achieving it, see 207 | // http://gruntjs.com/configuring-tasks#globbing-patterns 208 | 'web-mocha/index.html': [ 209 | SINON_SOURCE_DIR + 'util/core.js', 210 | SINON_SOURCE_DIR + 'typeof.js', 211 | SINON_SOURCE_DIR + '**/*.js', 212 | '!' + SINON_SOURCE_DIR + 'mock.js', 213 | SINON_SOURCE_DIR + 'mock.js' 214 | ] 215 | } 216 | } 217 | }, 218 | 219 | requirejs : { 220 | unifiedBuild : { 221 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/unified/build-config.js', false ) 222 | }, 223 | splitBuildVendor : { 224 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/vendor-config.js', false ) 225 | }, 226 | splitBuildApp : { 227 | options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/app-config.js', false ) 228 | } 229 | }, 230 | 231 | // Use focus to run Grunt watch with a hand-picked set of simultaneous watch targets. 232 | focus: { 233 | demo: { 234 | include: ['livereloadDemo'] 235 | }, 236 | demoCi: { 237 | include: ['build', 'livereloadDemo'] 238 | }, 239 | demoCiDirty: { 240 | include: ['buildDirty', 'livereloadDemo'] 241 | } 242 | }, 243 | 244 | // Use watch to monitor files for changes, and to kick off a task then. 245 | watch: { 246 | options: { 247 | nospawn: false 248 | }, 249 | // Live-reloads the web page when the source files or the spec files change. Meant for test pages. 250 | livereloadTest: { 251 | options: { 252 | livereload: LIVERELOAD_PORT 253 | }, 254 | files: WATCHED_FILES_SRC.concat( WATCHED_FILES_SPEC ) 255 | }, 256 | // Live-reloads the web page when the dist files or the demo files change. Meant for demo pages. 257 | livereloadDemo: { 258 | options: { 259 | livereload: LIVERELOAD_PORT 260 | }, 261 | files: WATCHED_FILES_DEMO.concat( WATCHED_FILES_DIST ) 262 | }, 263 | // Runs the "build" task (ie, runs linter and tests, then compiles the dist files) when the source files or the 264 | // spec files change. Meant for continuous integration tasks ("ci", "demo-ci"). 265 | build: { 266 | tasks: ['build'], 267 | files: WATCHED_FILES_SRC.concat( WATCHED_FILES_SPEC ) 268 | }, 269 | // Runs the "build-dirty" task (ie, compiles the dist files without running linter and tests) when the source 270 | // files change. Meant for "dirty" continuous integration tasks ("ci-dirty", "demo-ci-dirty"). 271 | buildDirty: { 272 | tasks: ['build-dirty'], 273 | files: WATCHED_FILES_SRC 274 | } 275 | }, 276 | 277 | // Spins up a web server. 278 | connect: { 279 | options: { 280 | port: HTTP_PORT, 281 | // For restricting access to localhost only, change the hostname from '*' to 'localhost' 282 | hostname: '*', 283 | open: true, 284 | base: '.' 285 | }, 286 | livereload: { 287 | livereload: LIVERELOAD_PORT 288 | }, 289 | test: { 290 | options: { 291 | open: 'http://localhost:<%= connect.options.port %>/web-mocha/', 292 | livereload: LIVERELOAD_PORT 293 | } 294 | }, 295 | testNoReload: { 296 | options: { 297 | open: 'http://localhost:<%= connect.options.port %>/web-mocha/', 298 | keepalive: true 299 | } 300 | }, 301 | demo: { 302 | options: { 303 | open: 'http://localhost:<%= connect.options.port %>/demo/', 304 | livereload: LIVERELOAD_PORT 305 | } 306 | } 307 | }, 308 | 309 | replace: { 310 | version: { 311 | src: ['bower.json', 'package.json'], 312 | overwrite: true, 313 | replacements: [{ 314 | from: /"version"\s*:\s*"((\d+\.\d+\.)(\d+))"\s*,/, 315 | to: function (matchedWord, index, fullText, regexMatches) { 316 | var version = grunt.option('inc') ? regexMatches[1] + (parseInt(regexMatches[2], 10) + 1) : grunt.option('to'); 317 | 318 | 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'); 319 | if (typeof version !== "string") grunt.fail.fatal('Version number is not a string. Provide a semantic version number, e.g. --to=1.2.3'); 320 | 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'); 321 | 322 | grunt.log.writeln('Modifying file: Changing the version number from ' + regexMatches[0] + ' to ' + version); 323 | return '"version": "' + version + '",'; 324 | } 325 | }] 326 | } 327 | }, 328 | getver: { 329 | files: ['bower.json', 'package.json'] 330 | } 331 | }); 332 | 333 | grunt.loadNpmTasks('grunt-preprocess'); 334 | grunt.loadNpmTasks('grunt-contrib-concat'); 335 | grunt.loadNpmTasks('grunt-contrib-jshint'); 336 | grunt.loadNpmTasks('grunt-contrib-uglify'); 337 | grunt.loadNpmTasks('grunt-karma'); 338 | grunt.loadNpmTasks('grunt-contrib-watch'); 339 | grunt.loadNpmTasks('grunt-contrib-connect'); 340 | grunt.loadNpmTasks('grunt-sails-linker'); 341 | grunt.loadNpmTasks('grunt-text-replace'); 342 | grunt.loadNpmTasks('grunt-contrib-requirejs'); 343 | grunt.loadNpmTasks('grunt-focus'); 344 | 345 | grunt.registerTask('lint', ['jshint:components']); 346 | grunt.registerTask('hint', ['jshint:components']); // alias 347 | grunt.registerTask('test', ['jshint:components', 'karma:test']); 348 | grunt.registerTask('webtest', ['preprocess:interactive', 'sails-linker:interactive_sinon', 'sails-linker:interactive_spec', 'connect:testNoReload']); 349 | grunt.registerTask('interactive', ['preprocess:interactive', 'sails-linker:interactive_sinon', 'sails-linker:interactive_spec', 'connect:test', 'watch:livereloadTest']); 350 | grunt.registerTask('demo', ['connect:demo', 'focus:demo']); 351 | grunt.registerTask('build', ['jshint:components', 'karma:build', 'preprocess:build', 'concat', 'uglify', 'jshint:concatenated', 'requirejs']); 352 | grunt.registerTask('ci', ['build', 'watch:build']); 353 | grunt.registerTask('setver', ['replace:version']); 354 | grunt.registerTask('getver', function () { 355 | grunt.config.get('getver.files').forEach(function (file) { 356 | var config = grunt.file.readJSON(file); 357 | grunt.log.writeln('Version number in ' + file + ': ' + config.version); 358 | }); 359 | }); 360 | 361 | // Special tasks, not mentioned in Readme documentation: 362 | // 363 | // - requirejs: 364 | // creates build files for the AMD demo with r.js 365 | // - build-dirty: 366 | // builds the project without running checks (no linter, no tests) 367 | // - ci-dirty: 368 | // builds the project without running checks (no linter, no tests) on every source change 369 | // - demo-ci: 370 | // Runs the demo (= "demo" task), and also rebuilds the project on every source change (= "ci" task) 371 | // - demo-ci-dirty: 372 | // Runs the demo (= "demo" task), and also rebuilds the project "dirty", without tests or linter, on every source 373 | // change (= "ci-dirty" task) 374 | grunt.registerTask('build-dirty', ['preprocess:build', 'concat', 'uglify', 'requirejs']); 375 | grunt.registerTask('ci-dirty', ['build-dirty', 'watch:buildDirty']); 376 | grunt.registerTask('demo-ci', ['build', 'connect:demo', 'focus:demoCi']); 377 | grunt.registerTask('demo-ci-dirty', ['build-dirty', 'connect:demo', 'focus:demoCiDirty']); 378 | 379 | // Make 'build' the default task. 380 | grunt.registerTask('default', ['build']); 381 | 382 | 383 | }; 384 | -------------------------------------------------------------------------------- /spec/matcher.spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it */ 2 | (function () { 3 | "use strict"; 4 | 5 | describe( 'Matcher test', function () { 6 | 7 | var $template, expected, view, 8 | $head = $( "head" ); 9 | 10 | afterEach( function () { 11 | if ( $template ) $template.remove(); 12 | $template = expected = view = undefined; 13 | Backbone.InlineTemplate.clearCache(); 14 | } ); 15 | 16 | describe( 'Identifying the `el` and its content. When the `el` is defined', function () { 17 | 18 | var tagFormatScenario = { 19 | "with a self-closing tag, using a closing slash": { isSelfClosing: true, selfClosingChar: "/" }, 20 | "with a self-closing tag, omitting a closing slash": { isSelfClosing: true, selfClosingChar: "" }, 21 | "with a tag which is opened and closed": { isSelfClosing: false } 22 | }, 23 | 24 | tagNameScenario = { 25 | "with a single-letter tag": "p", 26 | "with a multi-letter tag": "section" 27 | }, 28 | 29 | attributeScenario = { 30 | "without attributes": {}, 31 | "with attributes class, id, and others (values in single quotes)": { 32 | quoteStyle: "'", 33 | attrs: { className: "fooClass barClass", id: "fooId", lang: "fr", contenteditable: "true", style: "font-weight: bold;" } 34 | }, 35 | "with attributes class, id, and others (values in double quotes)": { 36 | quoteStyle: '"', 37 | attrs: { className: "fooClass barClass", id: "fooId", lang: "fr", contenteditable: "true", style: "font-weight: bold;" } 38 | }, 39 | "with attributes class, id, and others (values without quotes)": { 40 | quoteStyle: "", 41 | attrs: { className: "fooClass", id: "fooId", lang: "fr", contenteditable: "true" } 42 | } 43 | }, 44 | 45 | whitespaceScenario = { 46 | "without redundant white space around and inside the tag": "", 47 | "with redundant whitespace around and inside the tag": " ", 48 | "with redundant whitespace, including newlines, around and inside the tag": " \n \n " 49 | }; 50 | 51 | 52 | describeWithData( tagFormatScenario, function ( tagFormat ) { 53 | 54 | describeWithData( tagNameScenario, function ( tagName ) { 55 | 56 | describeWithData( attributeScenario, function ( attributesSetup ) { 57 | 58 | describeWithData( whitespaceScenario, function ( extraSpace ) { 59 | 60 | beforeEach( function () { 61 | 62 | var isSelfClosing = tagFormat.isSelfClosing, 63 | 64 | elDefinition = { 65 | tagName: tagName, 66 | attributes: attributesSetup.attrs 67 | }, 68 | 69 | elFormatting = { 70 | isSelfClosing: isSelfClosing, 71 | selfClosingChar: tagFormat.selfClosingChar, 72 | quoteStyle: attributesSetup.quoteStyle, 73 | extraSpace: extraSpace 74 | }, 75 | 76 | elContent = isSelfClosing ? "" : extraSpace + createInnerContent( "{{", "}}", { indentation: extraSpace } ) + extraSpace, 77 | 78 | inlineTemplate = createInlineTemplate( elDefinition, elContent, elFormatting ), 79 | templateId = _.uniqueId( "template_" ); 80 | 81 | $template = createTemplateNode( templateId, inlineTemplate.html.fullContent, { "data-el-definition": "inline" } ) 82 | .appendTo( $head ); 83 | 84 | expected = { 85 | tagName: tagName, 86 | className: inlineTemplate.el.className, 87 | id: inlineTemplate.el.id, 88 | attributes: inlineTemplate.el.attributes, 89 | elContent: elContent 90 | }; 91 | 92 | view = new Backbone.View( { template: "#" + templateId } ); 93 | } ); 94 | 95 | it( 'the `el` is detected correctly', function () { 96 | // Determined by checking the cache. Could also be determined by examining view.el. 97 | var cached = view.inlineTemplate.getCachedTemplate(); 98 | 99 | expect( cached.tagName ).to.equal( expected.tagName ); 100 | expect( cached.className ).to.equal( expected.className ); 101 | expect( cached.id ).to.equal( expected.id ); 102 | expect( normalizeAttributes( cached.attributes ) ).to.eql( expected.attributes ); 103 | } ); 104 | 105 | it( 'the `el` content is detected correctly', function () { 106 | // Determined by checking the cache. 107 | var cached = view.inlineTemplate.getCachedTemplate(); 108 | expect( cached.html ).to.equal( expected.elContent ); 109 | } ); 110 | 111 | } ); 112 | 113 | } ); 114 | 115 | } ); 116 | 117 | } ); 118 | 119 | } ); 120 | 121 | describe( 'HTML comments', function () { 122 | 123 | var comments = { 124 | empty: "", 125 | singleLine: "", 126 | containingTag: "", 127 | multiLine: "" 128 | }; 129 | 130 | describe( 'When the `el` is defined in a template', function () { 131 | 132 | var commentSetScenario = { 133 | "with an empty HTML comment": [[ comments.empty ]], 134 | "with a single-line HTML comment": [[ comments.singleLine ]], 135 | "with a multi-line HTML comment": [[ comments.multiLine ]], 136 | 'with an HTML comment containing an HTML tag ("

") inside the comment': [[ comments.containingTag ]], 137 | "with multiple HTML comments of mixed type": [[ comments.singleLine, comments.multiLine, comments.containingTag, comments.empty ]] 138 | }, 139 | 140 | commentPlacementScenario = { 141 | "preceding the `el`": { preceding: true }, 142 | "trailing the `el`": { trailing: true }, 143 | "preceding and trailing the `el`": { preceding: true, trailing: true } 144 | }, 145 | 146 | whitespaceScenario = { 147 | "without redundant whitespace surrounding a comment": "", 148 | "with redundant whitespace surrounding a comment": " ", 149 | "with redundant whitespace, including newlines, surrounding a comment": " \n \n " 150 | }; 151 | 152 | 153 | describeWithData( commentSetScenario, function ( commentSet ) { 154 | 155 | describeWithData( commentPlacementScenario, function ( commentPlacement ) { 156 | 157 | describeWithData( whitespaceScenario, function ( whitespace ) { 158 | 159 | var _updateTemplateSourceDefault; 160 | 161 | before( function () { 162 | _updateTemplateSourceDefault = Backbone.InlineTemplate.updateTemplateSource; 163 | Backbone.InlineTemplate.updateTemplateSource = true; 164 | } ); 165 | 166 | after( function () { 167 | Backbone.InlineTemplate.updateTemplateSource = _updateTemplateSourceDefault; 168 | } ); 169 | 170 | beforeEach( function () { 171 | var commentString = whitespace + commentSet.join( whitespace ) + whitespace, 172 | 173 | elDefinition = { 174 | tagName: "p", 175 | className: "fooClass barClass", 176 | id: "fooId", 177 | attributes: { lang: "fr" } 178 | }, 179 | 180 | elContent = createInnerContent( "{{", "}}" ), 181 | 182 | inlineTemplate = createInlineTemplate( elDefinition, elContent ), 183 | templateId = _.uniqueId( "template_" ); 184 | 185 | // Prepend and append HTML comments 186 | if ( commentPlacement.preceding ) inlineTemplate.html.fullContent = commentString + inlineTemplate.html.fullContent; 187 | if ( commentPlacement.trailing ) inlineTemplate.html.fullContent += commentString; 188 | 189 | $template = createTemplateNode( templateId, inlineTemplate.html.fullContent, { "data-el-definition": "inline" } ) 190 | .appendTo( $head ); 191 | 192 | expected = { 193 | tagName: elDefinition.tagName, 194 | className: elDefinition.className, 195 | id: elDefinition.id, 196 | attributes: elDefinition.attributes, 197 | elContent: elContent 198 | }; 199 | 200 | view = new Backbone.View( { template: "#" + templateId } ); 201 | } ); 202 | 203 | it( 'HTML comments are ignored and the `el` is detected correctly', function () { 204 | // Determined by checking the cache. Could also be determined by examining view.el. 205 | var cached = view.inlineTemplate.getCachedTemplate(); 206 | 207 | expect( cached.tagName ).to.equal( expected.tagName ); 208 | expect( cached.className ).to.equal( expected.className ); 209 | expect( cached.id ).to.equal( expected.id ); 210 | expect( normalizeAttributes( cached.attributes ) ).to.eql( expected.attributes ); 211 | } ); 212 | 213 | it( 'the `el` content is detected correctly', function () { 214 | // Determined by checking the cache. 215 | var cached = view.inlineTemplate.getCachedTemplate(); 216 | expect( cached.html ).to.equal( expected.elContent ); 217 | } ); 218 | 219 | it( 'the template no longer contains the HTML comments when updated with `updateTemplateSource`', function () { 220 | expect ( $template.html() ).to.equal( expected.elContent ); 221 | } ); 222 | 223 | } ); 224 | 225 | } ); 226 | 227 | } ); 228 | 229 | } ); 230 | 231 | describe( 'HTML comments which are part of the `el` content', function () { 232 | 233 | var commentPlacementScenario = { 234 | "and placed at the beginning of the content inside the `el`": { leading: true }, 235 | "and placed at the end of the content inside the `el`": { trailing: true }, 236 | "and placed somewhere in the middle of the content inside the `el`": { among: true } 237 | }; 238 | 239 | describeWithData( commentPlacementScenario, function ( commentPlacement ) { 240 | 241 | var _updateTemplateSourceDefault; 242 | 243 | before( function () { 244 | _updateTemplateSourceDefault = Backbone.InlineTemplate.updateTemplateSource; 245 | Backbone.InlineTemplate.updateTemplateSource = true; 246 | } ); 247 | 248 | after( function () { 249 | Backbone.InlineTemplate.updateTemplateSource = _updateTemplateSourceDefault; 250 | } ); 251 | 252 | beforeEach( function () { 253 | var inlineTemplate, 254 | 255 | commentString = _.values( comments ).join( "\n" ), 256 | elContent = createInnerContent( "{{", "}}", { insertion: commentPlacement.among ? commentString : "" } ), 257 | templateId = _.uniqueId( "template_" ); 258 | 259 | // Prepend or append HTML comments 260 | if ( commentPlacement.leading ) elContent = commentString + elContent; 261 | if ( commentPlacement.trailing ) elContent += commentString; 262 | 263 | inlineTemplate = createInlineTemplate( { tagName: "p" }, elContent ); 264 | $template = createTemplateNode( templateId, inlineTemplate.html.fullContent, { "data-el-definition": "inline" } ) 265 | .appendTo( $head ); 266 | 267 | expected = { 268 | elContent: elContent 269 | }; 270 | 271 | view = new Backbone.View( { template: "#" + templateId } ); 272 | } ); 273 | 274 | it( 'are preserved, unaltered, in the cached template content', function () { 275 | var cached = view.inlineTemplate.getCachedTemplate(); 276 | expect( cached.html ).to.equal( expected.elContent ); 277 | } ); 278 | 279 | it( 'are preserved, unaltered, in the updated template (using `updateTemplateSource`)', function () { 280 | expect ( $template.html() ).to.equal( expected.elContent ); 281 | } ); 282 | 283 | } ) 284 | } ); 285 | 286 | } ); 287 | 288 | describe( 'Template variable format', function () { 289 | 290 | var delimiterScenario = { 291 | "When the `el` contains template variables in EJS format": [ "<%= ", " %>" ], 292 | "When the `el` contains template variables in Mustache format": [ "{{", "}}" ], 293 | "When the `el` contains template variables in ES6 format": [ "${", "}" ] 294 | }; 295 | 296 | describeWithData( delimiterScenario, function ( varStartDelimiter, varEndDelimiter ) { 297 | 298 | var _updateTemplateSourceDefault; 299 | 300 | before( function () { 301 | _updateTemplateSourceDefault = Backbone.InlineTemplate.updateTemplateSource; 302 | Backbone.InlineTemplate.updateTemplateSource = true; 303 | } ); 304 | 305 | after( function () { 306 | Backbone.InlineTemplate.updateTemplateSource = _updateTemplateSourceDefault; 307 | } ); 308 | 309 | beforeEach( function () { 310 | var templateId = _.uniqueId( "template_" ), 311 | elContent = createInnerContent( varStartDelimiter, varEndDelimiter ), 312 | inlineTemplate = createInlineTemplate( { tagName: "p" }, elContent ); 313 | 314 | $template = createTemplateNode( templateId, inlineTemplate.html.fullContent, { "data-el-definition": "inline" } ) 315 | .appendTo( $head ); 316 | 317 | expected = { 318 | elContent: elContent 319 | }; 320 | 321 | view = new Backbone.View( { template: "#" + templateId } ); 322 | } ); 323 | 324 | it( 'the `el` content is cached correctly', function () { 325 | var cached = view.inlineTemplate.getCachedTemplate(); 326 | expect( cached.html ).to.equal( expected.elContent ); 327 | } ); 328 | 329 | it( 'the `el` content shows up correctly in the template when it is updated with `updateTemplateSource`', function () { 330 | expect ( $template.html() ).to.equal( expected.elContent ); 331 | } ); 332 | 333 | } ) 334 | 335 | } ); 336 | 337 | } ); 338 | 339 | })(); -------------------------------------------------------------------------------- /spec/chai-helpers/backbone-el.js: -------------------------------------------------------------------------------- 1 | (function ( chaiBackboneEl ) { 2 | // Module systems magic dance. 3 | if ( typeof require === "function" && typeof exports === "object" && typeof module === "object" ) { 4 | // NodeJS 5 | module.exports = chaiBackboneEl; 6 | } else if ( typeof define === "function" && define.amd ) { 7 | // AMD 8 | define( function () { 9 | return function ( chai, utils ) { 10 | return chaiBackboneEl( chai, utils ); 11 | }; 12 | } ); 13 | } else { 14 | // Other environment (usually