data-tier
Version:
Tiny and fast two way (MV-VM) data binding framework for browser environments.
258 lines (231 loc) • 6.69 kB
JavaScript
import {
TARGET_TYPES,
getPath,
setPath,
callViewMethod,
extractViewParams
} from './utils.js';
const
MUTATION_OBSERVER_OPTIONS = Object.freeze({
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['data-tie'],
attributeOldValue: true,
characterData: false,
characterDataOldValue: false
}),
ADD_LISTENER = 'addEventListener',
REMOVE_LISTENER = 'removeEventListener';
export class DOMProcessor {
constructor(dataTierInstance) {
this._dtInstance = dataTierInstance;
this._roots = new WeakMap();
this._elementsMap = new WeakSet();
this._boundDOMChangesListener = this._domChangesListener.bind(this);
this._boundChangeListener = this._changeListener.bind(this);
}
addDocument(rootDocument) {
if (!rootDocument || (Node.DOCUMENT_NODE !== rootDocument.nodeType && Node.DOCUMENT_FRAGMENT_NODE !== rootDocument.nodeType)) {
throw new Error('invalid argument, must be one of: DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE');
}
if (this._roots.has(rootDocument)) {
console.warn('any root document may be added only once');
return false;
}
const mo = new MutationObserver(this._boundDOMChangesListener)
mo.observe(rootDocument, MUTATION_OBSERVER_OPTIONS);
this._roots.set(rootDocument, mo);
for (let i = 0, l = rootDocument.children.length; i < l; i++) {
this._addTree(rootDocument.children[i]);
}
return true;
}
removeDocument(rootDocument) {
if (!this._roots.has(rootDocument)) {
console.warn(`no root document ${rootDocument} known`);
return false;
}
this._roots.get(rootDocument).disconnect();
this._roots.delete(rootDocument);
for (let i = 0, l = rootDocument.children.length; i < l; i++) {
this._dropTree(rootDocument.children[i]);
}
return true;
}
_domChangesListener(changes) {
let change, changeType, nodes, ni, nl, next;
for (let i = 0, l = changes.length; i < l; i++) {
change = changes[i];
changeType = change.type;
if (changeType === 'childList') {
nodes = change.addedNodes;
for (ni = 0, nl = nodes.length; ni < nl; ni++) {
next = nodes[ni];
if (next.nodeType === Node.ELEMENT_NODE) {
this._addTree(next);
}
}
nodes = change.removedNodes;
for (ni = 0, nl = nodes.length; ni < nl; ni++) {
next = nodes[ni];
if (next.nodeType === Node.ELEMENT_NODE) {
this._dropTree(next);
}
}
} else if (changeType === 'attributes') {
const
attributeName = change.attributeName,
node = change.target,
oldValue = change.oldValue,
newValue = node.getAttribute(attributeName);
if (attributeName === 'data-tie' && oldValue !== newValue) {
this._onTieParamChange(node, newValue, oldValue);
}
}
}
}
_onTieParamChange(element, newParam, oldParam) {
if (oldParam) {
const viewParamsOld = element[this._dtInstance.paramsKey];
if (viewParamsOld) {
this._dtInstance.views.delView(element, viewParamsOld);
this._handleChangeListener(element, REMOVE_LISTENER, viewParamsOld);
}
}
if (newParam) {
const viewParams = extractViewParams(element);
if (viewParams) {
this._dtInstance.views.addView(element, viewParams);
this._updateFromView(element, viewParams);
this._handleChangeListener(element, ADD_LISTENER, viewParams);
}
}
}
_addTree(root) {
this._addOne(root);
if (root.childElementCount) {
let nextNode;
const tw = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while ((nextNode = tw.nextNode())) {
this._addOne(nextNode);
}
}
}
_dropTree(root) {
this._dropOne(root);
if (root.childElementCount) {
let nextNode;
const tw = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while ((nextNode = tw.nextNode())) {
this._dropOne(nextNode);
}
}
}
_addOne(element) {
if (this._elementsMap.has(element)) {
return;
} else {
this._elementsMap.add(element);
}
if (element.nodeName.indexOf('-') > 0 && !element.matches(':defined')) {
this._waitDefined(element);
} else {
const viewParams = extractViewParams(element);
if (viewParams) {
this._dtInstance.views.addView(element, viewParams);
this._updateFromView(element, viewParams);
this._handleChangeListener(element, ADD_LISTENER, viewParams);
}
if (element.shadowRoot) {
this.addDocument(element.shadowRoot);
}
}
}
_waitDefined(element) {
customElements.whenDefined(element.nodeName.toLowerCase()).then(() => {
this._elementsMap.delete(element);
this._addOne(element);
});
}
_dropOne(element) {
if (!this._elementsMap.has(element)) {
return;
} else {
this._elementsMap.delete(element);
}
const viewParams = element[this._dtInstance.paramsKey];
if (viewParams) {
this._dtInstance.views.delView(element, viewParams);
this._handleChangeListener(element, REMOVE_LISTENER, viewParams);
}
if (element.shadowRoot) {
this.removeDocument(element.shadowRoot);
}
}
_changeListener(changeEvent) {
const
changeEventType = changeEvent.type,
element = changeEvent.currentTarget,
viewParams = element[this._dtInstance.paramsKey];
if (!viewParams) {
return;
}
let tieParam, tie, newValue;
let i = viewParams.length;
while (i--) {
tieParam = viewParams[i];
if (tieParam.changeEvent !== changeEventType) {
continue;
}
tie = this._dtInstance.ties.get(tieParam.tieKey);
if (tie) {
if (tieParam.targetType === TARGET_TYPES.ATTRIBUTE) {
newValue = element.getAttribute(tieParam.targetKey);
} else {
newValue = element[tieParam.targetKey];
}
setPath(tie, tieParam.path, newValue);
}
}
}
_handleChangeListener(element, action, viewParams) {
let viewParam, changeEvent;
for (let i = 0, l = viewParams.length; i < l; i++) {
viewParam = viewParams[i];
changeEvent = viewParam.changeEvent;
if (changeEvent) {
element[action](changeEvent, this._boundChangeListener);
}
}
}
_updateFromView(element, viewParams) {
let i = viewParams.length;
while (i--) {
const param = viewParams[i];
if (param.targetType === TARGET_TYPES.METHOD) {
let someData = false;
const args = [];
param.fParams.forEach(fp => {
let arg;
const tie = this._dtInstance.ties.get(fp.tieKey);
if (tie) {
arg = getPath(tie, fp.path);
someData = true;
}
args.push(arg);
});
if (someData) {
args.push(null);
callViewMethod(element, param.targetKey, args);
}
} else {
const tie = this._dtInstance.ties.get(param.tieKey);
if (tie !== undefined) {
const value = getPath(tie, param.path);
this._dtInstance.views.updateViewByModel(element, param, value);
}
}
}
}
}