Comparing version 0.0.2 to 0.2.0
@@ -1,7 +0,1 @@ | ||
module.exports = { | ||
Expression: Expression | ||
, ElseExpression: ElseExpression | ||
, ContextMeta: ContextMeta | ||
, Context: Context | ||
}; | ||
@@ -22,2 +16,13 @@ //// Example framework-specific classes //// | ||
// Expression::truthy(context) | ||
if (typeof require === 'function') { | ||
var serializeObject = require('serialize-object'); | ||
} | ||
module.exports = { | ||
Expression: Expression, | ||
ElseExpression: ElseExpression, | ||
Context: Context | ||
}; | ||
function Expression(source) { | ||
@@ -30,3 +35,6 @@ this.source = source; | ||
Expression.prototype.get = function(context) { | ||
return (this.source == null) ? context.data : context._get(this.source); | ||
return ((this.source == null) | ||
? context.data | ||
: context._get(this.source) | ||
); | ||
}; | ||
@@ -36,2 +44,7 @@ Expression.prototype.truthy = function(context) { | ||
}; | ||
Expression.prototype.module = 'expressions'; | ||
Expression.prototype.type = 'Expression'; | ||
Expression.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.source); | ||
}; | ||
@@ -43,8 +56,6 @@ function ElseExpression() {} | ||
}; | ||
ElseExpression.prototype.type = 'ElseExpression'; | ||
function templateTruthy(value) { | ||
if (Array.isArray(value)) { | ||
return value.length > 0; | ||
} | ||
return value != null && value !== false && value !== ''; | ||
return (Array.isArray(value)) ? value.length > 0 : !!value; | ||
} | ||
@@ -59,4 +70,4 @@ | ||
// The required interface methods are: | ||
// Context::onAdd(binding) | ||
// Context::onRemove(binding) | ||
// Context::addBinding(binding) | ||
// Context::removeBinding(binding) | ||
// Context::child(expression) | ||
@@ -69,7 +80,8 @@ // Context::eachChild(index) | ||
} | ||
Context.prototype.onAdd = function(binding) { | ||
this.meta.onAdd(binding); | ||
Context.prototype = new Expression(); | ||
Context.prototype.addBinding = function(binding) { | ||
this.meta.addBinding(binding); | ||
}; | ||
Context.prototype.onRemove = function(binding) { | ||
this.meta.onRemove(binding); | ||
Context.prototype.removeBinding = function(binding) { | ||
this.meta.removeBinding(binding); | ||
}; | ||
@@ -89,5 +101,1 @@ Context.prototype.child = function(expression) { | ||
}; | ||
function ContextMeta(options) { | ||
this.onAdd = options.onAdd || noop; | ||
this.onRemove = options.onRemove || noop; | ||
} |
482
index.js
@@ -0,3 +1,7 @@ | ||
if (typeof require === 'function') { | ||
var serializeObject = require('serialize-object'); | ||
} | ||
// UPDATE_PROPERTIES map HTML attribute names to an Element DOM property that | ||
// should be used for setting on bindings updates instead of setAttribute. | ||
// should be used for setting on bindings updates instead of s'test'Attribute. | ||
// | ||
@@ -66,2 +70,3 @@ // https://github.com/jquery/jquery/blob/1.x-master/src/attributes/prop.js | ||
, Template: Template | ||
, Doctype: Doctype | ||
, Text: Text | ||
@@ -78,3 +83,2 @@ , DynamicText: DynamicText | ||
, DynamicAttribute: DynamicAttribute | ||
, AttributesMap: AttributesMap | ||
@@ -86,4 +90,2 @@ // Binding Classes | ||
, RangeBinding: RangeBinding | ||
, replaceBindings: replaceBindings | ||
}; | ||
@@ -105,6 +107,49 @@ | ||
}; | ||
Template.prototype.attachTo = function(parent, node, context) { | ||
return attachContent(parent, node, this.content, context); | ||
}; | ||
Template.prototype.stringify = function(value) { | ||
return (value == null) ? '' : value + ''; | ||
}; | ||
Template.prototype.module = 'templates'; | ||
Template.prototype.type = 'Template'; | ||
Template.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.content); | ||
}; | ||
function Doctype(name, publicId, systemId) { | ||
this.name = name; | ||
this.publicId = publicId; | ||
this.systemId = systemId; | ||
} | ||
Doctype.prototype = new Template(); | ||
Doctype.prototype.get = function() { | ||
var publicText = (this.publicId) ? | ||
' PUBLIC "' + this.publicId + '"' : | ||
''; | ||
var systemText = (this.systemId) ? | ||
(this.publicId) ? | ||
' "' + this.systemId + '"' : | ||
' SYSTEM "' + this.systemId + '"' : | ||
''; | ||
return '<!DOCTYPE ' + this.name + publicText + systemText + '>'; | ||
}; | ||
Doctype.prototype.appendTo = function() { | ||
// Doctype could be created via: | ||
// document.implementation.createDocumentType(this.name, this.publicId, this.systemId) | ||
// However, it does not appear possible or useful to append it to the | ||
// document fragment. Therefore, just don't render it in the browser | ||
}; | ||
Doctype.prototype.attachTo = function(parent, node) { | ||
if (!node || node.nodeType !== 10) { | ||
throw attachError(parent, node); | ||
} | ||
return node.nextSibling; | ||
}; | ||
Doctype.prototype.type = 'Doctype'; | ||
Doctype.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.name, this.publicId, this.systemId); | ||
}; | ||
function Text(data) { | ||
@@ -122,2 +167,9 @@ this.data = data; | ||
}; | ||
Text.prototype.attachTo = function(parent, node) { | ||
return attachText(parent, node, this.data, this); | ||
}; | ||
Text.prototype.type = 'Text'; | ||
Text.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.data); | ||
}; | ||
@@ -148,8 +200,51 @@ function DynamicText(expression) { | ||
parent.appendChild(node); | ||
context.onAdd(new NodeBinding(this, context, node)); | ||
addNodeBinding(this, context, node); | ||
}; | ||
DynamicText.prototype.attachTo = function(parent, node, context) { | ||
var value = this.expression.get(context); | ||
if (value instanceof Template) { | ||
return value.attachTo(parent, node, context); | ||
} | ||
var data = this.stringify(value); | ||
return attachText(parent, node, data, this, context); | ||
}; | ||
DynamicText.prototype.update = function(context, binding) { | ||
binding.node.data = this.stringify(this.expression.get(context)); | ||
}; | ||
DynamicText.prototype.type = 'DynamicText'; | ||
DynamicText.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.expression); | ||
}; | ||
function attachText(parent, node, data, template, context) { | ||
if (!node) { | ||
var newNode = document.createTextNode(data); | ||
parent.appendChild(newNode); | ||
addNodeBinding(template, context, newNode); | ||
return; | ||
} | ||
if (node.nodeType === 3) { | ||
// Proceed if nodes already match | ||
if (node.data === data) { | ||
addNodeBinding(template, context, node); | ||
return node.nextSibling; | ||
} | ||
// Split adjacent text nodes that would have been merged together in HTML | ||
var nextNode = splitData(node, data.length); | ||
if (node.data !== data) { | ||
throw attachError(parent, node); | ||
} | ||
addNodeBinding(template, context, node); | ||
return nextNode; | ||
} | ||
// An empty text node might not be created at the end of some text | ||
if (data === '') { | ||
var newNode = document.createTextNode(''); | ||
parent.insertBefore(newNode, node || null); | ||
addNodeBinding(template, context, newNode); | ||
return node; | ||
} | ||
throw attachError(parent, node); | ||
} | ||
function Comment(data) { | ||
@@ -166,2 +261,9 @@ this.data = data; | ||
}; | ||
Comment.prototype.attachTo = function(parent, node) { | ||
return attachComment(parent, node, this.data); | ||
}; | ||
Comment.prototype.type = 'Comment'; | ||
Comment.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.data); | ||
} | ||
@@ -174,10 +276,17 @@ function DynamicComment(expression) { | ||
var value = getUnescapedValue(this.expression, context); | ||
return '<!--' + this.stringify(value) + '-->'; | ||
var data = this.stringify(value); | ||
return '<!--' + data + '-->'; | ||
}; | ||
DynamicComment.prototype.appendTo = function(parent, context) { | ||
var value = getUnescapedValue(this.expression, context); | ||
var node = document.createComment(this.stringify(value)); | ||
var data = this.stringify(value); | ||
var node = document.createComment(data); | ||
parent.appendChild(node); | ||
context.onAdd(new NodeBinding(this, context, node)); | ||
addNodeBinding(this, context, node); | ||
}; | ||
DynamicComment.prototype.attachTo = function(parent, node, context) { | ||
var value = getUnescapedValue(this.expression, context); | ||
var data = this.stringify(value); | ||
return attachComment(parent, node, data, this, context); | ||
}; | ||
DynamicComment.prototype.update = function(context, binding) { | ||
@@ -187,3 +296,29 @@ var value = getUnescapedValue(this.expression, context); | ||
}; | ||
DynamicComment.prototype.type = 'DynamicComment'; | ||
DynamicComment.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.expression); | ||
} | ||
function attachComment(parent, node, data, template, context) { | ||
// Sometimes IE fails to create Comment nodes from HTML or innerHTML. | ||
// This is an issue inside of <select> elements, for example. | ||
if (!node || node.nodeType !== 8) { | ||
var newNode = document.createComment(data); | ||
parent.insertBefore(newNode, node || null); | ||
addNodeBinding(template, context, newNode); | ||
return node; | ||
} | ||
// Proceed if nodes already match | ||
if (node.data === data) { | ||
addNodeBinding(template, context, node); | ||
return node.nextSibling; | ||
} | ||
throw attachError(parent, node); | ||
} | ||
function addNodeBinding(template, context, node) { | ||
if (!context) return; | ||
context.addBinding(new NodeBinding(template, context, node)); | ||
} | ||
function Attribute(data) { | ||
@@ -195,18 +330,25 @@ this.data = data; | ||
}; | ||
Attribute.prototype.module = Template.prototype.module; | ||
Attribute.prototype.type = 'Attribute'; | ||
Attribute.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.data); | ||
}; | ||
function DynamicAttribute(template) { | ||
// In attributes, template may be an instance of Template or Expression | ||
this.template = template; | ||
function DynamicAttribute(expression) { | ||
// In attributes, expression may be an instance of Template or Expression | ||
this.expression = expression; | ||
} | ||
DynamicAttribute.prototype = new Attribute(); | ||
DynamicAttribute.prototype.get = function(context) { | ||
return getUnescapedValue(this.template, context); | ||
return getUnescapedValue(this.expression, context); | ||
}; | ||
DynamicAttribute.prototype.getBound = function(context, element, name) { | ||
context.onAdd(new AttributeBinding(this, context, element, name)); | ||
return getUnescapedValue(this.template, context); | ||
context.addBinding(new AttributeBinding(this, context, element, name)); | ||
return getUnescapedValue(this.expression, context); | ||
}; | ||
DynamicAttribute.prototype.update = function(context, binding) { | ||
var value = getUnescapedValue(this.template, context); | ||
var value = getUnescapedValue(this.expression, context); | ||
var propertyName = UPDATE_PROPERTIES[binding.name]; | ||
if (propertyName) { | ||
if (value === void 0) value = null; | ||
binding.element[propertyName] = value; | ||
@@ -221,2 +363,6 @@ } else if (value === false || value == null) { | ||
}; | ||
DynamicAttribute.prototype.type = 'DynamicAttribute'; | ||
DynamicAttribute.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.expression); | ||
}; | ||
@@ -232,15 +378,19 @@ function getUnescapedValue(expression, context) { | ||
function AttributesMap(object) { | ||
if (object) mergeInto(object, this); | ||
} | ||
function Element(tag, attributes, content) { | ||
this.tag = tag; | ||
function Element(tagName, attributes, content, hooks, selfClosing, notClosed) { | ||
this.tagName = tagName; | ||
this.attributes = attributes; | ||
this.content = content; | ||
this.isVoid = VOID_ELEMENTS[tag.toLowerCase()]; | ||
this.hooks = hooks; | ||
this.selfClosing = selfClosing; | ||
this.notClosed = notClosed; | ||
var lowerTagName = tagName.toLowerCase(); | ||
var isVoid = VOID_ELEMENTS[lowerTagName]; | ||
this.startClose = (selfClosing) ? ' />' : '>'; | ||
this.endTag = (notClosed || isVoid) ? '' : '</' + tagName + '>'; | ||
this.unescapedContent = (lowerTagName === 'script' || lowerTagName === 'style'); | ||
} | ||
Element.prototype = new Template(); | ||
Element.prototype.get = function(context) { | ||
var tagItems = [this.tag]; | ||
var tagItems = [this.tagName]; | ||
for (var key in this.attributes) { | ||
@@ -254,12 +404,12 @@ var value = this.attributes[key].get(context); | ||
} | ||
var startTag = '<' + tagItems.join(' ') + '>'; | ||
var endTag = '</' + this.tag + '>'; | ||
var startTag = '<' + tagItems.join(' ') + this.startClose; | ||
if (this.content) { | ||
var inner = contentHtml(this.content, context); | ||
return startTag + inner + endTag; | ||
var inner = contentHtml(this.content, context, this.unescapedContent); | ||
return startTag + inner + this.endTag; | ||
} | ||
return (this.isVoid) ? startTag : startTag + endTag; | ||
return startTag + this.endTag; | ||
}; | ||
Element.prototype.appendTo = function(parent, context) { | ||
var element = document.createElement(this.tag); | ||
var element = document.createElement(this.tagName); | ||
emitHooks(this.hooks, context, element); | ||
for (var key in this.attributes) { | ||
@@ -269,2 +419,3 @@ var value = this.attributes[key].getBound(context, element, key); | ||
if (propertyName) { | ||
if (value === void 0) value = null; | ||
element[propertyName] = value; | ||
@@ -280,3 +431,45 @@ } else if (value === true) { | ||
}; | ||
Element.prototype.attachTo = function(parent, node, context) { | ||
if ( | ||
!node || | ||
node.nodeType !== 1 || | ||
(node.tagName).toLowerCase() !== (this.tagName).toLowerCase() | ||
) { | ||
throw attachError(parent, node); | ||
} | ||
emitHooks(this.hooks, context, node); | ||
for (var key in this.attributes) { | ||
// Get each attribute to create bindings | ||
this.attributes[key].getBound(context, node, key); | ||
// TODO: Ideally, this would also check that the node's current attributes | ||
// are equivalent, but there are some tricky edge cases | ||
} | ||
if (this.content) attachContent(node, node.firstChild, this.content, context); | ||
return node.nextSibling; | ||
}; | ||
Element.prototype.type = 'Element'; | ||
Element.prototype.serialize = function() { | ||
return serializeObject.instance( | ||
this | ||
, this.tagName | ||
, this.attributes | ||
, this.content | ||
, this.hooks | ||
, this.selfClosing | ||
, this.notClosed | ||
); | ||
}; | ||
function getAttributeValue(element, name) { | ||
var propertyName = UPDATE_PROPERTIES[name]; | ||
return (propertyName) ? element[propertyName] : element.getAttribute(name); | ||
} | ||
function emitHooks(hooks, context, value) { | ||
if (!hooks) return; | ||
for (var i = 0, len = hooks.length; i < len; i++) { | ||
hooks[i].emit(context, value); | ||
} | ||
} | ||
function Block(expression, content) { | ||
@@ -301,2 +494,12 @@ this.expression = expression; | ||
}; | ||
Block.prototype.attachTo = function(parent, node, context) { | ||
var blockContext = context.child(this.expression); | ||
var start = document.createComment(this.expression); | ||
var end = document.createComment(this.ending); | ||
parent.insertBefore(start, node || null); | ||
node = attachContent(parent, node, this.content, blockContext); | ||
parent.insertBefore(end, node || null); | ||
updateRange(context, null, this, start, end); | ||
return node; | ||
}; | ||
Block.prototype.update = function(context, binding) { | ||
@@ -309,6 +512,10 @@ // Get start and end in advance, since binding is mutated in getFragment | ||
}; | ||
Block.prototype.type = 'Block'; | ||
Block.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.expression, this.content); | ||
}; | ||
function ConditionalBlock(expressions, contents) { | ||
this.expressions = expressions; | ||
this.beginning = expressions[0]; | ||
this.beginning = expressions.join('; '); | ||
this.ending = '/' + this.beginning; | ||
@@ -323,3 +530,4 @@ this.contents = contents; | ||
if (expression.truthy(context)) { | ||
html += contentHtml(this.contents[i], context.child(expression), unescaped); | ||
var blockContext = context.child(expression); | ||
html += contentHtml(this.contents[i], blockContext, unescaped); | ||
break; | ||
@@ -337,3 +545,4 @@ } | ||
if (expression.truthy(context)) { | ||
appendContent(parent, this.contents[i], context.child(expression)); | ||
var blockContext = context.child(expression); | ||
appendContent(parent, this.contents[i], blockContext); | ||
break; | ||
@@ -345,2 +554,22 @@ } | ||
}; | ||
ConditionalBlock.prototype.attachTo = function(parent, node, context) { | ||
var start = document.createComment(this.beginning); | ||
var end = document.createComment(this.ending); | ||
parent.insertBefore(start, node || null); | ||
for (var i = 0, len = this.expressions.length; i < len; i++) { | ||
var expression = this.expressions[i]; | ||
if (expression.truthy(context)) { | ||
var blockContext = context.child(expression); | ||
node = attachContent(parent, node, this.contents[i], blockContext); | ||
break; | ||
} | ||
} | ||
parent.insertBefore(end, node || null); | ||
updateRange(context, null, this, start, end); | ||
return node; | ||
}; | ||
ConditionalBlock.prototype.type = 'ConditionalBlock'; | ||
ConditionalBlock.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.expressions, this.contents); | ||
}; | ||
@@ -399,2 +628,33 @@ function EachBlock(expression, content, elseContent) { | ||
}; | ||
EachBlock.prototype.attachTo = function(parent, node, context) { | ||
var items = this.expression.get(context); | ||
var listContext = context.child(this.expression); | ||
var start = document.createComment(this.expression); | ||
var end = document.createComment(this.ending); | ||
parent.insertBefore(start, node || null); | ||
if (items && items.length) { | ||
for (var i = 0, len = items.length; i < len; i++) { | ||
var itemContext = listContext.eachChild(i); | ||
node = this.attachItemTo(parent, node, itemContext); | ||
} | ||
} else if (this.elseContent) { | ||
node = attachContent(parent, node, this.elseContent, listContext); | ||
} | ||
parent.insertBefore(end, node || null); | ||
updateRange(context, null, this, start, end); | ||
return node; | ||
}; | ||
EachBlock.prototype.attachItemTo = function(parent, node, context) { | ||
var start, end; | ||
var nextNode = attachContent(parent, node, this.content, context); | ||
if (nextNode === node) { | ||
start = end = document.createComment('empty'); | ||
parent.insertBefore(start, node || null); | ||
} else { | ||
start = node; | ||
end = (nextNode && nextNode.previousSibling) || parent.lastChild; | ||
} | ||
updateRange(context, null, this, start, end, true); | ||
return nextNode; | ||
}; | ||
EachBlock.prototype.update = function(context, binding) { | ||
@@ -420,3 +680,3 @@ var start = binding.start; | ||
} | ||
binding.start.parentNode.insertBefore(fragment, node); | ||
binding.start.parentNode.insertBefore(fragment, node || null); | ||
}; | ||
@@ -450,4 +710,8 @@ EachBlock.prototype.remove = function(context, binding, index, howMany) { | ||
node = indexStartNode(binding.start, to, binding.end); | ||
binding.start.parentNode.insertBefore(fragment, node); | ||
binding.start.parentNode.insertBefore(fragment, node || null); | ||
}; | ||
EachBlock.prototype.type = 'EachBlock'; | ||
EachBlock.prototype.serialize = function() { | ||
return serializeObject.instance(this, this.expression, this.content, this.elseContent); | ||
}; | ||
@@ -472,3 +736,3 @@ function indexStartNode(node, index, endBound) { | ||
} else { | ||
context.onAdd(new RangeBinding(template, context, start, end, isItem)); | ||
context.addBinding(new RangeBinding(template, context, start, end, isItem)); | ||
} | ||
@@ -482,2 +746,8 @@ } | ||
} | ||
function attachContent(parent, node, content, context) { | ||
for (var i = 0, len = content.length; i < len; i++) { | ||
node = content[i].attachTo(parent, node, context); | ||
} | ||
return node; | ||
} | ||
function contentHtml(content, context, unescaped) { | ||
@@ -492,2 +762,5 @@ var html = ''; | ||
var parent = start.parentNode; | ||
// This shouldn't happen if bindings are cleaned up properly, but check | ||
// in case they aren't | ||
if (!parent) return; | ||
if (start === end) { | ||
@@ -509,13 +782,13 @@ parent.replaceChild(fragment, start); | ||
// This also works if nextNode is null, by doing an append | ||
parent.insertBefore(fragment, nextNode); | ||
parent.insertBefore(fragment, nextNode || null); | ||
} | ||
function emitRemoved(context, node, ignore) { | ||
var binding = node.$bindNode; | ||
if (binding && binding !== ignore) context.onRemove(binding); | ||
if (binding && binding !== ignore) context.removeBinding(binding); | ||
binding = node.$bindStart; | ||
if (binding && binding !== ignore) context.onRemove(binding); | ||
if (binding && binding !== ignore) context.removeBinding(binding); | ||
var attributes = node.$bindAttributes; | ||
if (attributes) { | ||
for (var key in attributes) { | ||
context.onRemove(attributes[key]); | ||
context.removeBinding(attributes[key]); | ||
} | ||
@@ -528,3 +801,15 @@ } | ||
function Binding() {} | ||
function attachError(parent, node) { | ||
if (typeof console !== 'undefined') { | ||
console.error('Attach failed for', node, 'within', parent); | ||
} | ||
return new Error('Attaching bindings failed, because HTML structure ' + | ||
'does not match client rendering.' | ||
); | ||
} | ||
function Binding() { | ||
this.meta = null; | ||
} | ||
Binding.prototype.type = 'Binding'; | ||
Binding.prototype.update = function() { | ||
@@ -538,5 +823,7 @@ this.template.update(this.context, this); | ||
this.node = node; | ||
this.meta = null; | ||
setNodeProperty(node, '$bindNode', this); | ||
} | ||
NodeBinding.prototype = new Binding(); | ||
NodeBinding.prototype.type = 'NodeBinding'; | ||
@@ -549,2 +836,3 @@ function AttributeBindingsMap() {} | ||
this.name = name; | ||
this.meta = null; | ||
var map = element.$bindAttributes || | ||
@@ -555,2 +843,3 @@ (element.$bindAttributes = new AttributeBindingsMap()); | ||
AttributeBinding.prototype = new Binding(); | ||
AttributeBinding.prototype.type = 'AttributeBinding'; | ||
@@ -563,2 +852,3 @@ function RangeBinding(template, context, start, end, isItem) { | ||
this.isItem = isItem; | ||
this.meta = null; | ||
setNodeProperty(start, '$bindStart', this); | ||
@@ -568,2 +858,3 @@ setNodeProperty(end, '$bindEnd', this); | ||
RangeBinding.prototype = new Binding(); | ||
RangeBinding.prototype.type = 'RangeBinding'; | ||
RangeBinding.prototype.insert = function(index, howMany) { | ||
@@ -580,101 +871,2 @@ this.template.insert(this.context, this, index, howMany); | ||
//// HTML page initialization //// | ||
function replaceBindings(fragment, mirror) { | ||
var node = fragment.firstChild; | ||
var mirrorNode = mirror.firstChild; | ||
var nextMirrorNode; | ||
do { | ||
nextMirrorNode = mirrorNode && mirrorNode.nextSibling; | ||
// Split or create empty TextNodes as needed | ||
if (node.nodeType === 3) { | ||
if (mirrorNode && mirrorNode.nodeType === 3) { | ||
if (node.data !== mirrorNode.data) { | ||
nextMirrorNode = splitData(mirrorNode, node.data.length); | ||
} | ||
} else { | ||
nextMirrorNode = mirrorNode; | ||
mirrorNode = document.createTextNode(''); | ||
// Also works if nextMirrorNode is null | ||
mirror.insertBefore(mirrorNode, nextMirrorNode); | ||
} | ||
// Create missing CommentNodes. Comments are used as DOM location markers | ||
// when rendering bindings, but not when rendering HTML. In addition, old | ||
// versions of IE fail to create some CommentNodes when parsing HTML | ||
} else if (node.nodeType === 8) { | ||
if ( | ||
!mirrorNode || | ||
(mirrorNode.nodeType !== 8) || | ||
(node.data !== mirrorNode.data) | ||
) { | ||
nextMirrorNode = mirrorNode; | ||
mirrorNode = node.cloneNode(false); | ||
mirror.insertBefore(mirrorNode, nextMirrorNode); | ||
} | ||
} | ||
// Verify that the nodes are equivalent | ||
if (mismatchedNodes(node, mirrorNode)) { | ||
throw new Error('Attaching bindings failed, because HTML structure ' + | ||
'does not match client rendering' | ||
); | ||
} | ||
// Move bindings on the fragment to the corresponding node on the mirror | ||
replaceNodeBindings(node, mirrorNode); | ||
// Recursively traverse within Elements | ||
if (node.nodeType === 1 && node.hasChildNodes()) { | ||
replaceBindings(node, mirrorNode); | ||
} | ||
mirrorNode = nextMirrorNode; | ||
node = node.nextSibling; | ||
} while (node); | ||
} | ||
function mismatchedNodes(node, mirrorNode) { | ||
// Check that nodes are of matching types | ||
if (!node || !mirrorNode) return true; | ||
var type = node.nodeType; | ||
if (type !== mirrorNode.nodeType) return true; | ||
// Check that elements are of the same element type | ||
if (type === 1) { | ||
if (node.tagName !== mirrorNode.tagName) return true; | ||
// Check that TextNodes and CommentNodes have the same content | ||
} else if (type === 3 || type === 8) { | ||
if (node.data !== mirrorNode.data) return true; | ||
} | ||
} | ||
function replaceNodeBindings(node, mirrorNode) { | ||
var binding = node.$bindNode; | ||
if (binding) { | ||
binding.node = mirrorNode; | ||
setNodeProperty(node, '$bindNode', binding); | ||
} | ||
binding = node.$bindStart; | ||
if (binding) { | ||
binding.start = mirrorNode; | ||
setNodeProperty(mirrorNode, '$bindStart', binding); | ||
} | ||
binding = node.$bindEnd; | ||
if (binding) { | ||
binding.end = mirrorNode; | ||
setNodeProperty(mirrorNode, '$bindEnd', binding); | ||
} | ||
var attributes = node.$bindAttributes; | ||
if (attributes) { | ||
for (var key in attributes) { | ||
attributes[key].element = mirrorNode; | ||
} | ||
mirrorNode.$bindAttributes = attributes; | ||
} | ||
} | ||
//// Utility functions //// | ||
@@ -705,2 +897,8 @@ | ||
// General notes: | ||
// | ||
// In all cases, Node.insertBefore should have `|| null` after its second | ||
// argument. IE works correctly when the argument is ommitted or equal | ||
// to null, but it throws and error if it is equal to undefined. | ||
if (!Array.isArray) { | ||
@@ -717,3 +915,3 @@ Array.isArray = function(value) { | ||
node.deleteData(index, node.length - index); | ||
node.parentNode.insertBefore(newNode, node.nextSibling); | ||
node.parentNode.insertBefore(newNode, node.nextSibling || null); | ||
return newNode; | ||
@@ -766,3 +964,3 @@ } | ||
proxyNode.$bindProxy = node; | ||
node.parentNode.insertBefore(proxyNode, node.nextSibling); | ||
node.parentNode.insertBefore(proxyNode, node.nextSibling || null); | ||
} | ||
@@ -774,3 +972,3 @@ } else { | ||
proxyNode.$bindProxy = node; | ||
node.parentNode.insertBefore(proxyNode, node); | ||
node.parentNode.insertBefore(proxyNode, node || null); | ||
} | ||
@@ -777,0 +975,0 @@ } |
@@ -9,16 +9,15 @@ { | ||
}, | ||
"version": "0.0.2", | ||
"version": "0.2.0", | ||
"main": "./lib/index.js", | ||
"scripts": { | ||
"test": "grunt test" | ||
"test": "mocha --recursive test --bail --colors --reporter spec --debug" | ||
}, | ||
"dependencies": {}, | ||
"dependencies": { | ||
"serialize-object": "~0.0.2" | ||
}, | ||
"devDependencies": { | ||
"grunt": "~0.4.1", | ||
"mocha": "~1.9.0", | ||
"expect.js": "~0.2.0", | ||
"grunt-simple-mocha": "~0.4.0", | ||
"grunt-contrib-jshint": "~0.4.3" | ||
"expect.js": "~0.2.0" | ||
}, | ||
"optionalDependencies": {} | ||
} |
@@ -66,7 +66,7 @@ describe('Static rendering', function() { | ||
test({ | ||
template: new saddle.Element('div', new saddle.AttributesMap({ | ||
template: new saddle.Element('div', { | ||
id: new saddle.Attribute('page') | ||
, 'data-x': new saddle.Attribute('24') | ||
, 'class': new saddle.Attribute('content fit') | ||
})) | ||
}) | ||
, html: '<div id="page" data-x="24" class="content fit"></div>' | ||
@@ -85,5 +85,5 @@ , fragment: function(fragment) { | ||
test({ | ||
template: new saddle.Element('input', new saddle.AttributesMap({ | ||
template: new saddle.Element('input', { | ||
autofocus: new saddle.Attribute(true) | ||
})) | ||
}) | ||
, html: '<input autofocus>' | ||
@@ -100,5 +100,5 @@ , fragment: function(fragment) { | ||
test({ | ||
template: new saddle.Element('input', new saddle.AttributesMap({ | ||
template: new saddle.Element('input', { | ||
autofocus: new saddle.Attribute(false) | ||
})) | ||
}) | ||
, html: '<input>' | ||
@@ -206,5 +206,5 @@ , fragment: function(fragment) { | ||
test({ | ||
template: new saddle.Element('input', new saddle.AttributesMap({ | ||
template: new saddle.Element('input', { | ||
value: new saddle.Attribute('hello') | ||
})) | ||
}) | ||
, html: '<input value="hello">' | ||
@@ -220,6 +220,6 @@ , fragment: function(fragment) { | ||
test({ | ||
template: new saddle.Element('input', new saddle.AttributesMap({ | ||
template: new saddle.Element('input', { | ||
type: new saddle.Attribute('radio') | ||
, checked: new saddle.Attribute(true) | ||
})) | ||
}) | ||
, html: '<input type="radio" checked>' | ||
@@ -234,6 +234,6 @@ , fragment: function(fragment) { | ||
test({ | ||
template: new saddle.Element('input', new saddle.AttributesMap({ | ||
template: new saddle.Element('input', { | ||
type: new saddle.Attribute('radio') | ||
, checked: new saddle.Attribute(false) | ||
})) | ||
}) | ||
, html: '<input type="radio">' | ||
@@ -252,3 +252,3 @@ , fragment: function(fragment) { | ||
test({ | ||
template: new saddle.Element('div', new saddle.AttributesMap({ | ||
template: new saddle.Element('div', { | ||
'class': new saddle.DynamicAttribute(new saddle.Template([ | ||
@@ -262,3 +262,3 @@ new saddle.Text('dropdown') | ||
])) | ||
})) | ||
}) | ||
, html: '<div class="dropdown show"></div>' | ||
@@ -275,28 +275,83 @@ , fragment: function(fragment) { | ||
describe('replaceBindings', function() { | ||
describe('attachTo', function() { | ||
var fixture = document.getElementById('fixture'); | ||
after(function() { | ||
var fixture = document.getElementById('fixture'); | ||
fixture.innerHTML = ''; | ||
removeChildren(fixture); | ||
}); | ||
function renderAndReplace(template) { | ||
var fixture = document.getElementById('fixture'); | ||
fixture.innerHTML = template.get(); | ||
var fragment = template.getFragment(); | ||
saddle.replaceBindings(fragment, fixture); | ||
function renderAndAttach(template, context) { | ||
removeChildren(fixture); | ||
fixture.innerHTML = template.get(context); | ||
template.attachTo(fixture, fixture.firstChild, context); | ||
} | ||
it('traverses a simple, valid DOM tree', function() { | ||
it('splits static text nodes', function() { | ||
var template = new saddle.Template([ | ||
new saddle.Comment('Hi') | ||
, new saddle.Element('ul', null, [ | ||
new saddle.Text('Hi') | ||
, new saddle.Text(' there.') | ||
]); | ||
renderAndAttach(template); | ||
expect(fixture.childNodes.length).equal(2); | ||
}); | ||
it('splits empty static text nodes', function() { | ||
var template = new saddle.Template([ | ||
new saddle.Text('') | ||
, new saddle.Text('') | ||
]); | ||
renderAndAttach(template); | ||
expect(fixture.childNodes.length).equal(2); | ||
}); | ||
it('splits mixed empty static text nodes', function() { | ||
var template = new saddle.Template([ | ||
new saddle.Text('') | ||
, new saddle.Text('Hi') | ||
, new saddle.Text('') | ||
, new saddle.Text('') | ||
, new saddle.Text(' there.') | ||
, new saddle.Text('') | ||
]); | ||
renderAndAttach(template); | ||
expect(fixture.childNodes.length).equal(6); | ||
}); | ||
it('adds empty text nodes around a comment', function() { | ||
var template = new saddle.Template([ | ||
new saddle.Text('Hi') | ||
, new saddle.Text('') | ||
, new saddle.Comment('cool') | ||
, new saddle.Comment('thing') | ||
, new saddle.Text('') | ||
]); | ||
renderAndAttach(template); | ||
expect(fixture.childNodes.length).equal(5); | ||
}); | ||
it('attaches to nested elements', function() { | ||
var template = new saddle.Template([ | ||
new saddle.Element('ul', null, [ | ||
new saddle.Element('li', null, [ | ||
new saddle.Text('Hi') | ||
new saddle.Text('One') | ||
]) | ||
, new saddle.Element('li', null, [ | ||
new saddle.Text('Two') | ||
]) | ||
]) | ||
]); | ||
renderAndReplace(template); | ||
renderAndAttach(template); | ||
}); | ||
it('attaches to element attributes', function() { | ||
var template = new saddle.Template([ | ||
new saddle.Element('input', { | ||
type: new saddle.Attribute('text') | ||
, autofocus: new saddle.Attribute(true) | ||
, placeholder: new saddle.Attribute(null) | ||
}) | ||
]); | ||
renderAndAttach(template); | ||
}); | ||
it('traverses with comments in a table and select', function() { | ||
@@ -323,3 +378,3 @@ // IE fails to create comments in certain locations when parsing HTML | ||
]); | ||
renderAndReplace(template); | ||
renderAndAttach(template); | ||
}); | ||
@@ -340,3 +395,3 @@ | ||
expect(function() { | ||
renderAndReplace(template) | ||
renderAndAttach(template); | ||
}).to.throwException(); | ||
@@ -351,14 +406,32 @@ }); | ||
after(function() { | ||
fixture.innerHTML = ''; | ||
removeChildren(fixture); | ||
}); | ||
function render(template, data) { | ||
fixture.innerHTML = ''; | ||
var bindings = []; | ||
var context = getContext(data, bindings); | ||
var fragment = template.getFragment(context); | ||
fixture.appendChild(fragment); | ||
return bindings; | ||
} | ||
describe('getFragment', function() { | ||
testBindingUpdates(function render(template, data) { | ||
var bindings = []; | ||
var context = getContext(data, bindings); | ||
var fragment = template.getFragment(context); | ||
removeChildren(fixture); | ||
fixture.appendChild(fragment); | ||
return bindings; | ||
}); | ||
}); | ||
describe('get + attachTo', function() { | ||
testBindingUpdates(function render(template, data) { | ||
var bindings = []; | ||
var context = getContext(data, bindings); | ||
removeChildren(fixture); | ||
fixture.innerHTML = template.get(context); | ||
template.attachTo(fixture, fixture.firstChild, context); | ||
return bindings; | ||
}); | ||
}); | ||
}); | ||
function testBindingUpdates(render) { | ||
var fixture = document.getElementById('fixture'); | ||
it('updates a single TextNode', function() { | ||
@@ -405,6 +478,6 @@ var template = new saddle.Template([ | ||
var template = new saddle.Template([ | ||
new saddle.Element('div', new saddle.AttributesMap({ | ||
new saddle.Element('div', { | ||
'class': new saddle.Attribute('message') | ||
, 'data-greeting': new saddle.DynamicAttribute(new expressions.Expression('greeting')) | ||
})) | ||
}) | ||
]); | ||
@@ -671,14 +744,20 @@ var binding = render(template).pop(); | ||
it('removes all items from a list with an else'); | ||
} | ||
}); | ||
function getContext(data, bindings) { | ||
var contextMeta = new expressions.ContextMeta({ | ||
onAdd: function(binding) { | ||
var contextMeta = { | ||
addBinding: function(binding) { | ||
bindings && bindings.push(binding); | ||
} | ||
}); | ||
, removeBinding: function() {} | ||
}; | ||
return new expressions.Context(contextMeta, data); | ||
} | ||
function removeChildren(node) { | ||
while (node && node.firstChild) { | ||
node.removeChild(node.firstChild); | ||
} | ||
} | ||
// IE <=8 return comments for Node.children | ||
@@ -685,0 +764,0 @@ function getChildren(node) { |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
68640
2
1778
0
1
+ Addedserialize-object@~0.0.2
+ Addedserialize-object@0.0.5(transitive)