bindom
Version:
Tiny, powerful data binding & web application framework
160 lines (138 loc) • 5.04 kB
JavaScript
import {getClass} from './classRegistry.mjs';
import {addStyle} from './addStyle.mjs';
import $, {bind} from './bind.mjs';
let id = 0;
const templates = new WeakMap();
const previousClass = new WeakMap();
const previousElement = new WeakMap();
const pending = new WeakMap();
export function insertAndBindTemplate(template, opt = {}) {
return withOutBinding(true, template, opt);
}
export function insertTemplate(template, opt = {}) {
return withOutBinding(false, template, opt);
}
async function withOutBinding(withBinding, template, opt) {
const parent = determineParent(template);
if(!parent) return;
const insertMethod = determineInsertMethod(template);
const reset = id => parent[insertMethod]({id, template, array: []});
const options = await debounceGetInsertOptions(
withBinding, template, opt, reset,
);
return parent[insertMethod](options);
}
function determineParent(template) {
if(!template.dataset.parent) {
if(!template.dataset.insert) return $(template, ':host');
else return $(template.parentElement);
}
else return findParent(template);
}
function findParent(element) {
let parent = element.parentElement;
const select = element.dataset.parent;
while(parent) {
const node = parent.querySelector(select);
if(node) return $(node);
parent = parent.parentElement;
}
return undefined;
}
function determineInsertMethod(template) {
return template.dataset.insert ?
template.dataset.insert :
(template.dataset.parent ? 'append' : 'before');
}
function debounceGetInsertOptions(withBinding, template, opt, reset) {
return new Promise(async resolve => {
pending.set(template, resolve);
const options = await getInsertOptions(withBinding, template, opt, reset);
if(pending.get(template) !== resolve) return;
pending.delete(template);
resolve(options);
});
}
async function getInsertOptions(withBinding, template, opt, reset) {
const {id, Class} = await getTemplate(withBinding, template, reset);
const options = {id, template, ...opt, done: done(template, opt)};
if(withBinding) options.update = executeBind(Object.assign(opt, {Class}));
if(withBinding && !opt.array) options.array = [new Class()];
return options;
}
async function getTemplate(withBinding, template, reset) {
if(!templates.has(template)) return await iniTemplate(withBinding, template);
if(!withBinding) return templates.get(template);
const tmpl = templates.get(template);
if(tmpl.Class.name === template.dataset.class) return tmpl;
reset(tmpl.id);
return await updateTemplate(template, tmpl, true);
}
async function iniTemplate(withBinding, tmplElement) {
if(!withBinding) {
templates.set(tmplElement, {id: `sq-instantiate-template-${id++}`});
return templates.get(tmplElement);
}
const opt = {id: `sq-instantiate-template-${id++}`};
await updateTemplate(tmplElement, opt);
templates.set(tmplElement, opt);
return opt;
}
async function updateTemplate(tmplElement, opt, changed) {
const {Class, style, template} = await getClass(tmplElement.dataset.class);
if(style) addStyle(style);
const $template = $(tmplElement.content);
if(changed) $template.append({array: [], id: opt.id});
if(template) $template.append({template, id: opt.id});
opt.Class = Class;
return opt;
}
function executeBind({Class, update, updateStrategy}) {
return function($dom, arrayElement, idx) {
if(doReOrNewBind(updateStrategy, $dom, Class)) {
reOrNewBind(Class, update, $dom, arrayElement, idx);
}
else {
const forceUpdate = updateStrategy === 'always';
updateDom($dom, arrayElement, idx, update, forceUpdate);
}
};
}
function doReOrNewBind(updateStrategy, [dom], Class) {
if(!updateStrategy || (updateStrategy === 'rebind')) return true;
if(previousClass.get(dom) === Class) return false;
previousClass.set(dom, Class);
return true;
}
function reOrNewBind(Class, update, $dom, arrayElement, idx) {
const correctClass = arrayElement.constructor.name === Class.name;
const el = correctClass ? arrayElement : new Class();
$dom.unbind($dom.prop('proxy'));
const proxy = bind(el, $($dom), {noRoot: true});
$dom.forEach(node => iniProxy(node, proxy));
if(!correctClass) $dom.forEach(node => node.proxy = arrayElement);
if(Class.name !== 'Object') $dom.addClass(Class.name);
if(update) update($dom, arrayElement, idx);
$dom.emit('bound', {detail: arrayElement});
}
function iniProxy(node, proxy) {
Object.defineProperty(node, 'proxy', {
get( ) {return proxy;},
set(v) {Object.assign(proxy, v);},
configurable: true,
});
}
function updateDom($dom, arrayElement, idx, update, forceUpdate) {
if(!forceUpdate && isIdentical($dom, arrayElement)) return;
$dom.forEach(node => node.proxy = arrayElement);
if(update) update($dom, arrayElement, idx);
$dom.emit('updated', {detail: arrayElement});
}
function isIdentical([dom], arrayElement) {
if(previousElement.get(dom) === arrayElement) return true;
previousElement.set(dom, arrayElement);
}
function done(template, {done}) {return function() {
$(template).emit('inserted');
if(done) done();
};}