jQTpl - A simple jQuery template engine/mini framework.
I made this to easily be able to add UI elements, with 2-way binding, to websites using tampermonkey scripts. Feel free to do whatever you want.
/** | |
* Basic app example | |
*/ | |
var {$template, observable, textNode} = jQTpl; // import | |
$(function () { | |
var data = observable({ | |
name: 'Michael', | |
lastName: 'Test', | |
fullName () { | |
return this.name + ' ' + this.lastName | |
}, | |
seconds: 0, | |
showFooter: true, | |
time: { | |
minutes: 0 | |
} | |
}) | |
setInterval(function () { | |
data.seconds++ | |
data.time.minutes = (data.seconds / 60).toFixed(2) | |
}, 1000) | |
var footer = $template` | |
<div class="footer"> | |
Footer template! | |
</div> | |
` | |
var app = $template` | |
<div> | |
<h2>Hello {{name}}!</h2> | |
<p class="creator_message">You are the creator of jQTpl!!</p> | |
Name: <input name="name"><br> | |
<button name="changeName" data-name="Djilano">Change name to Djilano</button> | |
<div>You have been here for {{seconds}} seconds</div> | |
<p>But in p tag it goes to next line? {{time.minutes}} minutes</p> | |
<button name="showFooter">Toggle footer</button> | |
${footer()} | |
</div> | |
` | |
var $app = app(data, $('body')) | |
$app | |
.tplModel('input[name="name"]', data.name) | |
.tplShow('.creator_message', data.name, (name) => name === 'Michael') | |
.tplShow('.footer', data.showFooter) | |
// jQuery bindings like you are used to | |
$app.find('button[name="changeName"]').on('click', e => { | |
data.name = $(e.target).data('name') | |
}) | |
$app.find('button[name="showFooter"]').on('click', () => { | |
data.showFooter = !data.showFooter | |
}) | |
}) |
// collapse next line to easily get to example implementation | |
;window['jQTpl'] = (function ($) { | |
let inDepTarget = null | |
let dotFromObject = function (obj, dotNotation) { | |
return dotNotation.split('.').reduce((o,i) => o[i], obj) | |
} | |
let updateDotFromObject = function (obj, dotNotation, value) { | |
var dots = dotNotation.split('.') | |
var parentDataObject = dots.reduce((o, i, ci) => { | |
if (ci === dots.length - 1) { | |
return o | |
} | |
return o[i] | |
}, obj) | |
var lastdot = dots[dots.length - 1] | |
parentDataObject[lastdot] = value | |
} | |
let $template = function (strings, ...values) { | |
// compile html with tmp holders for nested nodes | |
let html = '' | |
let nodes = [] | |
for (var i in strings) { | |
html += strings[i] | |
let value = values[i] | |
if (value) { | |
if (value instanceof Node || value instanceof jQuery) { | |
html += `<div id="jQTpl-tmp-node-${i}"></div>` | |
nodes.push(i) | |
} else { | |
html += value | |
} | |
} | |
} | |
// find variables | |
html = html.replace(/{{(.*?)}}/g, function (m, key){ | |
return `<div class="jQTpl-tmp-var-${key}"></div>` | |
}) | |
return function ($data, $root) { | |
$data = $data || null | |
$root = $root || $('#app') | |
// add watcher for changes | |
let $dataEls = {} | |
let changeListeners = {} | |
let showListeners = {} | |
if ($data) { | |
$data._onChange((key, newValue, oldValue) => { | |
// Update textNodes | |
if (key in $dataEls) { | |
$dataEls[key].forEach(node => node.nodeValue = newValue) | |
} | |
// 2-way binding: from data to vioew | |
if (key in changeListeners) { | |
changeListeners[key].forEach(node => node.val(newValue)) | |
} | |
// show/hide elements | |
if (key in showListeners) { | |
showListeners[key].forEach(node => node[0].toggle(node[1](newValue))) | |
} | |
}) | |
} | |
let $doc = $(html) | |
nodes.forEach((i) => { | |
$doc.find(`#jQTpl-tmp-node-${i}`).replaceWith(values[i]) | |
$doc.find(`[class^=jQTpl-tmp-var-]`).each((k, $el) => { | |
var datakey = $el.className.split('-').pop() | |
var value = dotFromObject($data, datakey) | |
var t = document.createTextNode(value) | |
$el.replaceWith(t) | |
$dataEls[datakey] = $dataEls[datakey] || [] | |
$dataEls[datakey].push(t) | |
}) | |
}) | |
$doc.tplModel = function (selector, model) { | |
let cleanSelector = selector.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') | |
let tplModelRegex = new RegExp('tplModel[\\s]*\\([\\s]*([\'"`])' + cleanSelector + '\\1[\s]*,[\s]*(.+?)\\)', 'g') | |
let tplModelCall = tplModelRegex.exec(arguments.callee.caller.toString()) | |
if (!tplModelCall || tplModelCall.length < 3) { | |
return this; | |
} | |
var dots = tplModelCall[2].split('.') | |
dots.shift() | |
var dataDotNotation = dots.join('.') | |
var value = dotFromObject($data, dataDotNotation) | |
var $el = this.find(selector) | |
var $dataEl = $dataEls[dataDotNotation] | |
$el | |
.val(value) | |
.on('input', (e) => updateDotFromObject($data, dataDotNotation, e.target.value)) // this doesn't work for dot notation of course | |
changeListeners[dataDotNotation] = changeListeners[dataDotNotation] || [] | |
changeListeners[dataDotNotation].push($el) | |
return this | |
} | |
$doc.tplShow = function (selector, model, validator) { | |
if (!validator) { | |
validator = (value) => value | |
} | |
let cleanSelector = selector.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') | |
let tplShowRegex = new RegExp('tplShow[\\s]*\\([\\s]*([\'"`])' + cleanSelector + '\\1[\\s]*,[\\s]*(.+)[\\s]*\\)', 'g') | |
let tplShowCall = tplShowRegex.exec(arguments.callee.caller.toString()) | |
if (!tplShowCall || tplShowCall.length < 3) { | |
return this; | |
} | |
let lastParams = tplShowCall[2].split(',') | |
let m = lastParams.shift() | |
var dots = m.split('.') | |
dots.shift() | |
var dataDotNotation = dots.join('.') | |
var $el = this.find(selector) | |
showListeners[dataDotNotation] = showListeners[dataDotNotation] || [] | |
showListeners[dataDotNotation].push([$el, validator]) | |
return this | |
} | |
$root.append($doc) | |
return $doc | |
} | |
} | |
let observable = function (obj) { | |
let listeners = [] | |
const handler = function (root) { | |
root = root || '' | |
if (root) root += '.' | |
let deps = {} | |
return { | |
set(target, key, value, receiver) { | |
// extend proxify to appended nested object | |
if(({}).toString.call(value) === "[object Object]") { | |
value = deepApply(key, value) | |
} | |
let oldValue = target[key] | |
target[key] = value | |
if (key in deps && deps[key]) { | |
deps[key].forEach(changeFunc => { | |
changeFunc() | |
}) | |
} | |
listeners.forEach(cb => cb(`${root}${key}`, target[key], oldValue)) | |
return Reflect.set(target, key, value, receiver) | |
}, | |
get(target, key, receiver) { | |
if (key === 'toJSON') { | |
return function() { return target; } | |
} | |
if(!(key in target)) { | |
target[key] = new Proxy({}, handler()) | |
} | |
if (inDepTarget) { | |
deps[key] = deps[key] || [] | |
if (deps[key].indexOf(inDepTarget) == -1) { | |
deps[key].push(inDepTarget) | |
} | |
} | |
return Reflect.get(target, key, receiver) | |
}, | |
deleteProperty(target, key) { | |
delete target[key] | |
}, | |
has: function(target, prop) { | |
if (prop === '_onChange') { | |
return false | |
} | |
return prop in target | |
} | |
} | |
} | |
let deepApply = function (property, data) | |
{ | |
var proxy = new Proxy({}, handler(property)) | |
var props = Object.keys(data) | |
var size = props.length | |
for (var i = 0; i < size; i++) | |
{ | |
property = props[i] | |
proxy[property] = data[property] | |
} | |
return proxy | |
} | |
Object.defineProperty(obj, '_onChange', { | |
configurable: false, | |
writable: false, | |
enumerable: false, // hide it from for..in | |
value: function (cb) { | |
listeners.push(cb) //console.log('_onChange registered') | |
} | |
}) | |
Object.keys(obj).forEach(k => { | |
let v = obj[k] | |
if(({}).toString.call(v) === "[object Object]") { | |
v = deepApply(k, v) | |
} | |
obj[k] = v | |
}) | |
let p = new Proxy(obj || {}, handler()) | |
Object.keys(obj).forEach(key => { | |
if (typeof obj[key] !== 'function') { | |
return | |
} | |
let f = obj[key].bind(p) | |
let value; | |
let onDependencyUpdated = function () { | |
let oldValue = value | |
value = f() | |
listeners.forEach(cb => cb(key, value, oldValue)) | |
} | |
Object.defineProperty(p, key, { | |
get: function () { | |
inDepTarget = onDependencyUpdated | |
value = f() | |
inDepTarget = null | |
return value | |
} | |
}) | |
}) | |
return p | |
} | |
return { | |
$template: $template, | |
observable: observable, | |
textNode: (text) => document.createTextNode(text) | |
} | |
}(jQuery)); |
Nice.