├── .gitignore ├── .travis.yml ├── FAQ.md ├── LICENSE ├── README.md ├── bower.json ├── browser ├── form.html ├── perf-100-divs-with-10-children.html ├── perf-100-divs.html ├── perf-6-header-tags-10-list-items.html └── without-jquery.html ├── dist ├── transparency.js └── transparency.min.js ├── examples ├── backbone │ ├── customers.json │ ├── index.html │ └── js │ │ ├── app.js │ │ ├── backbone-min.js │ │ ├── jquery-1.7.2.min.js │ │ └── underscore-min.js ├── chat │ ├── css │ │ └── style.css │ ├── index.html │ └── js │ │ ├── coffee-script.js │ │ ├── jquery-1.7.1.min.js │ │ └── underscore-min.js ├── hello-server │ ├── README.md │ ├── npm-debug.log │ ├── package.json │ ├── public │ │ └── stylesheets │ │ │ └── style.css │ └── server.js ├── todo-app │ ├── css │ │ └── style.css │ ├── index.html │ └── js │ │ ├── coffee-script.js │ │ ├── jquery-1.7.1.min.js │ │ └── underscore-min.js └── todomvc │ ├── .editorconfig │ ├── .nojekyll │ ├── architecture-examples │ ├── backbone │ │ ├── css │ │ │ └── app.css │ │ ├── index.html │ │ └── js │ │ │ ├── app.js │ │ │ ├── collections │ │ │ └── todos.js │ │ │ ├── init.js │ │ │ ├── lib │ │ │ ├── backbone-localstorage.js │ │ │ ├── backbone-min.js │ │ │ └── underscore-min.js │ │ │ ├── models │ │ │ └── todo.js │ │ │ ├── routers │ │ │ └── router.js │ │ │ └── views │ │ │ ├── app.js │ │ │ └── todos.js │ └── spine │ │ ├── Cakefile │ │ ├── css │ │ └── app.css │ │ ├── index.html │ │ ├── js │ │ ├── app.js │ │ ├── controllers │ │ │ └── todos.js │ │ ├── lib │ │ │ ├── local.js │ │ │ ├── route.js │ │ │ └── spine.js │ │ └── models │ │ │ └── todo.js │ │ ├── readme.md │ │ └── src │ │ ├── app.coffee │ │ ├── controllers │ │ └── todos.coffee │ │ └── models │ │ └── todo.coffee │ ├── assets │ ├── base.css │ ├── base.js │ ├── bg.png │ ├── crossroads.min.js │ ├── handlebars.min.js │ ├── ie.js │ ├── jasmine │ │ ├── jasmine-html.js │ │ ├── jasmine.css │ │ └── jasmine.js │ ├── jquery.min.js │ ├── require.min.js │ └── signals.min.js │ ├── media │ ├── icon-bg.png │ ├── icon.png │ ├── logo.ai │ ├── logo.png │ ├── logo.svg │ ├── symbol.ai │ ├── symbol.png │ └── symbol.svg │ └── readme.md ├── gulpfile.coffee ├── index.js ├── lib ├── attributeFactory.js ├── context.js ├── elementFactory.js ├── helpers.js ├── instance.js ├── lodash.js └── transparency.js ├── package.json ├── spec ├── amdSpec.coffee ├── amdSpec.js ├── amdSpecRunner.html ├── assert.coffee ├── assert.js ├── basicsSpec.coffee ├── basicsSpec.js ├── cachedTemplatesSpec.coffee ├── cachedTemplatesSpec.js ├── configSpec.coffee ├── configSpec.js ├── directivesSpec.coffee ├── directivesSpec.js ├── formsSpec.coffee ├── formsSpec.js ├── functionalSpecRunner.html ├── lib │ ├── benchmark.js │ ├── handlebars.js │ └── jquery-1.9.1.min.js ├── listsSpec.coffee ├── listsSpec.js ├── modelReferencesSpec.coffee ├── modelReferencesSpec.js ├── nestedModelsSpec.coffee ├── nestedModelsSpec.js ├── performanceSpec.coffee ├── performanceSpec.js ├── performanceSpecRunner.html ├── serverSpec.coffee ├── serverSpec.js └── testlingSpecRunner.html ├── src ├── attributeFactory.coffee ├── context.coffee ├── elementFactory.coffee ├── helpers.coffee ├── instance.coffee └── transparency.coffee ├── testem.json └── testlingSpecRunner.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | *.sublime-project 4 | *.sublime-workspace 5 | src/*.js 6 | .DS_Store 7 | docs 8 | .grunt 9 | .tmp 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | node_js: 8 | - "0.10" 9 | 10 | before_script: 11 | - npm install 12 | 13 | notifications: 14 | email: 15 | - jarno.keskikangas@leonidasoy.fi 16 | irc: 17 | channels: 18 | - "irc.freenode.org#transparency.js" 19 | use_notice: true 20 | skip_join: true 21 | template: 22 | - "http://travis-ci.org/leonidas/transparency : %{message}" 23 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | Please refer to [on-line FAQ at Github wiki](https://github.com/leonidas/transparency/wiki/Frequently-Asked-Questions) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 Jarno Keskikangas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transparency", 3 | "version": "1.0.0", 4 | "main": "./dist/transparency.js", 5 | "ignore": [ 6 | "**/*", 7 | "!dist/transparency.js", 8 | "!dist/transparency.min.js" 9 | ], 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "requirejs": "~2.1.15", 13 | "jquery": "~2.1.1", 14 | "handlebars": "~2.0.0", 15 | "benchmark": "~1.0.0", 16 | "mocha": "~1.21.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /browser/form.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Form test 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /browser/perf-100-divs-with-10-children.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Transparency perf test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

14 | 
15 |     

Transparency

16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |

Handlebars

24 |
25 |
26 | 27 | 28 | 39 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /browser/perf-100-divs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Transparency perf test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

 13 | 
 14 |     

Transparency - first render

15 |
16 |
17 | 18 |

Handlebars - first render

19 |
20 |
21 | 22 |

Transparency

23 |
24 |
25 | 26 |

Handlebars

27 |
28 |
29 | 30 |
31 | 32 |
33 | 34 | 39 | 40 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /browser/perf-6-header-tags-10-list-items.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Transparency perf test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

15 |     
16 | 17 |

Transparency

18 |
19 |

20 |

21 |

22 |

23 |
24 |
25 | 28 |
29 | 30 |

Handlebars

31 |
32 |
33 | 34 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /browser/without-jquery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Transparency 5 |

Simple Transpency test without jQuery

6 | 7 |
8 | 9 |
10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/backbone/customers.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"id":1,"suffix":"Mr.","name":"Chuck Norris","email":"chuck@roundhouse.kick","phone":"+358401122334"} 3 | ] -------------------------------------------------------------------------------- /examples/backbone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Backbone.js + Transparency demo 4 | 17 | 18 | 19 | 20 |
21 |

Customers

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
SuffixNameE-MailPhone
Loading customers..Remove
42 | 43 |
44 |

Add new customer

45 |
46 |

47 | 55 |

56 |

57 | 58 |

59 |

60 | 61 |

62 |

63 | 64 |

65 |

66 | 67 |

68 |
69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/backbone/js/app.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var CustomersView, CustomerRowView, CustomerModel, CustomerCollection; 3 | 4 | CustomerModel = Backbone.Model.extend({}); 5 | 6 | CustomerCollection = Backbone.Collection.extend({ 7 | model: CustomerModel, 8 | url: 'customers.json' 9 | }); 10 | 11 | CustomersView = Backbone.View.extend({ 12 | events: { 13 | 'submit .new-customer': 'addCustomer', 14 | 'click .delete': 'deleteCustomer' 15 | }, 16 | 17 | el: $('#application'), 18 | 19 | render: function() { 20 | this.$('.customers').render(this.customers.toJSON(), { 21 | delete: { 22 | index: function(params) { return params.index; } 23 | } 24 | }); 25 | 26 | return this; 27 | }, 28 | 29 | initialize: function() { 30 | this.customers = new CustomerCollection(); 31 | this.customers.on('add', this.render, this); 32 | this.customers.on('reset', this.render, this); 33 | this.customers.on('destroy', this.render, this); 34 | this.customers.fetch(); 35 | }, 36 | 37 | addCustomer: function(event) { 38 | event.preventDefault(); 39 | this.customers.create({ 40 | suffix: this.$('.new-customer .suffixes').val(), 41 | name: this.$('.new-customer .name').val(), 42 | email: this.$('.new-customer .email').val(), 43 | phone: this.$('.new-customer .phone').val() 44 | }); 45 | }, 46 | 47 | deleteCustomer: function(event) { 48 | event.preventDefault(); 49 | this.customers.at(event.target.getAttribute('index')).destroy(); 50 | } 51 | }); 52 | 53 | //Kickstart 54 | var app = new CustomersView(); 55 | app.render(); 56 | }); 57 | -------------------------------------------------------------------------------- /examples/chat/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00b7ff; 8 | } 9 | 10 | textarea, pre { 11 | font-family: courier; 12 | font-size: small; 13 | display: block; 14 | width: 400px; 15 | margin-top: 10px; 16 | margin-bottom: 10px; 17 | } 18 | 19 | input[name="message"] { 20 | width: 300px; 21 | } 22 | 23 | #templates { 24 | display: none; 25 | } 26 | 27 | .col { 28 | float: left; 29 | margin-right: 15px; 30 | } 31 | -------------------------------------------------------------------------------- /examples/chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chat (a.k.a infinite feed) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Chat (a.k.a infinite feed)

13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/hello-server/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ``` 4 | npm install 5 | npm start 6 | open http://localhost:3000 7 | ``` 8 | -------------------------------------------------------------------------------- /examples/hello-server/npm-debug.log: -------------------------------------------------------------------------------- 1 | 0 info it worked if it ends with ok 2 | 1 verbose cli [ '/Users/pyykkis/.nvm/v0.10.1/bin/node', 3 | 1 verbose cli '/Users/pyykkis/.nvm/v0.10.1/bin/npm', 4 | 1 verbose cli 'start' ] 5 | 2 info using npm@1.2.15 6 | 3 info using node@v0.10.1 7 | 4 verbose read json /Users/pyykkis/work/transparency/examples/hello-server/package.json 8 | 5 verbose run-script [ 'prestart', 'start', 'poststart' ] 9 | 6 info prestart hello-server@0.0.1 10 | 7 info start hello-server@0.0.1 11 | 8 verbose unsafe-perm in lifecycle true 12 | 9 silly exec sh "-c" "node server.js" 13 | 10 silly sh,-c,node server.js,/Users/pyykkis/work/transparency/examples/hello-server spawning 14 | 11 info hello-server@0.0.1 Failed to exec start script 15 | 12 error hello-server@0.0.1 start: `node server.js` 16 | 12 error `sh "-c" "node server.js"` failed with 8 17 | 13 error Failed at the hello-server@0.0.1 start script. 18 | 13 error This is most likely a problem with the hello-server package, 19 | 13 error not with npm itself. 20 | 13 error Tell the author that this fails on your system: 21 | 13 error node server.js 22 | 13 error You can get their info via: 23 | 13 error npm owner ls hello-server 24 | 13 error There is likely additional logging output above. 25 | 14 error System Darwin 12.3.0 26 | 15 error command "/Users/pyykkis/.nvm/v0.10.1/bin/node" "/Users/pyykkis/.nvm/v0.10.1/bin/npm" "start" 27 | 16 error cwd /Users/pyykkis/work/transparency/examples/hello-server 28 | 17 error node -v v0.10.1 29 | 18 error npm -v 1.2.15 30 | 19 error code ELIFECYCLE 31 | 20 verbose exit [ 1, true ] 32 | -------------------------------------------------------------------------------- /examples/hello-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-server", 3 | "version": "0.0.1", 4 | "private": true, 5 | "dependencies": { 6 | "express": ">= 2.5.9", 7 | "jquery": ">= 1.7.2" 8 | }, 9 | "scripts": {"start": "node server.js"} 10 | } 11 | -------------------------------------------------------------------------------- /examples/hello-server/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } -------------------------------------------------------------------------------- /examples/hello-server/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | jsdom = require('jsdom').jsdom, 3 | doc = jsdom(''), 4 | Transparency = require('../../index'), 5 | helloTmpl = doc.getElementsByTagName('ul')[0], 6 | count = 0, 7 | app; 8 | 9 | app = express(); 10 | 11 | app.configure(function() { app.use(app.router); }); 12 | 13 | app.configure('development', function() { 14 | app.use(express.errorHandler({ 15 | dumpExceptions: true, 16 | showStack: true 17 | })); 18 | }); 19 | 20 | app.get('/', function(req, res) { 21 | var data = [ 22 | { title: 'Hello ' + count++}, 23 | { title: 'Howdy ' + count++}, 24 | { title: 'Cheers ' + count++}, 25 | { title: 'Byebye ' + count++} 26 | ]; 27 | 28 | return res.send(Transparency.render(helloTmpl, data).outerHTML); 29 | }); 30 | 31 | app.listen(3000); 32 | 33 | console.log('Express server listening on port 3000 in %s mode', app.settings.env); 34 | -------------------------------------------------------------------------------- /examples/todo-app/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00b7ff; 8 | } 9 | 10 | textarea, pre { 11 | font-family: courier; 12 | font-size: small; 13 | display: block; 14 | width: 400px; 15 | margin-top: 10px; 16 | margin-bottom: 10px; 17 | } 18 | 19 | #error { 20 | color: red; 21 | } 22 | #result div { 23 | margin-left: 10px; 24 | } 25 | 26 | .col { 27 | float: left; 28 | margin-right: 15px; 29 | } 30 | -------------------------------------------------------------------------------- /examples/todo-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | To-do List 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

To-do List

13 |
14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/todomvc/.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = LF -------------------------------------------------------------------------------- /examples/todomvc/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonidas/transparency/92149b83e7202423a764e1cc2ec5d78e43f77ce2/examples/todomvc/.nojekyll -------------------------------------------------------------------------------- /examples/todomvc/architecture-examples/backbone/css/app.css: -------------------------------------------------------------------------------- 1 | #todo-template { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /examples/todomvc/architecture-examples/backbone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Backbone.js 7 | 8 | 9 | 10 | 11 | 12 |
13 | 17 |
18 | 19 | 20 | 21 |
22 |
40 |
41 |

Double-click to edit a todo

42 |

Written by Addy Osmani

43 |

Part of TodoMVC

44 | 45 | 46 |
47 |
48 |
  • 49 |
    50 | 51 | 52 | 53 |
    54 | 55 |
  • 56 |
    57 |
    58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /examples/todomvc/architecture-examples/backbone/js/app.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | 3 | // Kick things off by creating the **App**. 4 | var App = new window.app.AppView; 5 | 6 | }); -------------------------------------------------------------------------------- /examples/todomvc/architecture-examples/backbone/js/collections/todos.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // Todo Collection 5 | // --------------- 6 | 7 | // The collection of todos is backed by *localStorage* instead of a remote 8 | // server. 9 | var TodoList = Backbone.Collection.extend({ 10 | 11 | // Reference to this collection's model. 12 | model: window.app.Todo, 13 | 14 | // Save all of the todo items under the `"todos"` namespace. 15 | localStorage: new Store("todos-backbone"), 16 | 17 | // Filter down the list of all todo items that are finished. 18 | completed: function() { 19 | return this.filter(function(todo){ return todo.get('completed'); }); 20 | }, 21 | 22 | // Filter down the list to only todo items that are still not finished. 23 | remaining: function() { 24 | return this.without.apply(this, this.completed()); 25 | }, 26 | 27 | // We keep the Todos in sequential order, despite being saved by unordered 28 | // GUID in the database. This generates the next order number for new items. 29 | nextOrder: function() { 30 | if ( !this.length ){ 31 | return 1; 32 | } 33 | return this.last().get('order') + 1; 34 | }, 35 | 36 | // Todos are sorted by their original insertion order. 37 | comparator: function(todo) { 38 | return todo.get('order'); 39 | } 40 | }); 41 | 42 | // Create our global collection of **Todos**. 43 | window.app.Todos = new TodoList; 44 | 45 | })(); 46 | -------------------------------------------------------------------------------- /examples/todomvc/architecture-examples/backbone/js/init.js: -------------------------------------------------------------------------------- 1 | 2 | // Constants 3 | var ENTER_KEY = 13; 4 | 5 | // Setup namespace for the app 6 | window.app = window.app || {}; -------------------------------------------------------------------------------- /examples/todomvc/architecture-examples/backbone/js/lib/backbone-localstorage.js: -------------------------------------------------------------------------------- 1 | // A simple module to replace `Backbone.sync` with *localStorage*-based 2 | // persistence. Models are given GUIDS, and saved into a JSON object. Simple 3 | // as that. 4 | 5 | // Generate four random hex digits. 6 | function S4() { 7 | return (((1+Math.random())*0x10000)|0).toString(16).substring(1); 8 | }; 9 | 10 | // Generate a pseudo-GUID by concatenating random hexadecimal. 11 | function guid() { 12 | return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); 13 | }; 14 | 15 | // Our Store is represented by a single JS object in *localStorage*. Create it 16 | // with a meaningful name, like the name you'd give a table. 17 | var Store = function(name) { 18 | this.name = name; 19 | var store = localStorage.getItem(this.name); 20 | this.data = (store && JSON.parse(store)) || {}; 21 | }; 22 | 23 | _.extend(Store.prototype, { 24 | 25 | // Save the current state of the **Store** to *localStorage*. 26 | save: function() { 27 | localStorage.setItem(this.name, JSON.stringify(this.data)); 28 | }, 29 | 30 | // Add a model, giving it a (hopefully)-unique GUID, if it doesn't already 31 | // have an id of it's own. 32 | create: function(model) { 33 | if (!model.id) model.id = model.attributes.id = guid(); 34 | this.data[model.id] = model; 35 | this.save(); 36 | return model; 37 | }, 38 | 39 | // Update a model by replacing its copy in `this.data`. 40 | update: function(model) { 41 | this.data[model.id] = model; 42 | this.save(); 43 | return model; 44 | }, 45 | 46 | // Retrieve a model from `this.data` by id. 47 | find: function(model) { 48 | return this.data[model.id]; 49 | }, 50 | 51 | // Return the array of all models currently in storage. 52 | findAll: function() { 53 | return _.values(this.data); 54 | }, 55 | 56 | // Delete a model from `this.data`, returning it. 57 | destroy: function(model) { 58 | delete this.data[model.id]; 59 | this.save(); 60 | return model; 61 | } 62 | 63 | }); 64 | 65 | // Override `Backbone.sync` to use delegate to the model or collection's 66 | // *localStorage* property, which should be an instance of `Store`. 67 | Backbone.sync = function(method, model, options) { 68 | 69 | var resp; 70 | var store = model.localStorage || model.collection.localStorage; 71 | 72 | switch (method) { 73 | case "read": resp = model.id ? store.find(model) : store.findAll(); break; 74 | case "create": resp = store.create(model); break; 75 | case "update": resp = store.update(model); break; 76 | case "delete": resp = store.destroy(model); break; 77 | } 78 | 79 | if (resp) { 80 | options.success(resp); 81 | } else { 82 | options.error("Record not found"); 83 | } 84 | }; -------------------------------------------------------------------------------- /examples/todomvc/architecture-examples/backbone/js/models/todo.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // Todo Model 5 | // ---------- 6 | 7 | // Our basic **Todo** model has `title`, `order`, and `completed` attributes. 8 | window.app.Todo = Backbone.Model.extend({ 9 | 10 | // Default attributes for the todo. 11 | defaults: { 12 | title: "empty todo...", 13 | completed: false 14 | }, 15 | 16 | // Ensure that each todo created has `title`. 17 | initialize: function() { 18 | if (!this.get("title")) { 19 | this.set({"title": this.defaults.title}); 20 | } 21 | }, 22 | 23 | // Toggle the `completed` state of this todo item. 24 | toggle: function() { 25 | this.save({completed: !this.get("completed")}); 26 | }, 27 | 28 | // Remove this Todo from *localStorage* and delete its view. 29 | clear: function() { 30 | this.destroy(); 31 | } 32 | 33 | }); 34 | 35 | 36 | })(); -------------------------------------------------------------------------------- /examples/todomvc/architecture-examples/backbone/js/routers/router.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // Todo Router 5 | // ---------- 6 | 7 | var Workspace = Backbone.Router.extend({ 8 | 9 | routes:{ 10 | "*filter": "setFilter" 11 | }, 12 | 13 | setFilter: function(param){ 14 | 15 | // Set the current filter to be used 16 | window.app.TodoFilter = param.trim() || ""; 17 | 18 | // Trigger a collection reset/addAll 19 | window.app.Todos.trigger('reset'); 20 | } 21 | 22 | }); 23 | 24 | window.app.TodoRouter = new Workspace; 25 | Backbone.history.start(); 26 | 27 | })(); -------------------------------------------------------------------------------- /examples/todomvc/architecture-examples/backbone/js/views/app.js: -------------------------------------------------------------------------------- 1 | $(function( $ ) { 2 | 'use strict'; 3 | 4 | // The Application 5 | // --------------- 6 | 7 | // Our overall **AppView** is the top-level piece of UI. 8 | window.app.AppView = Backbone.View.extend({ 9 | 10 | // Instead of generating a new element, bind to the existing skeleton of 11 | // the App already present in the HTML. 12 | el: $("#todoapp"), 13 | 14 | // Our template for the line of statistics at the bottom of the app. 15 | statsTemplate: $('#stats-template'), 16 | 17 | // Delegated events for creating new items, and clearing completed ones. 18 | events: { 19 | "keypress #new-todo": "createOnEnter", 20 | "click #clear-completed": "clearCompleted", 21 | "click #toggle-all": "toggleAllComplete" 22 | }, 23 | 24 | // At initialization we bind to the relevant events on the `Todos` 25 | // collection, when items are added or changed. Kick things off by 26 | // loading any preexisting todos that might be saved in *localStorage*. 27 | initialize: function() { 28 | 29 | this.input = this.$("#new-todo"); 30 | this.allCheckbox = this.$("#toggle-all")[0]; 31 | 32 | window.app.Todos.on('add', this.addOne, this); 33 | window.app.Todos.on('reset', this.addAll, this); 34 | window.app.Todos.on('all', this.render, this); 35 | 36 | this.$footer = $('#footer'); 37 | this.$main = $('#main'); 38 | 39 | window.app.Todos.fetch(); 40 | }, 41 | 42 | // Re-rendering the App just means refreshing the statistics -- the rest 43 | // of the app doesn't change. 44 | render: function() { 45 | var completed = window.app.Todos.completed().length; 46 | var remaining = window.app.Todos.remaining().length; 47 | 48 | if (window.app.Todos.length) { 49 | this.$main.show(); 50 | this.$footer.show(); 51 | 52 | this.statsTemplate.render({ 53 | completed: completed, 54 | remaining: remaining, 55 | items: (remaining == 1 ? 'item' : 'items') 56 | }); 57 | 58 | this.$('#filters li a') 59 | .removeClass('selected') 60 | .filter("[href='#/" + (window.app.TodoFilter || "") + "']") 61 | .addClass('selected'); 62 | 63 | 64 | } else { 65 | this.$main.hide(); 66 | this.$footer.hide(); 67 | } 68 | 69 | this.allCheckbox.checked = !remaining; 70 | }, 71 | 72 | // Add a single todo item to the list by creating a view for it, and 73 | // appending its element to the `
    task namedelete
    122 | 123 | 124 | 125 | 126 | 127 |
    foobar
    128 | """ 129 | 130 | template.find(".users").render [{username:'user1'}, {username:'user2'}] 131 | expect(template).toBeEqual $ """ 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
    user1
    user2
    142 | """ 143 | 144 | template.find(".users").render [username:'user1'] 145 | expect(template).toBeEqual $ """ 146 | 147 | 148 | 149 | 150 | 151 | 152 |
    user1
    153 | """ 154 | 155 | template.find(".users").render [{username:'user1'}, {username:'user3'}] 156 | expect(template).toBeEqual $ """ 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 |
    user1
    user3
    167 | """ 168 | 169 | template.find(".users").render [{username:'user4'}, {username:'user3'}] 170 | expect(template).toBeEqual $ """ 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 |
    user4
    user3
    181 | """ 182 | 183 | expectModelObjects = (elements, data) -> 184 | for object, i in data 185 | expect(elements[i].transparency.model).toEqual(object) 186 | -------------------------------------------------------------------------------- /spec/listsSpec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var expectModelObjects; 3 | 4 | describe("Transparency", function() { 5 | it("should render list of objects", function() { 6 | var data, expected, template; 7 | template = $("
    \n
    \n \n
    \n
    "); 8 | data = [ 9 | { 10 | name: 'John', 11 | text: 'That rules' 12 | }, { 13 | name: 'Arnold', 14 | text: 'Great post!' 15 | } 16 | ]; 17 | expected = $("
    \n
    \n JohnThat rules\n
    \n ArnoldGreat post!\n
    \n
    "); 18 | template.render(data); 19 | expect(template).toBeEqual(expected); 20 | expect(template.find('.comment')[0].transparency.model).toEqual(data[0]); 21 | return expectModelObjects(template.find('.comment'), data); 22 | }); 23 | it("should render empty lists", function() { 24 | var data, expected, template; 25 | template = $("
    \n
    \n \n \n
    \n
    "); 26 | data = []; 27 | expected = $("
    \n
    "); 28 | template.render(data); 29 | return expect(template).toBeEqual(expected); 30 | }); 31 | it("should render lists with duplicate content", function() { 32 | var data, expected, template; 33 | template = $("
    \n
    \n
    "); 34 | data = [ 35 | { 36 | name: "Same" 37 | }, { 38 | name: "Same" 39 | } 40 | ]; 41 | expected = $("
    \n
    Same
    \n
    Same
    \n
    "); 42 | template.render(data); 43 | return expect(template).toBeEqual(expected); 44 | }); 45 | it("should render plain values with 'this' directives", function() { 46 | var data, directives, expected, template; 47 | template = $("
    \n \n \n
    "); 48 | data = ["That rules", "Great post!", 5]; 49 | directives = { 50 | comment: { 51 | text: function() { 52 | return this.value; 53 | } 54 | } 55 | }; 56 | expected = $("
    \n \n That rules\n \n Great post!\n \n 5\n
    "); 57 | template.render(data, directives); 58 | return expect(template).toBeEqual(expected); 59 | }); 60 | it("should not fail when there's no child node in the simple list template", function() { 61 | var data, expected, template; 62 | template = $("
    \n
    "); 63 | data = ["That rules", "Great post!"]; 64 | expected = $("
    \n
    "); 65 | template.find('.comments').render(data); 66 | return expect(template).toBeEqual(expected); 67 | }); 68 | return it("should match table rows to the number of model objects", function() { 69 | var template; 70 | template = $("\n \n \n \n \n \n
    foobar
    "); 71 | template.find(".users").render([ 72 | { 73 | username: 'user1' 74 | }, { 75 | username: 'user2' 76 | } 77 | ]); 78 | expect(template).toBeEqual($("\n \n \n \n \n \n \n \n \n
    user1
    user2
    ")); 79 | template.find(".users").render([ 80 | { 81 | username: 'user1' 82 | } 83 | ]); 84 | expect(template).toBeEqual($("\n \n \n \n \n \n
    user1
    ")); 85 | template.find(".users").render([ 86 | { 87 | username: 'user1' 88 | }, { 89 | username: 'user3' 90 | } 91 | ]); 92 | expect(template).toBeEqual($("\n \n \n \n \n \n \n \n \n
    user1
    user3
    ")); 93 | template.find(".users").render([ 94 | { 95 | username: 'user4' 96 | }, { 97 | username: 'user3' 98 | } 99 | ]); 100 | return expect(template).toBeEqual($("\n \n \n \n \n \n \n \n \n
    user4
    user3
    ")); 101 | }); 102 | }); 103 | 104 | expectModelObjects = function(elements, data) { 105 | var i, j, len, object, results; 106 | results = []; 107 | for (i = j = 0, len = data.length; j < len; i = ++j) { 108 | object = data[i]; 109 | results.push(expect(elements[i].transparency.model).toEqual(object)); 110 | } 111 | return results; 112 | }; 113 | 114 | }).call(this); 115 | -------------------------------------------------------------------------------- /spec/modelReferencesSpec.coffee: -------------------------------------------------------------------------------- 1 | describe "Each element in a template instance", -> 2 | 3 | it "should have reference to the rendered model", -> 4 | template = $ """ 5 |
    6 |
  • Moar text
  • 7 | 8 | """ 9 | 10 | data = [ 11 | name: 'Foo' 12 | , 13 | name: 'Bar' 14 | ] 15 | 16 | directives = 17 | name: text: (params) -> 18 | expect(data[params.index]).toEqual(params.element.transparency.model) 19 | @name 20 | 21 | template.render data, directives 22 | 23 | for li, i in template.find('li') 24 | for element in $(li).add('*', li) 25 | expect(data[i]).toEqual(element.transparency.model) 26 | -------------------------------------------------------------------------------- /spec/modelReferencesSpec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | describe("Each element in a template instance", function() { 3 | return it("should have reference to the rendered model", function() { 4 | var data, directives, element, i, j, len, li, ref, results, template; 5 | template = $("
    \n
  • Moar text
  • \n"); 6 | data = [ 7 | { 8 | name: 'Foo' 9 | }, { 10 | name: 'Bar' 11 | } 12 | ]; 13 | directives = { 14 | name: { 15 | text: function(params) { 16 | expect(data[params.index]).toEqual(params.element.transparency.model); 17 | return this.name; 18 | } 19 | } 20 | }; 21 | template.render(data, directives); 22 | ref = template.find('li'); 23 | results = []; 24 | for (i = j = 0, len = ref.length; j < len; i = ++j) { 25 | li = ref[i]; 26 | results.push((function() { 27 | var k, len1, ref1, results1; 28 | ref1 = $(li).add('*', li); 29 | results1 = []; 30 | for (k = 0, len1 = ref1.length; k < len1; k++) { 31 | element = ref1[k]; 32 | results1.push(expect(data[i]).toEqual(element.transparency.model)); 33 | } 34 | return results1; 35 | })()); 36 | } 37 | return results; 38 | }); 39 | }); 40 | 41 | }).call(this); 42 | -------------------------------------------------------------------------------- /spec/performanceSpec.coffee: -------------------------------------------------------------------------------- 1 | describe "Transparency performance", -> 2 | @timeout 30000 3 | 4 | describe "with cached templates", -> 5 | 6 | describe "with one todo item", -> 7 | 8 | it "should be on the same ballpark with Handlebars", (done) -> 9 | transparency = new Benchmark 'transparency - cached template, one todo', 10 | setup: -> 11 | template = $('
    ')[0] 12 | index = 0 13 | data = ({todo: Math.random()} for i in [1..@count]) 14 | Transparency.render template, {todo: Math.random()} 15 | return 16 | 17 | fn: -> 18 | Transparency.render template, data[index++] 19 | return 20 | 21 | handlebars = new Benchmark 'handlebars - compiled and cached template, one todo', 22 | setup: -> 23 | parser = $('
    ')[0] 24 | template = Handlebars.compile('
    {{todo}}
    ') 25 | index = 0 26 | data = ({todo: Math.random()} for i in [1..@count]) 27 | return 28 | 29 | fn: -> 30 | parser.innerHTML = template data[index++] 31 | return 32 | 33 | new Benchmark.Suite() 34 | .add(transparency) 35 | .add(handlebars) 36 | 37 | .on('complete', -> 38 | expect(this[0]).toBeOnTheSameBallpark(this[1], 5); done()) 39 | .run() 40 | 41 | describe "with hundred todo items", -> 42 | 43 | it "should be on the same ballpark with Handlebars", -> 44 | transparency = new Benchmark 'transparency - cached template, 100 todos', 45 | setup: -> 46 | template = $('
    ')[0] 47 | index = 0 48 | data = for i in [1..@count] 49 | for j in [1..100] 50 | {todo: Math.random()} 51 | Transparency.render template, ({todo: Math.random()} for j in [1..100]) 52 | return 53 | 54 | fn: -> 55 | Transparency.render template, data[index++] 56 | return 57 | 58 | handlebars = new Benchmark 'handlebars - compiled and cached template, 100 todos', 59 | setup: -> 60 | parser = $('
    ')[0] 61 | template = Handlebars.compile('
    {{#each this}}
    {{todo}}
    {{/each}}
    ') 62 | index = 0 63 | data = for i in [1..@count] 64 | for j in [1..100] 65 | {todo: Math.random()} 66 | return 67 | 68 | fn: -> 69 | parser.innerHTML = template data[index++] 70 | return 71 | 72 | new Benchmark.Suite() 73 | .add(transparency) 74 | .add(handlebars) 75 | 76 | .on('complete', -> 77 | expect(this[0]).toBeOnTheSameBallpark(this[1], 5)) 78 | .run() 79 | 80 | describe "on first render call", -> 81 | 82 | describe "with one todo item", -> 83 | 84 | it "should be on the same ballpark with Handlebars", -> 85 | transparency = new Benchmark 'transparency - unused template, one todo', 86 | setup: -> 87 | template = for i in [1..@count] 88 | $('
    ')[0] 89 | index = 0 90 | data = ({todo: Math.random()} for i in [1..@count]) 91 | return 92 | 93 | fn: -> 94 | Transparency.render template[index], data[index++] 95 | return 96 | 97 | handlebars = new Benchmark 'handlebars - unused and compiled template, one todo', 98 | setup: -> 99 | parser = for i in [1..@count] 100 | $('
    ')[0] 101 | template = for i in [1..@count] 102 | Handlebars.compile('
    {{todo}}
    ') 103 | index = 0 104 | data = ({todo: Math.random()} for i in [1..@count]) 105 | return 106 | 107 | fn: -> 108 | parser[index].innerHTML = template[index] data[index++] 109 | return 110 | 111 | new Benchmark.Suite() 112 | .add(transparency) 113 | .add(handlebars) 114 | 115 | .on('complete', -> 116 | expect(this[0]).toBeOnTheSameBallpark(this[1], 5)) 117 | .run() 118 | 119 | describe "with hundred todo items", -> 120 | 121 | it "should be on the same ballpark with Handlebars", -> 122 | transparency = new Benchmark 'transparency - unused template, 100 todos', 123 | setup: -> 124 | template = for i in [1..@count] 125 | $('
    ')[0] 126 | index = 0 127 | data = for i in [1..@count] 128 | for j in [1..100] 129 | {todo: Math.random()} 130 | return 131 | 132 | fn: -> 133 | Transparency.render template[index], data[index++] 134 | return 135 | 136 | handlebars = new Benchmark 'handlebars - unused and compiled template, 100 todos', 137 | setup: -> 138 | parser = for i in [1..@count] 139 | $('
    ')[0] 140 | template = for i in [1..@count] 141 | Handlebars.compile('
    {{#each this}}
    {{todo}}
    {{/each}}
    ') 142 | index = 0 143 | data = for i in [1..@count] 144 | for j in [1..100] 145 | {todo: Math.random()} 146 | return 147 | 148 | fn: -> 149 | parser[index].innerHTML = template[index] data[index++] 150 | return 151 | 152 | new Benchmark.Suite() 153 | .add(transparency) 154 | .add(handlebars) 155 | 156 | .on('complete', -> 157 | expect(this[0]).toBeOnTheSameBallpark(this[1], 7)) 158 | .run() 159 | -------------------------------------------------------------------------------- /spec/performanceSpecRunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Tests 6 | 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /spec/serverSpec.coffee: -------------------------------------------------------------------------------- 1 | {jsdom} = require "jsdom" 2 | {expect} = require "./assert" 3 | 4 | Transparency = require "../index" 5 | 6 | describe "Transparency", -> 7 | 8 | it "should work on node.js", (done) -> 9 | 10 | jsdom.env "", [], (errors, {document}) -> 11 | 12 | template = document.createElement 'div' 13 | template.innerHTML = 14 | """ 15 |
    16 |
    17 |
    18 | """ 19 | 20 | data = hello: 'Hello' 21 | 22 | expected = document.createElement 'div' 23 | expected.innerHTML = 24 | """ 25 |
    26 |
    Hello
    27 |
    28 | """ 29 | 30 | Transparency.render template, data 31 | expect(template.innerHTML).toEqual expected.innerHTML 32 | done() 33 | -------------------------------------------------------------------------------- /spec/serverSpec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Transparency, expect, jsdom; 3 | 4 | jsdom = require("jsdom").jsdom; 5 | 6 | expect = require("./assert").expect; 7 | 8 | Transparency = require("../index"); 9 | 10 | describe("Transparency", function() { 11 | return it("should work on node.js", function(done) { 12 | return jsdom.env("", [], function(errors, arg) { 13 | var data, document, expected, template; 14 | document = arg.document; 15 | template = document.createElement('div'); 16 | template.innerHTML = "
    \n
    \n
    "; 17 | data = { 18 | hello: 'Hello' 19 | }; 20 | expected = document.createElement('div'); 21 | expected.innerHTML = "
    \n
    Hello
    \n
    "; 22 | Transparency.render(template, data); 23 | expect(template.innerHTML).toEqual(expected.innerHTML); 24 | return done(); 25 | }); 26 | }); 27 | }); 28 | 29 | }).call(this); 30 | -------------------------------------------------------------------------------- /spec/testlingSpecRunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/attributeFactory.coffee: -------------------------------------------------------------------------------- 1 | _ = require '../lib/lodash' 2 | helpers = require './helpers' 3 | 4 | module.exports = AttributeFactory = 5 | Attributes: {} 6 | 7 | createAttribute: (element, name) -> 8 | Attr = AttributeFactory.Attributes[name] or Attribute 9 | new Attr(element, name) 10 | 11 | 12 | class Attribute 13 | constructor: (@el, @name) -> 14 | @templateValue = @el.getAttribute(@name) || '' 15 | 16 | set: (value) -> 17 | @el[@name] = value 18 | @el.setAttribute @name, value.toString() 19 | 20 | 21 | class BooleanAttribute extends Attribute 22 | BOOLEAN_ATTRIBUTES = ['hidden', 'async', 'defer', 'autofocus', 'formnovalidate', 'disabled', 23 | 'autofocus', 'formnovalidate', 'multiple', 'readonly', 'required', 'checked', 'scoped', 24 | 'reversed', 'selected', 'loop', 'muted', 'autoplay', 'controls', 'seamless', 'default', 25 | 'ismap', 'novalidate', 'open', 'typemustmatch', 'truespeed'] 26 | 27 | for name in BOOLEAN_ATTRIBUTES 28 | AttributeFactory.Attributes[name] = this 29 | 30 | constructor: (@el, @name) -> 31 | @templateValue = @el.getAttribute(@name) || false 32 | 33 | set: (value) -> 34 | @el[@name] = value 35 | if value 36 | then @el.setAttribute @name, @name 37 | else @el.removeAttribute @name 38 | 39 | 40 | class Text extends Attribute 41 | AttributeFactory.Attributes['text'] = this 42 | 43 | constructor: (@el, @name) -> 44 | @templateValue = 45 | (child.nodeValue for child in @el.childNodes when child.nodeType == helpers.TEXT_NODE).join '' 46 | 47 | @children = _.toArray @el.children 48 | 49 | unless @textNode = @el.firstChild 50 | @el.appendChild @textNode = @el.ownerDocument.createTextNode '' 51 | else unless @textNode.nodeType is helpers.TEXT_NODE 52 | @textNode = @el.insertBefore @el.ownerDocument.createTextNode(''), @textNode 53 | 54 | set: (text) -> 55 | # content editable creates a new text node 56 | # which needs to be removed, otherwise the content is duplicated to both text nodes. 57 | # http://jsfiddle.net/xAMQa/1/ 58 | @el.removeChild child while child = @el.firstChild 59 | 60 | @textNode.nodeValue = text 61 | @el.appendChild @textNode 62 | 63 | for child in @children 64 | @el.appendChild child 65 | 66 | 67 | class Html extends Attribute 68 | AttributeFactory.Attributes['html'] = this 69 | 70 | constructor: (@el) -> 71 | @templateValue = '' 72 | @children = _.toArray @el.children 73 | 74 | set: (html) -> 75 | @el.removeChild child while child = @el.firstChild 76 | 77 | @el.innerHTML = html + @templateValue 78 | for child in @children 79 | @el.appendChild child 80 | 81 | 82 | class Class extends Attribute 83 | AttributeFactory.Attributes['class'] = this 84 | 85 | constructor: (el) -> super el, 'class' 86 | -------------------------------------------------------------------------------- /src/context.coffee: -------------------------------------------------------------------------------- 1 | {before, after, chainable, cloneNode} = require './helpers' 2 | Instance = require './instance' 3 | 4 | # **Context** stores the original `template` elements and is responsible for creating, 5 | # adding and removing template `instances` to match the amount of `models`. 6 | module.exports = class Context 7 | 8 | detach = chainable -> 9 | @parent = @el.parentNode 10 | if @parent 11 | @nextSibling = @el.nextSibling 12 | @parent.removeChild @el 13 | 14 | attach = chainable -> 15 | if @parent 16 | if @nextSibling 17 | then @parent.insertBefore @el, @nextSibling 18 | else @parent.appendChild @el 19 | 20 | constructor: (@el, @Transparency) -> 21 | @template = cloneNode @el 22 | @instances = [new Instance(@el, @Transparency)] 23 | @instanceCache = [] 24 | 25 | render: \ 26 | before(detach) \ 27 | after(attach) \ 28 | chainable \ 29 | (models, directives, options) -> 30 | 31 | # Cloning DOM elements is expensive, so save unused template `instances` and reuse them later. 32 | while models.length < @instances.length 33 | @instanceCache.push @instances.pop().remove() 34 | 35 | # DOM elements needs to be created before rendering 36 | # https://github.com/leonidas/transparency/issues/94 37 | while models.length > @instances.length 38 | instance = @instanceCache.pop() || new Instance(cloneNode(@template), @Transparency) 39 | @instances.push instance.appendTo(@el) 40 | 41 | for model, index in models 42 | instance = @instances[index] 43 | 44 | children = [] 45 | instance 46 | .prepare(model, children) 47 | .renderValues(model, children) 48 | .renderDirectives(model, index, directives) 49 | .renderChildren(model, children, directives, options) 50 | -------------------------------------------------------------------------------- /src/elementFactory.coffee: -------------------------------------------------------------------------------- 1 | _ = require '../lib/lodash.js' 2 | helpers = require './helpers' 3 | AttributeFactory = require './attributeFactory' 4 | 5 | 6 | module.exports = ElementFactory = 7 | Elements: input: {} 8 | 9 | createElement: (el) -> 10 | if 'input' == name = el.nodeName.toLowerCase() 11 | El = ElementFactory.Elements[name][el.type.toLowerCase()] || Input 12 | else 13 | El = ElementFactory.Elements[name] || Element 14 | 15 | new El(el) 16 | 17 | 18 | class Element 19 | constructor: (@el) -> 20 | @attributes = {} 21 | @childNodes = _.toArray @el.childNodes 22 | @nodeName = @el.nodeName.toLowerCase() 23 | @classNames = @el.className.split ' ' 24 | @originalAttributes = {} 25 | 26 | empty: -> 27 | @el.removeChild child while child = @el.firstChild 28 | this 29 | 30 | reset: -> 31 | for name, attribute of @attributes 32 | attribute.set attribute.templateValue 33 | 34 | render: (value) -> @attr 'text', value 35 | 36 | attr: (name, value) -> 37 | attribute = @attributes[name] ||= AttributeFactory.createAttribute @el, name, value 38 | attribute.set value if value? 39 | attribute 40 | 41 | renderDirectives: (model, index, attributes) -> 42 | for own name, directive of attributes when typeof directive == 'function' 43 | value = directive.call model, 44 | element: @el 45 | index: index 46 | value: @attr(name).templateValue 47 | 48 | @attr(name, value) if value? 49 | 50 | 51 | class Select extends Element 52 | ElementFactory.Elements['select'] = this 53 | 54 | constructor: (el) -> 55 | super el 56 | @elements = helpers.getElements el 57 | 58 | render: (value) -> 59 | value = value.toString() 60 | for option in @elements when option.nodeName == 'option' 61 | option.attr 'selected', option.el.value == value 62 | 63 | 64 | class VoidElement extends Element 65 | 66 | # From http://www.w3.org/TR/html-markup/syntax.html: void elements in HTML 67 | VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 68 | 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'] 69 | 70 | for nodeName in VOID_ELEMENTS 71 | ElementFactory.Elements[nodeName] = this 72 | 73 | attr: (name, value) -> super name, value unless name in ['text', 'html'] 74 | 75 | 76 | class Input extends VoidElement 77 | render: (value) -> @attr 'value', value 78 | 79 | 80 | class TextArea extends Input 81 | ElementFactory.Elements['textarea'] = this 82 | 83 | 84 | class Checkbox extends Input 85 | ElementFactory.Elements['input']['checkbox'] = this 86 | 87 | render: (value) -> @attr 'checked', Boolean(value) 88 | 89 | 90 | class Radio extends Checkbox 91 | ElementFactory.Elements['input']['radio'] = this 92 | -------------------------------------------------------------------------------- /src/helpers.coffee: -------------------------------------------------------------------------------- 1 | ElementFactory = require './elementFactory' 2 | 3 | exports.before = (decorator) -> (method) -> -> 4 | decorator.apply this, arguments 5 | method.apply this, arguments 6 | 7 | exports.after = (decorator) -> (method) -> -> 8 | method.apply this, arguments 9 | decorator.apply this, arguments 10 | 11 | # Decorate method to support chaining. 12 | # 13 | # // in console 14 | # > o = {} 15 | # > o.hello = "Hello" 16 | # > o.foo = chainable(function(){console.log(this.hello + " World")}); 17 | # > o.foo().hello 18 | # Hello World 19 | # "Hello" 20 | # 21 | exports.chainable = exports.after -> this 22 | 23 | exports.onlyWith$ = (fn) -> if jQuery? || Zepto? 24 | do ($ = jQuery || Zepto) -> fn arguments 25 | 26 | exports.getElements = (el) -> 27 | elements = [] 28 | _getElements el, elements 29 | elements 30 | 31 | _getElements = (template, elements) -> 32 | child = template.firstChild 33 | while child 34 | if child.nodeType == exports.ELEMENT_NODE 35 | elements.push new ElementFactory.createElement(child) 36 | _getElements child, elements 37 | 38 | child = child.nextSibling 39 | 40 | exports.ELEMENT_NODE = 1 41 | exports.TEXT_NODE = 3 42 | 43 | # IE8 <= fails to clone detached nodes properly, shim with jQuery 44 | # jQuery.clone: https://github.com/jquery/jquery/blob/master/src/manipulation.js#L594 45 | # jQuery.support.html5Clone: https://github.com/jquery/jquery/blob/master/src/support.js#L83 46 | html5Clone = () -> document.createElement('nav').cloneNode(true).outerHTML != '<:nav>' 47 | exports.cloneNode = 48 | if not document? or html5Clone() 49 | (node) -> node.cloneNode true 50 | else 51 | (node) -> 52 | cloned = Transparency.clone(node) 53 | if cloned.nodeType == exports.ELEMENT_NODE 54 | cloned.removeAttribute expando 55 | for element in cloned.getElementsByTagName '*' 56 | element.removeAttribute expando 57 | cloned 58 | 59 | # Minimal implementation of jQuery.data 60 | # 61 | # // in console 62 | # > template = document.getElementById('template') 63 | # > data(template).hello = 'Hello World!' 64 | # > console.log(data(template).hello) 65 | # Hello World! 66 | # 67 | # Expanding DOM element with a JS object is generally unsafe. 68 | # However, as references to expanded DOM elements are never lost, no memory leaks are introduced 69 | # http://perfectionkills.com/whats-wrong-with-extending-the-dom/ 70 | expando = 'transparency' 71 | exports.data = (element) -> element[expando] ||= {} 72 | 73 | exports.nullLogger = () -> 74 | exports.consoleLogger = -> console.log arguments 75 | exports.log = exports.nullLogger 76 | -------------------------------------------------------------------------------- /src/instance.coffee: -------------------------------------------------------------------------------- 1 | _ = require '../lib/lodash.js' 2 | {chainable} = helpers = require './helpers' 3 | 4 | # Template **Instance** is created for each model we are about to render. 5 | # `instance` object keeps track of template DOM nodes and elements. 6 | # It memoizes the matching elements to `queryCache` in order to speed up the rendering. 7 | module.exports = class Instance 8 | 9 | constructor: (template, @Transparency) -> 10 | @queryCache = {} 11 | @childNodes = _.toArray template.childNodes 12 | @elements = helpers.getElements template 13 | 14 | remove: chainable -> 15 | for node in @childNodes 16 | node.parentNode.removeChild node 17 | 18 | appendTo: chainable (parent) -> 19 | for node in @childNodes 20 | parent.appendChild node 21 | 22 | prepare: chainable (model) -> 23 | for element in @elements 24 | element.reset() 25 | 26 | # A bit of offtopic, but let's think about writing event handlers. 27 | # It would be convenient to have an access to the associated `model` 28 | # when the user clicks a todo element without setting `data-id` attributes or other 29 | # identifiers manually. So be it. 30 | # 31 | # $('#todos').on('click', '.todo', function(e) { 32 | # console.log(e.target.transparency.model); 33 | # }); 34 | # 35 | helpers.data(element.el).model = model 36 | 37 | # Rendering values takes care of the most common use cases like 38 | # rendering text content, form values and DOM elements (.e.g., Backbone Views). 39 | # Rendering as a text content is a safe default, as it is HTML escaped 40 | # by the browsers. 41 | renderValues: chainable (model, children) -> 42 | if _.isElement(model) and element = @elements[0] 43 | element.empty().el.appendChild model 44 | 45 | else if typeof model == 'object' 46 | for own key, value of model when value? 47 | 48 | if _.isString(value) or _.isNumber(value) or _.isBoolean(value) or _.isDate(value) 49 | for element in @matchingElements key 50 | 51 | # Element type also affects on rendering. Given a model 52 | # 53 | # {todo: 'Do some OSS', type: 2} 54 | # 55 | # `div` element should have `textContent` set, 56 | # `input` element should have `value` attribute set and 57 | # with `select` element, the matching `option` element should set to `selected="selected"`. 58 | # 59 | #
    60 | #
    Do some OSS 61 | # 62 | # 66 | #
    67 | # 68 | element.render value 69 | 70 | # Rendering nested models breadth-first is more robust, as there might be colliding keys, 71 | # i.e., given a model 72 | # 73 | # { 74 | # name: "Jack", 75 | # friends: [ 76 | # {name: "Matt"}, 77 | # {name: "Carol"} 78 | # ] 79 | # } 80 | # 81 | # and a template 82 | # 83 | #
    84 | #
    85 | #
    86 | #
    87 | #
    88 | #
    89 | # 90 | # the depth-first rendering might give us wrong results, if the children are rendered 91 | # before the `name` field on the parent model (child template values are overwritten by the parent). 92 | # 93 | #
    94 | #
    Jack
    95 | #
    96 | #
    Jack
    97 | #
    Jack
    98 | #
    99 | #
    100 | # 101 | # Save the key of the child model and take care of it once 102 | # we're done with the parent model. 103 | else if typeof value == 'object' 104 | children.push key 105 | 106 | # With `directives`, user can give explicit rules for rendering and set 107 | # attributes, which would be potentially unsafe by default (e.g., unescaped HTML content or `src` attribute). 108 | # Given a template 109 | # 110 | #
    111 | #
    112 | #
    113 | # 114 | #
    115 | # 116 | # and a model and directives 117 | # 118 | # model = { 119 | # content: "" 120 | # url: "http://trusted.com/funny.gif" 121 | # }; 122 | # 123 | # directives = { 124 | # escaped: { text: { function() { return this.content } } }, 125 | # unescaped: { html: { function() { return this.content } } }, 126 | # trusted: { url: { function() { return this.url } } } 127 | # } 128 | # 129 | # $('#template').render(model, directives); 130 | # 131 | # should give the result 132 | # 133 | #
    134 | #
    <script>alert('Injected')</script>
    135 | #
    136 | # 137 | #
    138 | # 139 | # Directives are executed after the default rendering, so that they can be used for overriding default rendering. 140 | renderDirectives: chainable (model, index, directives) -> 141 | for own key, attributes of directives when typeof attributes == 'object' 142 | model = {value: model} unless typeof model == 'object' 143 | 144 | for element in @matchingElements key 145 | element.renderDirectives model, index, attributes 146 | 147 | renderChildren: chainable (model, children, directives, options) -> 148 | for key in children 149 | for element in @matchingElements key 150 | @Transparency.render element.el, model[key], directives[key], options 151 | 152 | matchingElements: (key) -> 153 | elements = @queryCache[key] ||= (el for el in @elements when @Transparency.matcher el, key) 154 | helpers.log "Matching elements for '#{key}':", elements 155 | elements 156 | 157 | -------------------------------------------------------------------------------- /src/transparency.coffee: -------------------------------------------------------------------------------- 1 | _ = require '../lib/lodash.js' 2 | helpers = require './helpers' 3 | Context = require './context' 4 | 5 | # **Transparency** is a client-side template engine which binds JSON objects to DOM elements. 6 | # 7 | # // Template: 8 | # // 9 | # // 12 | # 13 | # template = document.querySelector('#todos'); 14 | # 15 | # models = [ 16 | # {todo: "Eat"}, 17 | # {todo: "Do some programming"}, 18 | # {todo: "Sleep"} 19 | # ]; 20 | # 21 | # Transparency.render(template, models); 22 | # 23 | # // Result: 24 | # // 29 | # 30 | # This documentation focuses on the implementation and internals. 31 | # For the full API reference, please see the README. 32 | 33 | # ## Public API 34 | Transparency = {} 35 | 36 | # `Transparency.render` maps JSON objects to DOM elements. 37 | Transparency.render = (context, models = [], directives = {}, options = {}) -> 38 | # First, check if we are in debug mode and if so, log the arguments. 39 | log = if options.debug and console then helpers.consoleLogger else helpers.nullLogger 40 | log "Transparency.render:", context, models, directives, options 41 | 42 | return unless context 43 | models = [models] unless _.isArray models 44 | 45 | # Context element, state and functionality is wrapped to `Context` object. Get it, or create a new 46 | # if it doesn't exist yet. 47 | context = helpers.data(context).context ||= new Context(context, Transparency) 48 | 49 | # Rendering is a lot faster when the context element is detached from the DOM, as 50 | # reflow calculations are not triggered. So, detach it before rendering. 51 | context.render(models, directives, options).el 52 | 53 | # ### Configuration 54 | 55 | # By default, Transparency matches model properties to elements by `id`, `class`, `name` and `data-bind` attributes. 56 | # Override `Transparency.matcher` to change the default behavior. 57 | # 58 | # // Match only by `data-bind` attribute 59 | # Transparency.matcher = function (element, key) { 60 | # element.el.getAttribute('data-bind') == key; 61 | # }; 62 | # 63 | Transparency.matcher = (element, key) -> 64 | element.el.id == key || 65 | key in element.classNames || 66 | element.el.name == key || 67 | element.el.getAttribute('data-bind') == key 68 | 69 | # IE6-8 fails to clone nodes properly. By default, Transparency uses jQuery.clone() as a shim. 70 | # Override `Transparency.clone` with a custom clone function, if oldIE needs to be 71 | # supported without jQuery. 72 | # 73 | # Transparency.clone = myCloneFunction; 74 | # 75 | Transparency.clone = (node) -> 76 | $(node).clone()[0] 77 | 78 | # ### Exports 79 | 80 | # In order to use Transparency as a jQuery plugin, add Transparency.jQueryPlugin to jQuery.fn object. 81 | # 82 | # $.fn.render = Transparency.jQueryPlugin; 83 | # 84 | # // Render with jQuery 85 | # $('#template').render({hello: 'World'}); 86 | # 87 | Transparency.jQueryPlugin = helpers.chainable (models, directives, options) -> 88 | for context in this 89 | Transparency.render context, models, directives, options 90 | 91 | # Register Transparency, if jQuery or Zepto is defined 92 | if (jQuery? || Zepto?) 93 | $ = jQuery || Zepto 94 | $?.fn.render = Transparency.jQueryPlugin 95 | 96 | # Exports for node.js, browser global and AMD 97 | if module?.exports then module.exports = Transparency 98 | if window? then window.Transparency = Transparency 99 | if define?.amd then define -> Transparency 100 | -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "on_start": { 3 | "command": "browserstack tunnel localhost:", 4 | "wait_for_text": "You can now access your local server(s) in our remote browser" 5 | }, 6 | 7 | "test_page": "spec/SpecRunner.html", 8 | 9 | "launchers": { 10 | "bs_ie_10": { 11 | "command": "browserstack launch --attach ie:10.0 ", 12 | "protocol": "browser" 13 | }, 14 | "bs_ie_9": { 15 | "command": "browserstack launch --attach ie:9.0 ", 16 | "protocol": "browser" 17 | }, 18 | "bs_ie_8": { 19 | "command": "browserstack launch --attach ie:8.0 ", 20 | "protocol": "browser" 21 | }, 22 | "bs_ie_7": { 23 | "command": "browserstack launch --attach ie:7.0 ", 24 | "protocol": "browser" 25 | }, 26 | "bs_ie_6": { 27 | "command": "browserstack launch --attach ie:6.0 ", 28 | "protocol": "browser" 29 | } 30 | }, 31 | 32 | "launch_in_dev": ["Chrome", "Safari", "Firefox", "PhantomJS", "bs_ie_7", "bs_ie_8", "bs_ie_9", "bs_ie_10"] 33 | } 34 | -------------------------------------------------------------------------------- /testlingSpecRunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 36 | 37 | 38 | --------------------------------------------------------------------------------