data-tier
Version:
Tiny and fast two way (MV-VM) data binding framework for browser environments.
234 lines (216 loc) • 5.94 kB
JavaScript
import { extractViewParams, getRandomKey, TARGET_TYPES } from './utils.js';
export class Views {
constructor(dtInstance) {
this.dti = dtInstance;
this.views = {};
this.scopes = {};
this.unscoped = [];
}
obtainTieViews(tieKey) {
let result = this.views[tieKey];
if (!result) {
result = { _pathsCache: [] };
this.views[tieKey] = result;
}
return result;
}
deleteTieViews(tieKey) {
delete this.views[tieKey];
}
addView(element, tieParams) {
this._handleView(element, tieParams, true);
}
delView(element, tieParams) {
this._handleView(element, tieParams, false);
}
addScope(element) {
let scopeKey = element.getAttribute('data-tie-scope');
if (!scopeKey) {
// TODO: review this one
element.setAttribute('data-tie-scope', getRandomKey(16));
return;
}
if (scopeKey in this.scopes && this.scopes[scopeKey] !== element) {
throw new Error(`scope key '${scopeKey} already claimed by another element`);
}
if (this.scopes[scopeKey] === element) {
return;
}
this.scopes[scopeKey] = element;
for (const unscoped of this.unscoped) {
if (element.contains(unscoped)) {
const viewParams = extractViewParams(unscoped);
if (viewParams) {
this.addView(unscoped, viewParams);
this.unscoped.splice(this.unscoped.indexOf(unscoped), 1);
}
}
}
}
delScope() {
throw new Error('not implemented');
}
_handleView(element, tieParams, toAdd) {
let tieParam, fParams, fp;
let i = tieParams.length, l;
while (i--) {
tieParam = tieParams[i];
if (tieParam.targetType === TARGET_TYPES.METHOD) {
fParams = tieParam.fParams;
l = fParams.length;
while (l--) {
fp = fParams[l];
this[toAdd ? '_seekAndInsertView' : '_seekAndRemoveView'](fp, element);
}
} else {
this[toAdd ? '_seekAndInsertView' : '_seekAndRemoveView'](tieParam, element);
}
}
if (toAdd) {
element[this.dti.paramsKey] = tieParams;
} else {
delete element[this.dti.paramsKey];
}
}
_seekAndInsertView(tieParam, element) {
let tieKey = tieParam.tieKey;
if (tieKey === 'scope') {
tieKey = this._lookupClosestScopeKey(element);
if (!tieKey) {
this.unscoped.push(element);
return;
} else {
tieParam.tieKey = tieKey;
}
}
const rawPath = tieParam.rawPath;
const tieViews = this.obtainTieViews(tieKey);
let pathViews = tieViews[rawPath];
if (!pathViews) {
pathViews = [];
tieViews[rawPath] = pathViews;
tieViews._pathsCache.push(rawPath);
}
if (pathViews.indexOf(element) < 0) {
pathViews.push(element);
}
}
_seekAndRemoveView(tieParam, element) {
const tieKey = tieParam.tieKey;
const rawPath = tieParam.rawPath;
const tieViews = this.views[tieKey];
if (tieViews) {
const pathViews = tieViews[rawPath];
if (pathViews) {
const index = pathViews.indexOf(element);
if (index >= 0) {
pathViews.splice(index, 1);
}
}
}
}
_lookupClosestScopeKey(element) {
let tmp = element, result;
do {
result = tmp.getAttribute('data-tie-scope');
if (result) {
break;
}
tmp = tmp.parentNode;
if (tmp.host) {
tmp = tmp.host;
}
} while (tmp && tmp.nodeType !== Node.DOCUMENT_NODE);
return result;
}
updateViewByModel(elem, param, value, oldValue) {
const targetType = param.targetType || TARGET_TYPES.PROPERTY;
const targetKey = param.targetKey;
try {
switch (targetType) {
case TARGET_TYPES.ATTRIBUTE:
this._unsafeSetAttribute(elem, targetKey, value);
break;
case TARGET_TYPES.EVENT:
this._unsafeSetEvent(elem, targetKey, value, oldValue);
break;
case TARGET_TYPES.PROPERTY:
this._unsafeSetProperty(elem, param, targetKey, value);
break;
default:
throw new Error(`unsupported target type '${targetType}'`);
}
} catch (e) {
console.error(`failed to set '${targetKey}' of '${elem}' to '${value}'`, e);
}
}
_unsafeSetAttribute(view, targetAttribute, value) {
if (value === null || value === undefined) {
view.removeAttribute(targetAttribute);
} else {
view.setAttribute(targetAttribute, String(value));
}
}
_unsafeSetEvent(view, targetEvent, value, oldValue) {
if (typeof oldValue === 'function') {
view.removeEventListener(targetEvent, oldValue);
}
if (typeof value === 'function') {
view.addEventListener(targetEvent, value);
}
}
_unsafeSetProperty(view, param, targetProperty, value) {
if (targetProperty === 'textContent') {
this._setTextContentProperty(view, value);
} else if (targetProperty === 'value') {
this._setValueProperty(view, value);
} else if (targetProperty === 'href' && typeof view.href === 'object') {
view.href.baseVal = value;
} else if (targetProperty === 'scope') {
// TODO: this is the ONLY line that refers to a state
this.dti.ties.update(view, value);
} else if (targetProperty === 'classList') {
const classes = param.iClasses.slice(0);
if (value) {
if (Array.isArray(value) && value.length) {
value.forEach(c => {
if (classes.indexOf(c) < 0) {
classes.push(c);
}
});
} else if (typeof value === 'object') {
Object.keys(value).forEach(c => {
const i = classes.indexOf(c);
if (value[c]) {
if (i < 0) {
classes.push(c);
}
} else if (i >= 0) {
classes.splice(i, 1);
}
});
} else if (typeof value === 'string') {
if (classes.indexOf(value) < 0) {
classes.push(value);
}
}
}
view.className = classes.join(' ');
} else {
view[targetProperty] = value;
}
}
_setTextContentProperty(view, value) {
view.textContent = value === undefined || value === null ? '' : value;
}
_setValueProperty(view, value) {
let v = value;
if (value === undefined || value === null) {
const viewName = view.nodeName;
if (viewName === 'INPUT' || viewName === 'SELECT' || viewName === 'TEXTAREA') {
v = '';
}
}
view.value = v;
}
}