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.