@ribajs/core
Version:
Core module of Riba.js
187 lines (168 loc) • 5.34 kB
text/typescript
import { Bindable } from "../types/index.js";
import { View } from "../view.js";
import { Binder } from "../binder.js";
import { times, camelCase } from "@ribajs/utils";
/**
* each-*
* Appends bound instances of the element in place for each item in the array.
*/
export class EachStarBinder extends Binder<any[], HTMLElement> {
static key = "each-*";
static block = true;
priority = 4000;
iterated: View[] = [];
bind(el: HTMLElement) {
if (!this.marker) {
this.marker = window?.document?.createComment(` riba: ${this.type} `);
if (!el.parentNode?.insertBefore || !this.marker) {
// console.warn('No parent node!');
} else {
el.parentNode.insertBefore(this.marker, el);
el.parentNode.removeChild(el);
}
} else {
this.iterated.forEach((view: View) => {
view.bind();
});
}
}
unbind() {
if (this.iterated) {
this.iterated.forEach((view: View) => {
view.unbind();
});
}
}
routine(el: HTMLElement, collection: any[]) {
if (this.args === null) {
throw new Error("args is null");
}
let isObject = false;
const modelName = camelCase(this.args[0].toString());
collection = collection || [];
// Transform object to array to iterate over
if (
!Array.isArray(collection) &&
typeof collection === "object" &&
collection !== null
) {
collection = Object.entries(collection).map(([key, value]) => {
return { key, value };
});
isObject = true;
}
if (!Array.isArray(collection)) {
throw new Error(
`each-${
this.args[0]
} needs an array or object to iterate over, but it is ${typeof collection}`,
);
}
// if index name is set by `index-property` use this name, otherwise `%[modelName]%`
const indexProp =
el.getAttribute("index-property") || this.getIterationAlias(modelName);
collection.forEach((model, index) => {
const scope: any = { $parent: this.view.models };
// Is object transformed to array
if (isObject) {
scope[indexProp] = model.key;
scope[modelName] = model.value;
}
// Is Array
else {
scope[indexProp] = index;
scope[modelName] = model;
}
let view = this.iterated[index];
if (!view) {
let previous: HTMLElement | Element | Node | undefined;
if (this.iterated.length) {
previous = this.iterated[this.iterated.length - 1].els[0];
} else if (this.marker) {
previous = this.marker;
}
if (!previous) {
return;
}
view = View.create(this, scope, previous.nextSibling);
this.iterated.push(view);
} else {
if (view.models[modelName] !== model) {
// search for a view that matches the model
let matchIndex;
let nextView: View = this.iterated[index];
for (
let nextIndex = index + 1;
nextIndex < this.iterated.length;
nextIndex++
) {
nextView = this.iterated[nextIndex];
if (nextView.models[modelName] === model) {
matchIndex = nextIndex;
break;
}
}
if (matchIndex !== undefined) {
// model is in other position
// TODO: consider avoiding the splice here by setting a flag
// profile performance before implementing such change
this.iterated.splice(matchIndex, 1);
if (!this.marker || !this.marker.parentNode?.insertBefore) {
throw new Error("Marker has no parent node");
}
if (nextView && nextView.els[0] && view.els[0]) {
this.marker.parentNode.insertBefore(nextView.els[0], view.els[0]);
}
nextView.models[indexProp] = index;
} else {
// new model
nextView = View.create(this, scope, view.els[0]);
}
this.iterated.splice(index, 0, nextView);
} else {
view.models[indexProp] = index;
}
}
});
if (this.iterated.length > collection.length) {
times(this.iterated.length - collection.length, () => {
const view = this.iterated.pop();
if (!view) {
throw new Error("view is undefined!");
}
view.unbind();
if (!this.marker || !this.marker.parentNode) {
throw new Error("Marker has no parent node");
}
this.marker.parentNode.removeChild(view.els[0]);
});
}
if (el.nodeName === "OPTION" && this.view.bindings) {
this.view.bindings.forEach((binding: Bindable) => {
if (
this.marker &&
binding.el === this.marker.parentNode &&
binding.type === "value" &&
binding.sync
) {
binding.sync();
}
});
}
}
update(models: any) {
const data: any = {};
// TODO: add test and fix if necessary
Object.keys(models).forEach((key) => {
if (this.args === null) {
throw new Error("args is null");
}
if (key !== this.args[0]) {
data[key] = models[key];
}
});
this.iterated.forEach((view: View) => {
view.update(data);
});
}
}