Skip to content

Instantly share code, notes, and snippets.

@michaelowens
Last active March 20, 2020 07:18
Show Gist options
  • Save michaelowens/82ae0fddc6a7c3dd1471809f7d21a923 to your computer and use it in GitHub Desktop.
Save michaelowens/82ae0fddc6a7c3dd1471809f7d21a923 to your computer and use it in GitHub Desktop.
jQTpl - A simple jQuery template engine/mini framework.

jQTpl

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));
@weichensw
Copy link

Nice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment