function arrayView({
name = "arrayNode" + DOM.uid().id,
value = [],
initial = [],
builder
} = {}) {
if (value.length > 0 && !builder)
throw new Error(
"You cannot initialize an arrayView with data without a builder"
);
const frag = new DocumentFragment();
const subviewToFragmentEventCloner = (e) => {
const new_e = new e.constructor(e.type, e);
frag.dispatchEvent(new_e);
};
const _builder = builder
? (arg) => {
const subview = builder(arg);
subview.addEventListener("input", subviewToFragmentEventCloner);
return subview;
}
: undefined;
const unbuilder = (subview) => {
subview.removeEventListener("input", subviewToFragmentEventCloner);
};
initial.forEach((subview) =>
subview.addEventListener("input", subviewToFragmentEventCloner)
);
const start = document.createComment("START:" + name);
const end = document.createComment("END:" + name);
let subviews = (_builder ? value.map(_builder) : []).concat(initial);
frag.append(...[start, ...subviews, end]);
frag.addEventListener("input", (e) => {
// https://stackoverflow.com/questions/11974262/how-to-clone-or-re-dispatch-dom-events
const new_e = new e.constructor(e.type, e);
start.dispatchEvent(new_e);
});
const getIndexProperty = (index) => ({
get: () => subviews[index],
enumerable: true,
configurable: true
});
const customSplice = (startIndex, deleteCount, ...items) => {
const parent = start.parentNode;
startIndex = Math.floor(startIndex);
const removedData = [];
// sync the splice with the DOM
let node = start;
// Forward to begining of the splice
for (let i = 0; i < startIndex && i < subviews.length; i++)
node = node.nextSibling;
// delete 'deleteCount' times
for (let i = 0; i < deleteCount && i < subviews.length; i++) {
const toDelete = node.nextSibling;
removedData.push(toDelete.value);
unbuilder(toDelete);
toDelete.remove();
}
// add additional items
const itemViews = [];
for (let i = items.length - 1; i >= 0; i--) {
const subview = _builder(items[i]);
Object.defineProperty(frag, i, getIndexProperty(i));
let presentation =
subview instanceof HTMLElement ? subview : htl.html`${subview}`;
itemViews.unshift(subview);
parent.insertBefore(presentation, node.nextSibling);
}
// Apply to cache
subviews.splice(startIndex, deleteCount, ...itemViews);
// Let flow upwards to array too
return removedData;
};
// We intercept operations to the data array and use it to drive DOM operations too.
const dataArrayProxyHandler = {
get: function (target, prop, receiver) {
const args = arguments;
if (prop === "splice") {
return customSplice;
} else if (prop === "push") {
return (...elements) => {
customSplice(subviews.length, 0, ...elements);
return subviews.length;
};
} else if (prop === "pop") {
return () => {
return customSplice(subviews.length - 1, 1)[0];
};
} else if (prop === "shift") {
return () => {
return customSplice(0, 1)[0];
};
} else if (prop === "unshift") {
return (...elements) => {
customSplice(0, 0, ...elements);
return subviews.length;
};
}
return Reflect.get(...args);
},
set(obj, prop, value) {
if (!isNaN(+prop)) {
// we also need to set the view
customSplice(+prop, 1, value);
}
return Reflect.set(...arguments);
}
};
// Add data channel
Object.defineProperties(frag, {
value: {
get: () =>
new Proxy(
subviews.map((sv) => sv.value),
dataArrayProxyHandler
),
set: (newArray) => {
const vArr = _.cloneDeep(newArray);
const parent = start.parentNode;
if (builder) {
// We should be true to the operation and tear of the DOM and then replace it.
subviews.forEach((sv) => (sv.remove ? sv.remove() : undefined));
subviews = vArr.map((data) => {
const subview = _builder(data);
let presentation =
subview instanceof HTMLElement ? subview : htl.html`${subview}`;
parent.insertBefore(presentation, end);
return subview;
});
} else {
// We have to work around the limitations and try to do the operation without
// building, so this only can work if you are setting it to something smaller
vArr.forEach((v, i) => {
if (i < subviews.length) {
subviews[i].value = v; // mutate inplace
} else {
let built = _builder(v); // append additional
subviews[i] = built;
if (!(built instanceof HTMLElement)) built = htl.html`${built}`;
parent.appendChild(built);
}
});
for (var i = subviews.length - 1; i >= vArr.length; i--) {
// delete backwards
const deleted = subviews.pop();
if (deleted.remove) deleted.remove();
}
}
}
}
});
// Add presentation channel
return Object.defineProperties(frag, {
remove: {
value: () => {
const toRemove = [];
for (var node = start; node !== end; node = node.nextSibling) {
toRemove.push(node);
}
toRemove.push(end);
toRemove.forEach((n) => n.remove());
}
},
length: {
get: () => subviews.length,
enumerable: true,
configurable: true
},
[Symbol.iterator]: {
value: () => {
let index = 0;
return {
next() {
if (index < subviews.length) {
let val = subviews[index];
index++;
return { value: val, done: false };
} else return { done: true };
}
};
}
},
...subviews.reduce((acc, sv, index) => {
acc[index] = getIndexProperty(index);
return acc;
}, {})
});
}