Skip to content

Instantly share code, notes, and snippets.

@ivankravchenko
Created June 9, 2015 17:38
Show Gist options
  • Save ivankravchenko/60a36080bce982801006 to your computer and use it in GitHub Desktop.
Save ivankravchenko/60a36080bce982801006 to your computer and use it in GitHub Desktop.
function evalInContext(expression, context) {
maskedContext = {}
for (var k in this) maskedContext[k] = undefined
for (var k in context) maskedContext[k] = context[k]
return (new Function("with(this){return (" + expression + ")}")).call(maskedContext)
}
<template name="hello">
<h1>Hello, {{name}}!</h1>
<template if="{{displayTasks}}">
<ul>
<template loop="{{tasks}}">
<li id="task-{{id}}" class="{{done?'done':''}}">
<input type="checkbox" {{done?'checked':''}}>
{{title}}
</li>
</template>
</ul>
</template>
<p data-time="time-{{time}}" data-name="{{name}}">Time is {{time}} and displayTasks={{displayTasks}}</p>
<template if="{{displayTasks}}">ku ku</template>
<div style="transform: translateX({{displayTasks ? 10 : 20}}px);">ku ku 2</div>
</template>
<meta charset="utf8">
<script src="evalInContext.js"></script>
<script src="View.js"></script>
<link rel="import" href="hello.tpl.html">
<script>
document.addEventListener("DOMContentLoaded", function(event) {
var model = {
name: "<b>world</b> (correctly escaped)",
time: (new Date()).getTime(),
tasks: [
{ id: 1, title: "task 1", done: false },
{ id: 2, title: "task 2", done: false },
{ id: 3, title: "task 3", done: true }
],
displayTasks: true
}
setInterval(function(){
model.time = (new Date()).getTime()
model.displayTasks = !model.displayTasks
}, 5000)
var view = new View(View.queryTemplate("hello"), model, document.body)
})
</script>
function View(template, model, parentNode) {
this.model = model
this.parentNode = null
this.bindings = {}
this.node = document.importNode(template.content, true)
this.childNodesSlice = Array.prototype.slice.apply(this.node.childNodes)
this.discoverNodes(this.node.childNodes)
this.setupObserve()
if (parentNode) {
this.mount(parentNode)
}
}
View.prototype.discoverNodes = function(nodes) {
for (var i = 0; i < nodes.length; i++) {
this.discoverNode(nodes[i])
}
}
View.prototype.discoverNode = function(node) {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
this.discoverElementNode(node)
break
case Node.ATTRIBUTE_NODE:
this.discoverAttributeNode(node)
break
case Node.TEXT_NODE:
this.discoverTextNode(node)
break
default:
throw new Error("incompatible node came into View.discoverNode")
}
}
View.prototype.discoverElementNode = function(node) {
if (node.tagName === "TEMPLATE") {
this.discoverTemplateNode(node)
} else {
this.discoverNodes(node.attributes)
this.discoverNodes(node.childNodes)
}
}
View.prototype.discoverAttributeNode = function(node) {
var textFragments = node.value.split(/(\{\{.+?\}\})/g).filter(function(s){
return s !== ""
})
var hasExpression = false
var expression = textFragments.map(function(s){
m = s.match(/^\{\{(.+)\}\}$/)
if (m) {
hasExpression = true
return m[1]
} else {
return JSON.stringify(s)
}
}).join(" + ")
if (hasExpression) {
this.bindNode(node, expression)
}
}
View.prototype.discoverTextNode = function(node) {
var textFragments = node.data.split(/(\{\{.+?\}\})/g).filter(function(s){
return s !== ""
})
if (textFragments.length > 1) {
for (var i in textFragments) {
var s = textFragments[i]
var textNode = document.createTextNode(s)
m = s.match(/^\{\{(.+)\}\}$/)
if (m) this.bindNode(textNode, m[1])
node.parentNode.insertBefore(textNode, node)
}
node.parentNode.removeChild(node)
} else if (textFragments.length === 1) {
var s = textFragments[0]
m = s.match(/^\{\{(.+)\}\}$/)
if (m) this.bindNode(node, m[1])
}
}
View.prototype.discoverTemplateNode = function(node) {
var mountPoint = document.createTextNode("")
node.parentNode.insertBefore(mountPoint, node)
node.remove()
node.mountPoint = mountPoint
this.discoverNodes(node.attributes)
}
View.prototype.applyNodeValue = function(node, value) {
switch (node.nodeType) {
case Node.ATTRIBUTE_NODE:
if (node.ownerElement.tagName === "TEMPLATE") {
var template = node.ownerElement
switch (node.name) {
case "if":
if (!!value) {
template.mountPoint.renderedView = new View(
template, this.model, template.mountPoint)
} else {
if (template.mountPoint.renderedView) {
template.mountPoint.renderedView.destroy()
}
}
break;
case "loop":
if (!!value && value instanceof Array && value.length > 0) {
template.mountPoint.renderedViews = value.map(function(v) {
return new View(template, v, template.mountPoint)
})
} else {
if (template.mountPoint.renderedViews) {
template.mountPoint.renderedViews.forEach(function(view){
view.destroy()
})
}
}
break
default:
throw new Error("don't know how to apply value on " + node.name + " attribute of template")
}
} else {
node.value = value
}
break
case Node.TEXT_NODE:
node.data = value
break
default:
throw new Error("incompatible node came into View.applyNodeValue")
}
}
View.prototype.bindNode = function(node, expression) {
if (!this.bindings[expression]) {
this.bindings[expression] = []
}
this.bindings[expression].push(node)
this.applyNodeValue(node, evalInContext(expression, this.model))
}
View.prototype.setupObserve = function() {
if (!this.model) {
return
}
var model = this.model
var bindings = this.bindings
var applyNodeValue = this.applyNodeValue.bind(this)
// to be written
Object.observe(model, function(changes){
changes.forEach(function(change){
if (change.name in bindings) {
bindings[change.name].forEach(function(node){
applyNodeValue(node, evalInContext(change.name, model))
})
}
})
})
}
View.prototype.unmount = function() {
if (!this.parentNode) {
return
}
for (var i = 0; i < this.childNodesSlice.length; i++) {
this.childNodesSlice[i].remove()
}
}
View.prototype.destroy = function() {
this.unmount()
// to be written
}
View.prototype.mount = function(parentNode) {
if (parentNode.nodeType === Node.TEXT_NODE) {
parentNode.parentNode.insertBefore(this.node, parentNode)
this.parentNode = parentNode.parentNode
} else {
parentNode.appendChild(this.node)
this.parentNode = parentNode
}
}
View.queryTemplate = function(templateName, rootDocument) {
rootDocument = rootDocument || document
var template = rootDocument.querySelector("template[name='" + templateName + "']")
if (template) {
return template
}
imports = rootDocument.querySelectorAll('link[rel="import"]')
for (var i = 0; i < imports.length; i++) {
template = View.queryTemplate(templateName, imports[i].import)
if (template) {
return template
}
}
throw new Error("cannot find template " + templateName)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment