├── filename ├── render.js ├── util.js ├── .npmignore ├── requisite.js ├── dynamictemplate.js ├── .travis.yml ├── .gitignore ├── src ├── dynamictemplate.coffee ├── requisite.coffee ├── alias.coffee ├── render.coffee ├── util.coffee ├── doctype.coffee ├── test │ ├── render.coffee │ └── template.coffee ├── cache.coffee ├── schema.coffee └── template.coffee ├── example ├── list.css ├── svg.html ├── circles.html ├── quadtree.html ├── animation.html ├── list.html ├── backbone.html ├── base.css ├── svg.coffee ├── list.coffee ├── animation.coffee ├── circles.coffee ├── backbone.coffee └── quadtree.coffee ├── LICENSE ├── package.json ├── Cakefile └── README.md /filename: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /render.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/render') 3 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/util') 3 | 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Cakefile 2 | src/ 3 | test/ 4 | example/ 5 | filename 6 | -------------------------------------------------------------------------------- /requisite.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/requisite') 3 | 4 | -------------------------------------------------------------------------------- /dynamictemplate.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/dynamictemplate') 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_script: "npm install --dev" 2 | script: "npm test" 3 | language: node_js 4 | node_js: 5 | - 0.8 6 | - 0.10 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *swp 3 | lib/* 4 | test/* 5 | example/* 6 | filename 7 | *.browser.* 8 | gh-pages 9 | .lock-wscript 10 | node_modules/ 11 | -------------------------------------------------------------------------------- /src/dynamictemplate.coffee: -------------------------------------------------------------------------------- 1 | 2 | { Tag, Builder } = require 'asyncxml' 3 | Template = require './template' 4 | 5 | module.exports = { Tag, Builder, Template } 6 | 7 | # browser support 8 | 9 | ( -> 10 | if @dynamictemplate? 11 | @dynamictemplate.Template = Template 12 | @dynamictemplate.Builder = Builder 13 | @dynamictemplate.Tag = Tag 14 | else 15 | @dynamictemplate = module.exports 16 | ).call window if process.title is 'browser' 17 | 18 | -------------------------------------------------------------------------------- /src/requisite.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | # optional require to lazy require template masks generated eg by dt-compiler 4 | requisite = (dirs..., name) -> 5 | fullname = path.join(dirs..., name) 6 | mod = (x) -> x 7 | try 8 | mod = require(fullname) 9 | catch err 10 | console.warn "requisite #{name} went missing." 11 | finally 12 | return mod 13 | 14 | # exports 15 | 16 | requisite.requisite = requisite 17 | module.exports = requisite 18 | -------------------------------------------------------------------------------- /src/alias.coffee: -------------------------------------------------------------------------------- 1 | 2 | # schema aliases 3 | aliases = 4 | 'default':'xml' 5 | 'null':'none' 6 | 'nil':'none' 7 | '0':'none' 8 | '5':'html5' 9 | 5:'html5' 10 | 0:'none' 11 | 'ce':'html-ce' 12 | '1.1':'xhtml1.1' 13 | 'html11':'xhtml1.1' 14 | 'basic':'xhtml' 15 | 'xhtml1':'xhtml' 16 | 'xhtml-basic':'xhtml' 17 | 'xhtml-strict':'strict' 18 | 'xhtml-mobile':'mobile' 19 | 'xhtml-frameset':'frameset' 20 | 'xhtml-trasitional':'transitional' 21 | 'svg':'svg1.1' 22 | 23 | # exports 24 | 25 | module.exports = { 26 | aliases, 27 | } 28 | -------------------------------------------------------------------------------- /example/list.css: -------------------------------------------------------------------------------- 1 | /* depends on base.css */ 2 | 3 | .list { 4 | clear:both; 5 | list-style-type: none; 6 | border:1px dashed gray; 7 | border-bottom:0px; 8 | padding:0; 9 | } 10 | .list li { 11 | list-style-type: none; 12 | border-bottom:1px dashed gray; 13 | padding:2px; 14 | padding-left:20px; 15 | margin:0; 16 | min-height: 2em; 17 | } 18 | 19 | .control { 20 | text-align:center; 21 | border-radius:1.1em; 22 | min-height: 1.3em; 23 | min-width: 1.3em; 24 | font-size:1.1em; 25 | float:right; 26 | padding:0; 27 | } 28 | 29 | input[type="number"] { 30 | width: 3em; 31 | } 32 | -------------------------------------------------------------------------------- /example/svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dynamictemplate svg test 5 | 6 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/circles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dynamictemplate svg bubbles 5 | 6 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/quadtree.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dynamictemplate svg bubbles 5 | 6 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/animation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dynamictemplate benchmark 5 | 6 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/render.coffee: -------------------------------------------------------------------------------- 1 | BufferStream = require 'bufferstream' 2 | streamify = require 'dt-stream' 3 | 4 | ## 5 | # useful to just pump out html data to a stream 6 | pipe = (template, stream) -> 7 | return streamify(template, {stream}).stream.once 'close', -> 8 | template.remove() # prevent memory leak 9 | 10 | ## 11 | # useful for rendering a template directly into a response: 12 | # use this to let template generation run and buffer the out when needed 13 | # render(new Template(schema:'html5', body)).pipe(res) 14 | # use this to let template geenration be paused by output stream 15 | # render(new Template(schema:'html5', body), res) 16 | render = (template, stream) -> 17 | return pipe template, stream ? new BufferStream 18 | encoding:'utf-8' # FIXME use template.options.encoding 19 | size:'flexible' 20 | disabled:yes 21 | 22 | 23 | # exports 24 | 25 | module.exports = render 26 | module.exports.render = render 27 | module.exports.pipe = pipe 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 ▟ ▖▟ ▖(dodo) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /example/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dynamictemplate test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

19 | Hit the buttons! 20 | Source Code 21 | 22 |

23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name": "dynamictemplate" 2 | , "description": "Δt - async & dynamic templating engine" 3 | , "version": "0.7.1" 4 | , "homepage": "https://github.com/dodo/node-dynamictemplate" 5 | , "author": "dodo (https://github.com/dodo)" 6 | , "repository": {"type": "git", "url": "git://github.com/dodo/node-dynamictemplate.git"} 7 | , "main": "dynamictemplate.js" 8 | , "engines": {"node": ">= 0.4.x"} 9 | , "keywords": ["dt", "async", "dynamic", "event", "template", "generation", "stream", "browser"] 10 | , "scripts": { 11 | "test": "cake build && nodeunit test", 12 | "prepublish": "cake build"} 13 | , "dependencies": { 14 | "bufferstream": ">= 0.6.0", 15 | "asyncxml": ">= 0.6.0"} 16 | , "peerDependencies": { 17 | "dt-stream": ">= 0.3.0"} 18 | , "devDependencies": { 19 | "dt-stream": ">= 0.3.0", 20 | "browserify": "1.6.1", 21 | "scopify": ">= 0.2.1", 22 | "coffee-script": ">= 1.1.3", 23 | "muffin": ">= 0.2.6", 24 | "nodeunit": ">= 0.5.4"} 25 | , "licenses" : [ 26 | { "type": "MIT" , 27 | "url": "http://github.com/dodo/node-dynamictemplate/raw/master/LICENSE"} ] 28 | } 29 | -------------------------------------------------------------------------------- /example/backbone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dynamictemplate test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

21 | Hit the buttons! 22 | Source Code 23 | 24 |

25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/util.coffee: -------------------------------------------------------------------------------- 1 | 2 | # add class to the list of classes in attribute 'class' 3 | exports.addClass = (tag, classes...) -> 4 | return unless tag?.attr 5 | tagclasses = (tag.attr('class') ? "").split ' ' 6 | for cls in classes when tagclasses.indexOf(cls) is -1 7 | tagclasses.push "#{cls}" 8 | tag.attr 'class', tagclasses.join(' ').trim().replace(/\s\s/g, " ") 9 | 10 | # remove class from the list of classes in attribute 'class' 11 | exports.removeClass = (tag, classes...) -> 12 | return unless tag?.attr 13 | tagclass = " #{tag.attr('class') ? ''} " 14 | tagclasses = tagclass.trim().split ' ' 15 | for cls in classes 16 | i = tagclasses.indexOf(cls) 17 | unless i is -1 18 | tagclasses[i] = '' 19 | tagclass = tagclass.replace(" #{cls} ", " ") 20 | tag.attr 'class', tagclass.trim() 21 | 22 | # run multiple functions in a row with the same this context 23 | exports.compose = (functions...) -> 24 | if functions.length is 1 and Array.isArray(functions[0]) 25 | functions = functions[0] 26 | return (args...) -> 27 | for fun in functions 28 | fun.apply this, args 29 | this 30 | 31 | # takes a function to create a clojure to call partial on an element 32 | exports.partialize = (create, moargs...) -> 33 | return (args...) -> 34 | partial = create.call(this, moargs..., args...) 35 | @partial partial 36 | return partial 37 | -------------------------------------------------------------------------------- /src/doctype.coffee: -------------------------------------------------------------------------------- 1 | 2 | doctype = 3 | 'xml': ({encoding}) -> "" 4 | 'html' : -> "" 5 | 'html5': -> "#{do doctype.html}" 6 | 'mobile' : -> 7 | '' 10 | 'html-ce': -> 11 | '' 14 | 'strict' : -> 15 | '' 18 | 'xhtml1.1': -> 19 | '' 22 | 'xhtml' : -> 23 | '' 26 | 'frameset': -> 27 | '' 30 | 'transitional': -> 31 | '' 34 | 35 | # exports 36 | 37 | module.exports = { 38 | doctype, 39 | } 40 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | { run, compileScript, minifyScript, writeFile, notify } = require 'muffin' 3 | 4 | task 'compile', 'compile coffeescript → javascript', (options) -> 5 | run 6 | options:options 7 | after:options.after 8 | files:[ 9 | "./src/**/*.coffee" 10 | ] 11 | map: 12 | 'src/test/(.+).coffee': (m) -> 13 | compileScript m[0], path.join("test" ,"#{m[1]}.js"), options 14 | 'src/(.+).coffee': (m) -> 15 | compileScript m[0], path.join("lib" ,"#{m[1]}.js"), options 16 | 17 | task 'bundle', 'build a browser bundle', (options) -> 18 | browserify = require 'browserify' 19 | { createScope } = require 'scopify' 20 | run 21 | options:options 22 | files:[ 23 | "./lib/*.js" 24 | ] 25 | map: 26 | 'lib/(dynamictemplate).js': (m) -> 27 | bundle = browserify({ 28 | require: path.join(__dirname, m[0]) 29 | cache: on 30 | }).use(createScope require:'./'+m[1]).bundle() 31 | notify m[0], "successful browserify!" 32 | filename = "#{m[1]}.browser.js" 33 | writeFile(filename, bundle, options).then -> 34 | minifyScript filename, options 35 | 36 | task 'build', 'compile && bundle', (options) -> 37 | timeout = 0 38 | options.after = -> 39 | clearTimeout(timeout) if timeout 40 | timeout = setTimeout( -> 41 | invoke 'bundle', options 42 | , 250) 43 | invoke 'compile' -------------------------------------------------------------------------------- /src/test/render.coffee: -------------------------------------------------------------------------------- 1 | { Template } = require '../dynamictemplate' 2 | { render, pipe } = require '../render' 3 | 4 | 5 | module.exports = 6 | 7 | pipe: (æ) -> 8 | output = pipe new Template schema:5, doctype:on, -> 9 | @$html -> 10 | @$head -> 11 | @$title "holla" 12 | @$body -> 13 | @$p "hello world" 14 | 15 | 16 | output.on 'data', (tag) -> æ.equal results.shift(), tag.toString() 17 | output.on 'end', -> 18 | æ.equal 0, results.length 19 | æ.done() 20 | results = [ 21 | '' 22 | '' 23 | '' 24 | '' 25 | 'holla' 26 | '' 27 | '' 28 | '' 29 | '

' 30 | 'hello world' 31 | '

' 32 | '' 33 | '' 34 | ] 35 | 36 | 37 | simple: (æ) -> 38 | output = render new Template schema:5, doctype:on, -> 39 | @$html -> 40 | @$head -> 41 | @$title "holla" 42 | @$body -> 43 | @$p "hello world" 44 | 45 | 46 | output.on 'data', (tag) -> æ.equal results.shift(), tag.toString() 47 | output.on 'end', -> 48 | æ.equal 0, results.length 49 | æ.done() 50 | results = [ 51 | '' 52 | '' 53 | '' 54 | '' 55 | 'holla' 56 | '' 57 | '' 58 | '' 59 | '

' 60 | 'hello world' 61 | '

' 62 | '' 63 | '' 64 | ] 65 | -------------------------------------------------------------------------------- /example/base.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background:white; 4 | color: black; 5 | } 6 | 7 | a, input { 8 | text-decoration:none; 9 | color: #545454; 10 | padding: 1px; 11 | border: 1px solid transparent; 12 | margin: 3px; 13 | display:inline-block; 14 | min-width: 1em; 15 | text-align:center; 16 | vertical-align:middle; 17 | -webkit-transition: 300ms; 18 | -moz-transition: 300ms; 19 | -o-transition: 300ms; 20 | transition: 300ms; 21 | border-radius:0.3em; 22 | 23 | background-size: 0% 0%; 24 | background-position: center center; 25 | background-repeat: no-repeat; 26 | background-color:transparent; 27 | background-image: -webkit-radial-gradient(circle contain, black, black 50%, transparent 50%, transparent); 28 | background-image: -moz-radial-gradient(circle contain, black, black 50%, transparent 50%, transparent); 29 | background-image: -o-radial-gradient(circle contain, black, black 50%, transparent 50%, transparent); 30 | background-image: -ms-radial-gradient(circle contain, black, black 50%, transparent 50%, transparent); 31 | background-image: radial-gradient(circle contain, black, black 50%, transparent 50%, transparent); 32 | } 33 | 34 | a:hover, a:focus, input:hover, input:focus { 35 | background-size: 2222% 2222%; 36 | border-color:transparent; 37 | color:white; 38 | } 39 | 40 | input { 41 | text-align:left; 42 | padding: 3px; 43 | 44 | -webkit-box-shadow: inset 0 0 3px rgba(0,0,0,0.42); 45 | -moz-box-shadow: inset 0 0 3px rgba(0,0,0,0.42); 46 | box-shadow: inset 0 0 3px rgba(0,0,0,0.42); 47 | } 48 | 49 | input[type="button"] { 50 | cursor: pointer; 51 | background-color:rgba(0,0,0,0.1); 52 | -webkit-box-shadow: inset 0 0 3px rgba(255,255,255,0.42); 53 | -moz-box-shadow: inset 0 0 3px rgba(255,255,255,0.42); 54 | box-shadow: inset 0 0 3px rgba(255,255,255,0.42); 55 | } 56 | -------------------------------------------------------------------------------- /example/svg.coffee: -------------------------------------------------------------------------------- 1 | srcdirurl = "https://github.com/dodo/node-dynamictemplate/blob/master/example" 2 | { Template, domify } = window.dynamictemplate 3 | { abs } = Math 4 | running = yes 5 | 6 | button = (tag, id, value) -> 7 | tag.$input 8 | class:'button' 9 | type:'button' 10 | id:id 11 | value:value 12 | title:id 13 | 14 | svgns = "http://www.w3.org/2000/svg" 15 | 16 | svg = domify new Template schema:5, -> 17 | @$div class:'controls', -> 18 | button this, "start", "▸" 19 | button this, "stop", "■" 20 | @$a href:"#{srcdirurl}/svg.coffee", "Source Code" 21 | @$div class:'canvas', -> 22 | @add new Template schema:'svg', -> 23 | @$svg { 24 | xmlns:svgns 25 | version:"1.1" 26 | height:"100px" 27 | width:"100px" 28 | preserveAspectRatio:"none" 29 | viewBox:"0 0 100 100" 30 | }, -> 31 | @$path { 32 | xmlns:svgns # is this really necessary ? 33 | d:"M0,50 Q20,0 50,50 T100,50" 34 | fill:"none" 35 | stroke:"red" 36 | }, -> 37 | i = 0 38 | setInterval => 39 | return unless running 40 | i = (i+5)%200 41 | @attr 'd', "M0,50 Q20,#{abs i-100} 50,50 T100,50" 42 | , 60 43 | 44 | 45 | # initialize 46 | 47 | svg.ready -> 48 | for el in svg.dom 49 | $('body').append el 50 | 51 | $('#start').live 'click', -> 52 | console.log "animation resumed." 53 | running = yes 54 | 55 | $('#stop').live 'click', -> 56 | console.log "animation paused." 57 | running = no 58 | 59 | console.log 'coffeescript loaded.' 60 | 61 | -------------------------------------------------------------------------------- /example/list.coffee: -------------------------------------------------------------------------------- 1 | { Template, List, jqueryify } = window.dynamictemplate 2 | EventEmitter = Template.__super__.constructor # i prefer nodejs eventemitter 3 | 4 | # the templates 5 | 6 | input = (tag, type, id, value, opts) -> 7 | tag.$input(_.extend({class:type, name:id, type, id, value}, opts)) 8 | 9 | items = null # all our list entries 10 | tplapi = new EventEmitter # every other event system should be suitable as well 11 | list = jqueryify use:List.jqueryify, new Template schema:5, -> 12 | @$div class:'controls', -> 13 | 14 | input this, 'button', "add", "list.push(Math.random())", 15 | title:"add to bottom" 16 | input this, 'button', "remove", "list.shift()", 17 | title:"remove from top" 18 | @$br() 19 | input this, 'button', "insert", "list.insert(i, text)", 20 | title:"insert at position" 21 | @span " where " 22 | @$label for:'nmb', -> 23 | @text " i=" 24 | input this, 'number','nmb', "0" 25 | @$label for:'text', -> 26 | @text " text=" 27 | input this, 'text', 'text', "", placeholder:"try me!" 28 | 29 | @$ul class:'list', -> 30 | items = new List 31 | tplapi.on 'add', => 32 | console.log "add entry" 33 | items.push @$li -> @$p "#{Math.random()}" 34 | tplapi.on 'insert', (i, text) => 35 | console.log "insert entry" 36 | items.insert i, @$li -> @$p "#{text}" 37 | tplapi.on 'remove', -> 38 | items.shift()?.remove() 39 | 40 | # initialize 41 | 42 | list.ready -> 43 | window.test = list 44 | $('body').append(list.jquery) 45 | 46 | $('#add').live 'click', -> 47 | tplapi.emit 'add' # tell the template to add a new entry 48 | 49 | $('#remove').live 'click', -> 50 | tplapi.emit 'remove' 51 | 52 | $('#insert').live 'click', -> 53 | i = parseInt($('#nmb').val()) 54 | return if i > items.length 55 | tplapi.emit 'insert', i, $('#text').val() 56 | 57 | 58 | console.log 'coffeescript loaded.' 59 | 60 | 61 | -------------------------------------------------------------------------------- /example/animation.coffee: -------------------------------------------------------------------------------- 1 | srcdirurl = "https://github.com/dodo/node-dynamictemplate/blob/master/example" 2 | { Template, jqueryify } = window.dynamictemplate 3 | { random, floor, pow } = Math 4 | running = yes 5 | maxsize = 100 6 | 7 | rnd = (most) -> 8 | floor(most*random()) 9 | 10 | differ = (c) -> 11 | r = {} 12 | for t in ['r', 'g', 'b'] 13 | r[t] = c[t] + 100 * random() - 50 14 | r[t] = 0 if c[t] < 0 15 | r[t] = 255 if c[t] > 255 16 | r[t] = floor(r[t]) 17 | return r 18 | 19 | rgb = (c) -> 20 | "background-color:rgb(#{c.r},#{c.g},#{c.b})" 21 | 22 | createSquare = (tag, col) -> 23 | tag.$div -> 24 | interval = null 25 | sat = 255/@level 26 | size = 100/pow(2,@level) 27 | setTimeout(@remove, 600 + 5*sat) if col? 28 | col ?= r:rnd(sat), g:rnd(sat), b:rnd(sat) 29 | @attr style:"#{rgb(col)};width:#{size}px;height:#{size}px;display:inline-block;" 30 | interval = setInterval => 31 | return unless running 32 | createSquare(this, differ(col)) 33 | createSquare(this, differ(col)) 34 | createSquare(this, differ(col)) 35 | createSquare(this, differ(col)) 36 | ,(300 + 4*sat) 37 | @once 'remove', -> 38 | clearInterval(interval) 39 | 40 | 41 | 42 | button = (tag, id, value) -> 43 | tag.$input class:'button', type:'button', id:id, value:value 44 | 45 | animation = jqueryify new Template schema:5, -> 46 | @$div class:'controls', -> 47 | button this, "start", "▸" 48 | button this, "stop", "■" 49 | @$a href:"#{srcdirurl}/animation.coffee", "Source Code" 50 | @$div class:'canvas', -> 51 | createSquare this 52 | createSquare this 53 | createSquare this 54 | createSquare this 55 | 56 | 57 | # initialize 58 | 59 | animation.ready -> 60 | for el in animation.jquery 61 | $('body').append el 62 | 63 | $('#start').live 'click', -> 64 | console.log "animation paused." 65 | running = yes 66 | 67 | $('#stop').live 'click', -> 68 | console.log "animation resumed." 69 | running = no 70 | 71 | console.log 'coffeescript loaded.' 72 | -------------------------------------------------------------------------------- /src/cache.coffee: -------------------------------------------------------------------------------- 1 | { Builder:DefaultBuilder } = require 'asyncxml' 2 | 3 | cache = {} 4 | 5 | 6 | ## 7 | # i made these function name short , 8 | # because its a little bit faster than with long names 9 | 10 | pp = (proto, name) -> # populate tag with specific child tag genertor 11 | proto[ name] = -> @tag.apply this, [name].concat arguments... 12 | proto["$"+name] = -> @$tag.apply this, [name].concat arguments... 13 | return 14 | 15 | ff = (proto, tags) -> # fill with tags 16 | for tagname in tags when tagname 17 | pp proto, tagname 18 | return 19 | 20 | 21 | get = (key, opts) -> 22 | ExtendedBuilder = cache[key].Builder 23 | # instantiate 24 | xml = new ExtendedBuilder opts 25 | # set Tag class so we get the same results like create(…) 26 | xml.Tag = xml.opts.Tag = cache[key].Tag 27 | return xml 28 | 29 | 30 | create = (key, opts) -> 31 | # get builder class from options 32 | Builder = opts.Builder ? DefaultBuilder 33 | # create new builder class to extend it with a schema 34 | class ExtendedBuilder extends Builder 35 | # create tag method shortcuts defined by schema 36 | ff ExtendedBuilder::, opts.schema 37 | # instantiate 38 | xml = new ExtendedBuilder opts 39 | # tag class is defined by builder 40 | Tag = xml.Tag 41 | # create new tag class to extend it with a schema 42 | class ExtendedTag extends Tag 43 | partial: (partial) -> 44 | return this unless partial.run? # tag should be already added then 45 | if partial.started 46 | console.warn "partial already started!", 47 | "you can delay it by adding option {partial:true} to your template" 48 | return this 49 | @add(partial) 50 | partial.run() # guess this is a template 51 | return this 52 | # create tag method shortcuts defined by schema 53 | ff ExtendedTag::, opts.schema 54 | # write it back so builder can use it to instantiate a new tag 55 | xml.Tag = xml.opts.Tag = ExtendedTag 56 | # fill cache 57 | cache[key] = {Builder:ExtendedBuilder, Tag:ExtendedTag} 58 | return xml 59 | 60 | 61 | lookup = (opts) -> 62 | key = opts._schema ? opts.schema 63 | return if cache[key]? 64 | get key, opts 65 | else 66 | create key, opts 67 | 68 | 69 | clear = () -> 70 | cache = {} 71 | 72 | 73 | # exports 74 | 75 | module.exports = { 76 | create, 77 | lookup, 78 | clear, 79 | get, 80 | } 81 | -------------------------------------------------------------------------------- /example/circles.coffee: -------------------------------------------------------------------------------- 1 | srcdirurl = "https://github.com/dodo/node-dynamictemplate/blob/master/example" 2 | { Template, domify } = window.dynamictemplate 3 | { random, floor, min } = Math 4 | running = yes 5 | 6 | svgns = "http://www.w3.org/2000/svg" 7 | 8 | 9 | button = (tag, id, value) -> 10 | tag.$input 11 | class:'button' 12 | type:'button' 13 | id:id 14 | value:value 15 | title:id 16 | 17 | 18 | createCircle = (tag, o) -> 19 | tag.$circle { 20 | xmlns:svgns 21 | fill:"none" 22 | stroke:"rgba(#{o.r},#{o.g},#{o.b},#{o.a})" 23 | style:"stroke-width:#{o.size}" 24 | cx:"#{o.x}" 25 | cy:"#{o.y}" 26 | r:"#{o.radius}" 27 | }, step = -> 28 | setTimeout => 29 | @attr 'r', "#{o.radius+=o['+']}" 30 | if --o.life then step.call(this) else @remove() 31 | ,60 32 | 33 | 34 | 35 | svg = domify new Template schema:5, -> 36 | @$div class:'controls', -> 37 | button this, "start", "▸" 38 | button this, "stop", "■" 39 | @$a href:"#{srcdirurl}/circles.coffee", "Source Code" 40 | @$div class:'canvas', -> 41 | @add new Template schema:'svg', -> 42 | @$svg { 43 | xmlns:svgns 44 | version:"1.1" 45 | height:"600px" 46 | width:"600px" 47 | preserveAspectRatio:"none" 48 | viewBox:"0 0 600 600" 49 | }, -> 50 | setInterval => 51 | return unless running 52 | createCircle this, 53 | '+':(0.00001 + 2 * random()) 54 | life:floor(5 + 42 * random()) 55 | radius:floor(3 + 20 * random()) 56 | size:floor(5 + 10 * random()) 57 | x:floor(600 * random()) 58 | y:floor(600 * random()) 59 | r:floor(255 * random()) 60 | g:floor(255 * random()) 61 | b:floor(255 * random()) 62 | a:min(0.8, 0.01 + random()) 63 | , 100 64 | 65 | 66 | # initialize 67 | 68 | svg.ready -> 69 | for el in svg.dom 70 | $('body').append el 71 | 72 | $('#start').live 'click', -> 73 | console.log "animation resumed." 74 | running = yes 75 | 76 | $('#stop').live 'click', -> 77 | console.log "animation paused." 78 | running = no 79 | 80 | console.log 'coffeescript loaded.' 81 | 82 | -------------------------------------------------------------------------------- /src/schema.coffee: -------------------------------------------------------------------------------- 1 | 2 | schema = 3 | 'none' : -> "" # allready includes tag 4 | 'xml' : -> "#{do schema.none }" 5 | 'html' : -> 6 | "#{do schema.xml } #{do schema['html-obsolete']} iframe label legend " + 7 | "#{do self_closing.html} html body div ul li a b body button colgroup "+ 8 | "dfn div dl dt em dd del form h1 h2 h3 h4 h5 h6 head hgroup html ins " + 9 | "li map i mark menu meter nav noscript object ol optgroup option p " + 10 | "pre script select small span strong style sub sup table tbody tfoot " + 11 | "td textarea th thead title tr u ul" 12 | 'html5': -> 13 | "#{do schema.html} #{do self_closing.html5} section article video q s "+ 14 | "audio abbr address aside bdi bdo blockquote canvas caption cite code "+ 15 | "datalist details fieldset figcaption figure footer header kbd output "+ 16 | "progress rp rt ruby samp summary time" 17 | 'strict': -> # xhtml1 18 | "#{do schema.html}" # FIXME 19 | 'xhtml': -> 20 | "#{do schema.html}" # FIXME 21 | 'xhtml1.1': -> 22 | "#{do schema.xhtml}" # FIXME 23 | 'frameset': -> 24 | "#{do schema.xhtml}" # FIXME 25 | 'transitional': -> 26 | "#{do schema.xhtml}" # FIXME 27 | 'mobile': -> 28 | "#{do schema.xhtml}" # FIXME 29 | 'html-ce': -> 30 | "#{do schema.xhtml}" # FIXME 31 | 'html-obsolete': -> 32 | "applet acronym bgsound dir frameset noframes isindex listing nextid " + 33 | "noembed plaintext rb strike xmp big blink center font marquee nobr " + 34 | "multicol spacer tt" 35 | "svg1.1": -> 36 | "altGlyph altGlyphDef altGlyphItem animate animateColor animateMotion" + 37 | "a animateTransform circle clipPath color-profile cursor defs desc" + 38 | "ellipse feBlend feColorMatrix feComponentTransfer feComposite" + 39 | "feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight" + 40 | "feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage" + 41 | "feMerge feMergeNode feMorphology feOffset fePointLight feSpotLight" + 42 | "feSpecularLighting feTile feTurbulence linearGradient polyline" + 43 | "filter font font-face font-face-format font-face-name font-face-src" + 44 | "font-face-uri foreignObject g glyph glyphRef hkern image line" + 45 | "marker mask metadata missing-glyph mpath path pattern polygon" + 46 | "radialGradient rect script set stop style svg switch symbol text" + 47 | "textPath title tref tspan use view vkern" 48 | 49 | 50 | # Valid self-closing HTML 5 elements. 51 | # set true when all tags are self closing 52 | self_closing = 53 | 'none': -> on 54 | 'xml' : -> off 55 | 'svg1.1': -> on 56 | 'html' : -> "area br col embed hr img input link meta param" 57 | 'html5': -> "#{do self_closing.html} base command keygen source track wbr main" 58 | 'mobile' : -> "#{do self_closing.xhtml}" 59 | 'html-ce': -> "#{do self_closing.xhtml}" 60 | 'strict' : -> "#{do self_closing.xhtml}" 61 | 'xhtml1.1': -> "#{do self_closing.xhtml}" 62 | 'xhtml' : -> "#{do self_closing.html}" 63 | 'frameset': -> "#{do self_closing.xhtml}" 64 | 'transitional': -> "#{do self_closing.xhtml}" 65 | 66 | 67 | # exports 68 | 69 | module.exports = { 70 | self_closing, 71 | schema, 72 | } 73 | -------------------------------------------------------------------------------- /src/template.coffee: -------------------------------------------------------------------------------- 1 | { EventEmitter } = require 'events' 2 | { schema, self_closing } = require './schema' 3 | { doctype } = require './doctype' 4 | { aliases } = require './alias' 5 | { lookup, clear } = require './cache' 6 | 7 | EVENTS = [ 8 | 'new','add' 9 | 'show', 'hide' 10 | 'attr','text', 'raw' 11 | 'remove', 'replace' 12 | 'data','close','end' 13 | ] 14 | 15 | 16 | class Template extends EventEmitter 17 | constructor: (opts = {}, template) -> 18 | # options 19 | [template, opts] = [opts, {}] if typeof opts is 'function' 20 | # defaults 21 | opts.run = off if opts.partial 22 | opts.encoding ?= 'utf-8' 23 | opts.doctype ?= off 24 | opts.end ?= on 25 | # schema 26 | opts._schema = opts.schema 27 | # resolve schema name input 28 | s = aliases[opts._schema] or opts._schema or 'xml' 29 | # load self closing schema 30 | opts.self_closing = self_closing[s]?(opts) 31 | # load tag list (xml schema) 32 | opts.schema = schema[s]?(opts).split(' ') 33 | # instantiate 34 | # lookup the cache to get a builder (and Tag class) patched with schema 35 | @xml = lookup opts 36 | @xml.template = this 37 | # override query 38 | old_query = @xml.query 39 | @xml.query = (type, tag, key) -> 40 | if type is 'tag' 41 | key.xml ? key 42 | else 43 | old_query.call(this, type, tag, key) 44 | 45 | unless opts.self_closing is off 46 | # add self closing tag behavior 47 | # some of the html tags dont need a closing tag 48 | @xml.register 'end', (tag, next) -> 49 | return next(tag) unless tag.isselfclosing 50 | if opts.self_closing is on or opts.self_closing.match tag.name 51 | tag.isempty = yes 52 | next(tag) 53 | 54 | # pipe events through 55 | EVENTS.forEach (event) => 56 | @xml.on(event, @emit.bind(this, event)) 57 | 58 | ## 59 | # start the templating process after user listened for events 60 | @run = @run.bind(this, template, opts) 61 | return if opts.run is off 62 | process.nextTick @run 63 | 64 | run: (template, opts, {restart} = {}) -> 65 | return if @started and not restart 66 | @started = yes 67 | # load doctype if enabled 68 | if opts.doctype is on 69 | opts.doctype = opts._schema or 'html' 70 | # resolve doctype name input 71 | d = aliases[opts.doctype] or opts.doctype 72 | # write doctype 73 | if opts.doctype and (dt = doctype[d]?(opts)) 74 | dt += "\n" if opts.pretty 75 | @xml.emit 'data', @xml, dt 76 | # templating process ... 77 | if typeof template is 'function' 78 | template.call @xml 79 | @end() if opts.end 80 | else if opts.end 81 | @end(template) 82 | 83 | toString: -> 84 | "[object Template]" 85 | 86 | register: => 87 | @xml.register arguments... 88 | 89 | remove: => 90 | @xml.remove arguments... 91 | @xml.template = null 92 | @xml = null 93 | 94 | end: => 95 | @xml.end arguments... 96 | 97 | ready: => 98 | @xml.ready arguments... 99 | 100 | show: => 101 | @xml.show arguments... 102 | 103 | hide: => 104 | @xml.hide arguments... 105 | 106 | 107 | # exports 108 | 109 | Template.schema = schema 110 | Template.doctype = doctype 111 | Template.self_closing = self_closing 112 | Template.aliases = aliases 113 | Template.clearCache = clear 114 | module.exports = Template 115 | -------------------------------------------------------------------------------- /example/backbone.coffee: -------------------------------------------------------------------------------- 1 | { Template, List, jqueryify } = window.dynamictemplate 2 | { random, floor } = Math 3 | { isArray } = Array 4 | 5 | # use an even simpler equals comparator for adiff 6 | 7 | adiff = window.adiff({ # npm i adiff - https://github.com/dominictarr/adiff 8 | equal: (a, b) -> 9 | return no if a and not b 10 | return no if isArray(a) and a.length isnt b.length 11 | return a is b 12 | }, window.adiff) 13 | 14 | # helpers 15 | 16 | EventHandler = (handler) -> 17 | return (ev) -> 18 | ev?.preventDefault?() 19 | handler.apply(this, arguments) 20 | no 21 | 22 | input = (tag, type, id, value, opts = {}) -> 23 | tag.$input(_.extend({class:type, name:id, type, id, value}, opts)) 24 | 25 | sync = (items, collection, options) -> 26 | # now its gonna get dirty! 27 | bycid = {} 28 | removed = [] 29 | old_models = [] 30 | # rebuild old collection state 31 | for item in items 32 | bycid[item.cid] = item 33 | old_models.push collection.getByCid(item.cid) 34 | # apply diff patches on items list 35 | for patch in adiff.diff(old_models, collection.models) 36 | # remove all items from dom before splicing them in 37 | for i in [(patch[0]) ... (patch[0]+patch[1])] 38 | removed.push items[i].remove(soft:yes) 39 | # replace models with items 40 | for i in [2 ... patch.length] 41 | patch[i] = bycid[patch[i].cid] 42 | # apply patch! 43 | items.splice.apply(items, patch) 44 | # read all removed items - this only works in the assumption, 45 | # that the collection doesn't change its size 46 | for item in removed 47 | @add(item) 48 | 49 | # different comparators 50 | 51 | ascending = (model) -> 52 | parseFloat(model.get 'value') 53 | 54 | descending = (model) -> 55 | - ascending(model) 56 | 57 | 58 | class BackboneExample extends Backbone.View 59 | 60 | # embbeded template 61 | 62 | template: (view) -> jqueryify use:List.jqueryify, new Template schema:5, -> 63 | @$div class:'controls', -> 64 | 65 | input this, 'button', "add", "collection.add({value:Math.random()})" 66 | input this, 'button', "desc", "▲", title:"descending" 67 | input this, 'button', "asc", "▼", title:"ascending" 68 | 69 | @$ul class:'list', -> 70 | items = new List 71 | view.model.on 'reset', sync.bind(this, items) 72 | view.model.on 'remove', (entry, collection, options) -> 73 | items.remove(options.index)?.remove() 74 | view.model.on 'add', (entry, collection, options) => 75 | [r,g,b] = (floor(255 * entry.get(k)) for k in ['r','g','b']) 76 | style = "background:rgba(#{r},#{g},#{b}, 0.3)" 77 | items.insert options.index, @$li {style}, -> 78 | @cid = entry.cid # mark it to find its model again (on reset) 79 | @$input 80 | type:'button' 81 | class:"remove control" 82 | 'data-cid':entry.cid 83 | value:"✖" 84 | title:"delete" 85 | @$p "#{entry.get 'value'}" 86 | 87 | # patch some backbone internals 88 | 89 | make: -> # do nothing 90 | delegateEvents: -> super if @$el? 91 | undelegateEvents: -> super if @$el? 92 | 93 | setElement: (element, delegate, callback) -> 94 | return unless element? 95 | @el = element 96 | @el.ready => 97 | console.log "ready" 98 | do @undelegateEvents if @$el 99 | @$el = @el.jquery 100 | do @delegateEvents if delegate isnt off 101 | callback?(@$el) 102 | this 103 | 104 | render: (callback) -> 105 | @setElement(@template(this), null, callback) 106 | 107 | # user input handling 108 | 109 | events: 110 | 'click #add': "on_add" 111 | 'click .remove': "on_remove" 112 | 'click #desc': "on_desc" 113 | 'click #asc': "on_asc" 114 | 115 | on_add: EventHandler (ev) -> 116 | @model.add value:"#{random()}", r:random(), g:random(), b:random() 117 | console.log "add", @model.length 118 | 119 | on_remove: EventHandler (ev) -> 120 | if(entry = @model.getByCid($(ev.target).data('cid')))? 121 | @model.remove(entry) 122 | console.log "remove", @model.length 123 | 124 | on_desc: EventHandler (ev) -> 125 | @model.comparator = descending 126 | @model.sort() 127 | 128 | on_asc: EventHandler (ev) -> 129 | @model.comparator = ascending 130 | @model.sort() 131 | 132 | 133 | # initialize 134 | 135 | $(document).ready -> 136 | example = new BackboneExample 137 | model:new Backbone.Collection 138 | window.example = example 139 | example.render ($el) -> 140 | $('body').append($el) 141 | 142 | 143 | console.log 'coffeescript loaded.' 144 | 145 | 146 | -------------------------------------------------------------------------------- /src/test/template.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | { readFile } = require 'fs' 3 | { Template } = require '../dynamictemplate' 4 | streamify = require 'dt-stream' 5 | 6 | module.exports = 7 | 8 | simple: (æ) -> 9 | xml = streamify new Template schema:0, -> 10 | @$tag('test') 11 | check = "no data" 12 | xml.stream.on 'data', (tag) -> 13 | æ.equal "", tag 14 | check = "got data" 15 | xml.stream.on 'end', -> 16 | æ.equal check, "got data" 17 | æ.done() 18 | 19 | 20 | html5: (æ) -> 21 | content = ["a", "b", "c"] 22 | xml = streamify new Template schema:5, doctype:on, -> 23 | @$html -> 24 | @body -> 25 | file = path.join(__dirname,"..","filename") 26 | readFile file, (err, filedata) => 27 | @$div class:'test', (filedata or err) 28 | @$ul -> 29 | content.forEach (data) => 30 | li = @$li({'data-content':data}, data) 31 | @end() 32 | 33 | xml.stream.on 'data', (tag) -> æ.equal results.shift(), tag 34 | xml.stream.on 'end', -> 35 | æ.equal 0, results.length 36 | æ.done() 37 | results = [ 38 | '' 39 | '' 40 | '' 41 | '
' 42 | 'hello world\n' 43 | '
' 44 | '' 55 | '' 56 | '' 57 | ] 58 | 59 | 60 | pretty: (æ) -> 61 | xml = streamify new Template schema:5, pretty:" ", doctype:on, -> 62 | @$html -> 63 | @$head -> 64 | @$title "holla" 65 | @$body -> 66 | @$p "hello world" 67 | # xml.ready æ.done 68 | xml.stream.on 'data', (tag) -> æ.equal results.shift(), tag 69 | xml.stream.on 'end', -> 70 | æ.equal 0, results.length 71 | æ.done() 72 | results = [ 73 | '\n' 74 | '\n' 75 | ' \n' 76 | ' \n' 77 | ' holla\n' 78 | ' \n' 79 | ' \n' 80 | ' \n' 81 | '

\n' 82 | ' hello world\n' 83 | '

\n' 84 | ' \n' 85 | '\n' 86 | ] 87 | 88 | 89 | layout: (æ) -> 90 | 91 | layout = ({title}, callback) -> 92 | new Template schema:'html5', -> 93 | @$html -> 94 | @$head -> 95 | @$title title 96 | @$body -> 97 | @div class:'body', -> 98 | callback?.call(this) 99 | 100 | template = (data) -> 101 | layout data, -> 102 | @$p data.content 103 | @end() 104 | 105 | 106 | xml = streamify template data = 107 | title:'test' 108 | content:'..' 109 | 110 | xml.stream.on 'data', (tag) -> æ.equal results.shift(), tag 111 | xml.stream.on 'end', -> 112 | æ.equal 0, results.length 113 | æ.done() 114 | results = [ 115 | '' 116 | '' 117 | '' 118 | data.title 119 | '' 120 | '' 121 | '' 122 | '
' 123 | '

' 124 | data.content 125 | '

' 126 | '
' 127 | '' 128 | '' 129 | ] 130 | 131 | 132 | 'self closing': (æ) -> 133 | xml = streamify new Template schema:5, pretty:" ", doctype:on, -> 134 | @$html -> 135 | @$body -> 136 | @$div() 137 | @$br() 138 | xml.stream.on 'data', (tag) -> æ.equal results.shift(), tag 139 | xml.stream.on 'end', -> 140 | æ.equal 0, results.length 141 | æ.done() 142 | results = [ 143 | '\n' 144 | '\n' 145 | ' \n' 146 | '
\n' 147 | '
\n' 148 | '
\n' 149 | ' \n' 150 | '\n' 151 | ] 152 | 153 | 154 | 'replace': (æ) -> 155 | replace = null 156 | xml = new Template schema:5, -> 157 | @$html -> 158 | @$body -> 159 | replace = @$span().replace 160 | xml.on 'add', (par, tag) -> console.log tag.toString(), "(#{par})" 161 | 162 | setTimeout -> 163 | replace new Template schema:5, -> 164 | @$div -> 165 | @$p "foo" 166 | @$p "bar" 167 | @ready æ.done 168 | , 5 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Δt](https://s3.amazonaws.com/cloud.ohloh.net/attachments/49947/%CE%94t_med.png)[dynamictemplate](http://dodo.github.com/node-dynamictemplate/) 2 | 3 | 4 | 5 | [dynamictemplate](http://dodo.github.com/node-dynamictemplate/) is yet 6 | another template engine, but this time loaded with full async support 7 | and capable of being changed even after the template was rendered. 8 | 9 | It works in browsers too. 10 | 11 | → [Check out the demo!](http://dodo.github.com/node-dynamictemplate/example/circles.html) 12 | 13 | ## Installation 14 | 15 | ```bash 16 | $ npm install dynamictemplate 17 | ``` 18 | 19 | ## Solutions 20 | 21 | if any of this problems are familiar to you, you should skip the tl;dr and read the documentation: 22 | 23 | * building a real-time user interface 24 | * updating large chunks of DOM 25 | * manipulating nested DOM structures 26 | * working with a designer 27 | * isomorph code 28 | * html streaming 29 | 30 | 31 | ## TL;DR 32 | 33 | Convenient DOM manipulation in template style for real-time user interfaces. 34 | 35 | * async & dynamic → changeable even after template was rendered 36 | * pure javascript with a hugh event based api → modular & extendable 37 | * runs on server and browser side 38 | * different approach than dom: don't get your elements out of the black box. keep only those which you need. 39 | * minimalistic (all the other stuff is hidden in modules :P) 40 | 41 | ## Documentation 42 | 43 | 44 | Writing and maintaining user interfaces can be hard. 45 | 46 | Δt is this new event based way of writing and maintaining user interfaces in javascript. 47 | 48 | DOM has growen old. It's one of the legacies from the last millenium each browser caries with it. 49 | Like nearly every browser API, the DOM has an ,opinionated, ugly interface to work with: 50 | the fastest way to fill a DOM is `innerHTML`, the way to get stuff back once its parsed is by querying it, the convenient way to manipulate it is by changing, creating and removing nodes. so WTF. 51 | These are the reasons why jquery and mootools are still the most used js libraries. but seriously, have you every tried to write an large userinterface with it? 52 | 53 | So let's try something new: 54 | 55 | ```javascript 56 | var Template = require('dynamictemplate').Template; 57 | var tpl = new Template({schema:'html5'}, function () { 58 | this.div(function () { 59 | this.text("hello world"); 60 | this.end(); 61 | }); 62 | }); 63 | ``` 64 | 65 | ```coffeescript 66 | { Template } = require 'dynamictemplate' 67 | tpl = new Template schema:'html5', -> 68 | @div -> 69 | @text "hello world" 70 | @end() 71 | ``` 72 | 73 | That was easy. We created a new template instance and with it a new `
` element with some text and closed it. 74 | 75 | Let's try something more complex: 76 | 77 | ```javascript 78 | function template(view) { 79 | return new Template({schema:5}, function () { 80 | this.$div(function () { 81 | view.on('set title', this.text); 82 | }); 83 | this.$a(function () { 84 | this.text("back"); 85 | view.on('navigate', function (url) { 86 | this.attr('href', url); 87 | }.bind(this)); 88 | }); 89 | }); 90 | } 91 | ``` 92 | 93 | ```coffeescript 94 | template = (view) -> 95 | new Template schema:'5', -> 96 | @$div -> 97 | view.on('set title', @text) 98 | @$a href:'/', -> 99 | @text "back" 100 | view.on('navigate', (url) => @attr(href:url)) 101 | ``` 102 | 103 | Ok. let me explain: we created a div which text changes on every 'set title' event the view object will emit and we created an anchor element which `href` attribute will change on every 'navigate' event. that's it. 104 | note that the div element will be empty at the beginning. 105 | if you play a while with it you might hit some known problems from nodejs: flow control. how convenient that it seems that nearly everybody has writting her own library. **Use your own flow control library!** 106 | if you don't know any, [async](https://github.com/caolan/async#readme) might be a good fit. 107 | 108 | if you already started playing around with it you might found out that nothing is happing. Its because each `this.div` call doesn't produce a div tag but a `new` and a `add` event with the object representation of the div tag as argument. Doesn't sound very useful to you? how about you use one of the many adapters? An Adapter is little modules that listens for these events and act accordingly on its domain. This means if you use dt-jquery or dt-dom it will create a dom element. in the case of dt-stream it will create a nodejs stream instance that emits html strings as data. 109 | 110 | ```javascript 111 | var jqueryify = require('dt-jquery'); 112 | tpl = jqueryify(template(view)); 113 | // or 114 | var domify = require('dt-dom'); 115 | tpl = domify(template(view)); 116 | // or 117 | var streamify = require('dt-stream'); 118 | tpl = streamify(template(view)); 119 | ``` 120 | For more information on the events look at [asyncxml](http://dodo.github.com/node-asyncxml/) which generates them. 121 | 122 | Let's have another example: 123 | 124 | ```javascript 125 | function template(view) { 126 | return new Template({schema:5}, function () { 127 | this.$div({class:'user'}, function () { 128 | var name = this.a({class:'name'}); 129 | var about = this.span({class:'about'}); 130 | view.on('set user', function setUser(user) { 131 | name.text(user.name); 132 | name.attr('href', user.url); 133 | about.text(user.description); 134 | }); 135 | setUser(view.currentUser); 136 | about.end(); 137 | name.end(); 138 | }); 139 | }); 140 | } 141 | ``` 142 | 143 | ```coffeescript 144 | template = (view) -> 145 | new Template schema:5, -> 146 | @$div class:'user', -> 147 | name = @a(class:'name') 148 | about = @span(class:'about') 149 | setUser = (user) -> 150 | name.text(user.name) 151 | name.attr(href:user.url) 152 | about.text(user.description) 153 | view.on('set user', setUser) 154 | setUser(view.currentUser) 155 | about.end() 156 | name.end() 157 | 158 | ``` 159 | 160 | Alright. here is the trick: unlike the DOM where you normally have to query most elements, which feels mostly like grabbing into a black box with spiders and snakes, with Δt you already created the tags, so store them in variables, scopes and/or closures when you need them. 161 | 162 | For more information look at the various examples and plugins supporting Δt: 163 | 164 | ### Plugins 165 | 166 | * [Δt compiler](http://dodo.github.com/node-dt-compiler) - this compiles static HTML (like mockup files from a designer) to template masks. 167 | * [Δt stream adapter](http://dodo.github.com/node-dt-stream) - this lets you use node's beloved Stream to get static HTML from the templates. 168 | * [Δt jquery adapter](http://dodo.github.com/node-dt-jquery) - this lets you insert the template into dom with the help of [jQuery](http://jquery.com/). 169 | * [Δt list](http://dodo.github.com/node-dt-list) - this gives all you need to handle an ordered list of tags. 170 | * [Δt selector](http://dodo.github.com/node-dt-selector) - this gives you specific selected tags without modifing the template. 171 | 172 | 173 | [![Build Status](https://secure.travis-ci.org/dodo/node-dynamictemplate.png)](http://travis-ci.org/dodo/node-dynamictemplate) 174 | 175 | 176 | 177 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/dodo/node-dynamictemplate/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 178 | 179 | -------------------------------------------------------------------------------- /example/quadtree.coffee: -------------------------------------------------------------------------------- 1 | srcdirurl = "https://github.com/dodo/node-dynamictemplate/blob/master/example" 2 | { Template, domify } = window.dynamictemplate 3 | { random, floor, min, max } = Math 4 | running = yes 5 | 6 | svgns = "http://www.w3.org/2000/svg" 7 | 8 | [test0, test1, test2] = ["M5.5,5.5h20v20h-20z","M24.359,18.424l-2.326,1.215c0.708,1.174,1.384,2.281,1.844,3.033l2.043-1.066C25.538,20.822,24.966,19.652,24.359,18.424zM19.143,14.688c0.445,0.84,1.342,2.367,2.274,3.926l2.414-1.261c-0.872-1.769-1.72-3.458-2.087-4.122c-0.896-1.621-1.982-3.108-3.454-5.417c-1.673-2.625-3.462-5.492-4.052-4.947c-1.194,0.384,1.237,4.094,1.876,5.715C16.73,10.147,17.991,12.512,19.143,14.688zM26.457,22.673l-1.961,1.022l1.982,4.598c0,0,0.811,0.684,1.92,0.213c1.104-0.469,0.81-1.706,0.81-1.706L26.457,22.673zM24.35,15.711c0.168,0.339,2.924,5.93,2.924,5.93h1.983v-5.93H24.35zM18.34,15.704h-4.726l-3.424,5.935h11.66C21.559,21.159,18.771,16.479,18.34,15.704zM3.231,21.613l3.437-5.902H2.083v5.93h1.133L3.231,21.613zM15.048,10.145c0-0.93-0.754-1.685-1.685-1.685c-0.661,0-1.231,0.381-1.507,0.936l2.976,1.572C14.97,10.725,15.048,10.444,15.048,10.145zM14.343,12.06l-3.188-1.684L9.62,13.012l3.197,1.689L14.343,12.06zM3.192,26.886l-0.384,1.108v0.299l0.298-0.128l0.725-0.896l2.997-2.354l-3.137-1.651L3.192,26.886zM9.02,14.044l-4.757,8.17l3.23,1.706l4.728-8.186L9.02,14.044z" ,"M23.356,17.485c-0.004,0.007-0.007,0.013-0.01,0.021l0.162,0.005c0.107,0.004,0.218,0.01,0.33,0.016c-0.046-0.004-0.09-0.009-0.136-0.013L23.356,17.485zM15.5,1.249C7.629,1.25,1.25,7.629,1.249,15.5C1.25,23.371,7.629,29.75,15.5,29.751c7.871-0.001,14.25-6.38,14.251-14.251C29.75,7.629,23.371,1.25,15.5,1.249zM3.771,17.093c0.849-0.092,1.833-0.148,2.791-0.156c0.262,0,0.507-0.006,0.717-0.012c0.063,0.213,0.136,0.419,0.219,0.613H7.492c-0.918,0.031-2.047,0.152-3.134,0.335c-0.138,0.023-0.288,0.051-0.441,0.08C3.857,17.67,3.81,17.383,3.771,17.093zM12.196,22.224c-0.1,0.028-0.224,0.07-0.357,0.117c-0.479,0.169-0.665,0.206-1.15,0.206c-0.502,0.015-0.621-0.019-0.921-0.17C9.33,22.171,8.923,21.8,8.651,21.353c-0.453-0.746-1.236-1.275-1.889-1.275c-0.559,0-0.664,0.227-0.261,0.557c0.608,0.496,1.062,0.998,1.248,1.385c0.105,0.215,0.266,0.546,0.358,0.744c0.099,0.206,0.311,0.474,0.511,0.676c0.472,0.441,0.928,0.659,1.608,0.772c0.455,0.06,0.567,0.06,1.105-0.004c0.26-0.03,0.479-0.067,0.675-0.118v0.771c0,1.049-0.008,1.628-0.031,1.945c-1.852-0.576-3.507-1.595-4.848-2.934c-1.576-1.578-2.706-3.592-3.195-5.848c0.952-0.176,2.073-0.32,3.373-0.43l0.208-0.018c0.398,0.925,1.011,1.631,1.876,2.179c0.53,0.337,1.38,0.685,1.808,0.733c0.118,0.02,0.46,0.09,0.76,0.16c0.302,0.066,0.89,0.172,1.309,0.236h0.009c-0.007,0.018-0.014,0.02-0.022,0.02C12.747,21.169,12.418,21.579,12.196,22.224zM13.732,27.207c-0.168-0.025-0.335-0.056-0.5-0.087c0.024-0.286,0.038-0.785,0.054-1.723c0.028-1.767,0.041-1.94,0.156-2.189c0.069-0.15,0.17-0.32,0.226-0.357c0.095-0.078,0.101,0.076,0.101,2.188C13.769,26.143,13.763,26.786,13.732,27.207zM15.5,27.339c-0.148,0-0.296-0.006-0.443-0.012c0.086-0.562,0.104-1.428,0.106-2.871l0.003-1.82l0.197,0.019l0.199,0.02l0.032,2.365c0.017,1.21,0.027,1.878,0.075,2.296C15.613,27.335,15.558,27.339,15.5,27.339zM17.006,27.24c-0.039-0.485-0.037-1.243-0.027-2.553c0.019-1.866,0.019-1.866,0.131-1.769c0.246,0.246,0.305,0.623,0.305,2.373c0,0.928,0.011,1.497,0.082,1.876C17.334,27.196,17.17,27.22,17.006,27.24zM27.089,17.927c-0.155-0.029-0.307-0.057-0.446-0.08c-0.96-0.162-1.953-0.275-2.804-0.32c1.25,0.108,2.327,0.248,3.246,0.418c-0.479,2.289-1.618,4.33-3.214,5.928c-1.402,1.4-3.15,2.448-5.106,3.008c-0.034-0.335-0.058-1.048-0.066-2.212c-0.03-2.167-0.039-2.263-0.17-2.602c-0.181-0.458-0.47-0.811-0.811-1.055c-0.094-0.057-0.181-0.103-0.301-0.14c0.145-0.02,0.282-0.021,0.427-0.057c1.418-0.188,2.168-0.357,2.772-0.584c1.263-0.492,2.129-1.301,2.606-2.468c0.044-0.103,0.088-0.2,0.123-0.279l0.011,0.001c0.032-0.07,0.057-0.118,0.064-0.125c0.02-0.017,0.036-0.085,0.038-0.151c0-0.037,0.017-0.157,0.041-0.317c0.249,0.01,0.58,0.018,0.938,0.02c0.959,0.008,1.945,0.064,2.794,0.156C27.194,17.356,27.148,17.644,27.089,17.927zM25.823,16.87c-0.697-0.049-1.715-0.064-2.311-0.057c0.02-0.103,0.037-0.218,0.059-0.336c0.083-0.454,0.111-0.912,0.113-1.823c0.002-1.413-0.074-1.801-0.534-2.735c-0.188-0.381-0.399-0.705-0.655-0.998c0.225-0.659,0.207-1.68-0.02-2.575c-0.19-0.734-0.258-0.781-0.924-0.64c-0.563,0.12-1.016,0.283-1.598,0.576c-0.274,0.138-0.652,0.354-0.923,0.522c-0.715-0.251-1.451-0.419-2.242-0.508c-0.799-0.092-2.759-0.04-3.454,0.089c-0.681,0.126-1.293,0.28-1.848,0.462c-0.276-0.171-0.678-0.4-0.964-0.547C9.944,8.008,9.491,7.846,8.925,7.727c-0.664-0.144-0.732-0.095-0.922,0.64c-0.235,0.907-0.237,1.945-0.004,2.603c0.026,0.075,0.043,0.129,0.05,0.17c-0.942,1.187-1.25,2.515-1.046,4.367c0.053,0.482,0.136,0.926,0.251,1.333c-0.602-0.004-1.457,0.018-2.074,0.057c-0.454,0.031-0.957,0.076-1.418,0.129c-0.063-0.5-0.101-1.008-0.101-1.524c0-3.273,1.323-6.225,3.468-8.372c2.146-2.144,5.099-3.467,8.371-3.467c3.273,0,6.226,1.323,8.371,3.467c2.145,2.147,3.468,5.099,3.468,8.372c0,0.508-0.036,1.008-0.098,1.499C26.78,16.946,26.276,16.899,25.823,16.87z"] 9 | 10 | 11 | button = (tag, id, value) -> 12 | tag.$input 13 | class:'button' 14 | type:'button' 15 | id:id 16 | value:value 17 | title:id 18 | 19 | createRect = (tag, o) -> 20 | tag.$rect 21 | xmlns:svgns 22 | fill: "rgba(#{o.r},#{o.g},#{o.b},#{o.a})" 23 | stroke:"none" 24 | height:"#{o.h}" 25 | width: "#{o.w}" 26 | x: "#{o.x}" 27 | y: "#{o.y}" 28 | 29 | createBox = (tag, o) -> 30 | tag.$rect 31 | xmlns:svgns 32 | stroke:"rgba(#{o.r},#{o.g},#{o.b},#{o.a})" 33 | 'stroke-width':"0.1" 34 | fill: "none" 35 | height:"#{o.h}" 36 | width: "#{o.w}" 37 | x: "#{o.x}" 38 | y: "#{o.y}" 39 | 40 | createCircle = (tag, o) -> 41 | tag.$circle 42 | xmlns:svgns 43 | fill: "rgba(#{o.r},#{o.g},#{o.b},#{o.a})" 44 | stroke:"none" 45 | cx: "#{o.x}" 46 | cy: "#{o.y}" 47 | r: "#{o.R}" 48 | 49 | createPath = (tag, o) -> 50 | tag.$path 51 | xmlns:svgns 52 | style: "pointer-events:visibileFill" 53 | # transform:"matrix(1,0,0,1,#{o.x},#{o.y})" 54 | # transform:"matrix(#{o.s},0,0,#{o.s},#{o.x},#{o.y})" 55 | fill: "rgba(#{o.r},#{o.g},#{o.b},#{o.a})" 56 | stroke: "none" 57 | d: "#{o.p}" 58 | 59 | 60 | calcRect = ({root, el, x, y, w, h}) -> 61 | p = root.createSVGPoint() 62 | [p.x, p.y] = [x, y] 63 | p = p.matrixTransform root.getCTM().inverse().multiply(el.getScreenCTM()) 64 | # p = p.matrixTransform el.getCTM().multiply(el.getScreenCTM().inverse()) 65 | return p unless w and h 66 | rec = root.createSVGRect() 67 | [rec.x, rec.y, rec.width, rec.height] = [p.x, p.y, w, h] 68 | return rec 69 | 70 | convertScreenPos = (root, x, y) -> 71 | p = root.createSVGPoint() 72 | [p.x, p.y] = [x, y] 73 | p.matrixTransform root.getScreenCTM().inverse() 74 | 75 | buildTree = (tag, path) -> return -> 76 | setTimeout -> # FIXME 77 | el = path._dom 78 | root = el.ownerSVGElement 79 | 80 | $(root).click (ev) -> 81 | p1 = convertScreenPos root, ev.clientX, ev.clientY 82 | p2 = calcRect {root, el, x:p1.x, y:p1.y} 83 | console.log p1.x, p1.y, p2.x, p2.y 84 | 85 | cs = 2 86 | cursor = createRect(tag, x:0, y:0, w:cs, h:cs, r:255, g:0, b:0, a:0.4) 87 | cursor.prev = 'miss' 88 | cursor.ready -> $(root).mousemove (ev) -> 89 | p = convertScreenPos root, ev.clientX, ev.clientY 90 | cursor.attr(x:p.x, y:p.y) 91 | 92 | # rec = calcRect {root, el, x:p.x, y:p.y, w:cs, h:cs} 93 | rec = root.createSVGRect() 94 | [rec.x, rec.y, rec.width, rec.height] = [ev.clientX, ev.clientY, cs, cs] 95 | if root.checkIntersection(el, rec) 96 | if cursor.prev is 'miss' 97 | cursor.attr(fill:"rgba(0,255,0,0.4)") 98 | cursor.prev = 'hit' 99 | else 100 | if cursor.prev is 'hit' 101 | cursor.attr(fill:"rgba(255,0,0,0.4)") 102 | cursor.prev = 'miss' 103 | 104 | [i, _max] = [0, 123] 105 | [_top, _bottom, _left, _right] = [600, 0, 600, 0] 106 | len = el.getTotalLength() 107 | 108 | # bbox = createBox(tag, x:0, y:0, w:0, h:0, r:200, g:10, b:5, a:0.7) 109 | interval = setInterval -> 110 | return unless running 111 | l = len * i / _max 112 | p = el.getPointAtLength(l)#.matrixTransform(el.getCTM()) 113 | # _top = min p.y, _top 114 | # _bottom = max p.y, _bottom 115 | # _left = min p.x, _left 116 | # _right = max p.x, _right 117 | # bbox.attr(x:_left, y:_top, width:_right-_left, height:_bottom-_top) 118 | createCircle(tag, x:p.x, y:p.y, R:0.2, r:200, g:10, b:5, a:0.7) 119 | if i++ > _max 120 | clearInterval(interval) 121 | # bbox.remove() 122 | 123 | jobs = [] 124 | interval = setInterval -> 125 | return unless running and jobs.length 126 | # jobs.splice(floor(jobs.length*random()),1)[0]?() 127 | jobs.shift()?() 128 | , 1 129 | 130 | cut = ({level, top, bottom, left, right}) -> 131 | # b = x:left, y:top, w:(right-left)*2, h:(bottom-top)*2, r:10, b:200, g:5, a:0.3 132 | b = x:left, y:top, w:right-left, h:bottom-top, r:10, b:200, g:5, a:0.3 133 | # b = x:left, y:top, w:1, h:1, r:10, b:200, g:5, a:0.3 134 | box = createBox(tag, b) 135 | start = calcRect {root, el, x:left, y:top} 136 | stop = calcRect {root, el, x:right, y:bottom} 137 | rec = root.createSVGRect() 138 | [rec.x, rec.y] = [start.x, start.y] 139 | rec.width = rec.height = max stop.x-start.x, stop.y-start.y 140 | 141 | # console.log "#{level} x:#{rec.x}\ty:#{rec.y}\tw:#{rec.width}\th:#{rec.height}" 142 | # [rec.x, rec.y, rec.width, rec.height] = [b.x+b.w, b.y+b.h, b.w, b.h] 143 | # [rec.x, rec.y, rec.width, rec.height] = [b.x*2, b.y*2, b.w, b.h] 144 | # console.log root.getCTM() 145 | # console.log "#{level} x:#{b.x}\ty:#{b.y}\tw:#{b.w}\th:#{b.h}\ttop:#{top}\tbot:#{bottom}\tlef:#{left}\trig:#{right}\t#{root.checkIntersection(el, rec) and 'INS' or '---'} #{root.checkEnclosure(el, rec) and 'ENC' or '---'} #{root.getIntersectionList(rec, el).length}" 146 | if ++level < 7 and root.checkIntersection(el, rec) 147 | # [hw, hh] = [b.w*0.5, b.h*0.5] 148 | [hw, hh] = [(right-left)*0.5, (bottom-top)*0.5] 149 | jobs.push -> cut {level, top, bottom:bottom-hh, left, right:right-hw} 150 | jobs.push -> cut {level, top, bottom:bottom-hh, left:left+hw, right} 151 | jobs.push -> cut {level, top:top+hh, bottom, left, right:right-hw} 152 | jobs.push -> cut {level, top:top+hh, bottom, left:left+hw, right} 153 | # else jobs.push -> clearInterval(interval) 154 | # [_w, _h] = [_right-_left, _bottom-_top] 155 | # _w = _h = max _w, _h 156 | # _bottom = _top + _h 157 | # _right = _left + _w 158 | 159 | bbox = el.getBBox() 160 | # cut {level:0, top:bbox.y, bottom:bbox.y+bbox.height, left:bbox.x, right:bbox.x+bbox.width} 161 | [_top, _bottom, _left, _right] = [bbox.y, bbox.y+bbox.height, bbox.x, bbox.x+bbox.width] 162 | 163 | console.log "x:#{_left}\ty:#{_top}\tw:#{_right-_left}\th:#{_bottom-_top}" 164 | cut {level:0, top:_top, bottom:_bottom, left:_left, right:_right} 165 | , 5 166 | , 100 167 | 168 | 169 | svg = domify new Template schema:5, -> 170 | @$div class:'controls', -> 171 | button this, "start", "▸" 172 | button this, "stop", "■" 173 | @$a href:"#{srcdirurl}/quadtree.coffee", "Source Code" 174 | @$div class:'canvas', -> 175 | @add new Template schema:'svg', -> 176 | @$svg { 177 | xmlns:svgns 178 | version:"1.1" 179 | height:"600px" 180 | width:"600px" 181 | preserveAspectRatio:"none" 182 | viewBox:"0 0 31 31" 183 | }, -> 184 | path = createPath(this, 185 | p:test2, x:0, y:0, r:0, b:0, g:0, a:1) 186 | @ready(buildTree(this, path)) 187 | 188 | 189 | # initialize 190 | 191 | svg.ready -> 192 | for el in svg.dom 193 | $('body').append el 194 | 195 | $('#start').live 'click', -> 196 | console.log "animation paused." 197 | running = yes 198 | 199 | $('#stop').live 'click', -> 200 | console.log "animation resumed." 201 | running = no 202 | 203 | 204 | console.log 'coffeescript loaded.' 205 | 206 | --------------------------------------------------------------------------------