').render hello: "World!"
19 | done()
20 |
21 | it "should work even if the $ is not in the global namespace", (done) ->
22 | require ['dist/transparency'], (t) ->
23 | $.noConflict();
24 | jQuery.fn.render = t.jQueryPlugin
25 | jQuery('
60 | """
61 |
62 | template.render data
63 | expect(template).toBeEqual expected
64 |
65 | it "should match model keys to template by element id, class, name attribute and data-bind attribute", ->
66 | template = $ """
67 |
114 | """
115 |
116 | template.find('.comments').render data
117 | expect(template).toBeEqual(expected)
118 |
119 | it "should match table rows to the number of model objects", ->
120 | template = $ """
121 |
");
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 = $("
";
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 | #
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 | # //