soma3/src/directives/attribute.ts
2024-10-19 08:17:26 -04:00

114 lines
3.1 KiB
TypeScript

import { Context, evalGet } from "..";
import { classNames, extractAttributeName } from "../util";
interface AttributeDirectiveOptions {
element: Element;
context: Context;
attr: Attr;
}
interface Is {
sameNameProperty: boolean;
bound: boolean;
spread: boolean;
componentProp: boolean;
}
export class AttributeDirective {
element: Element;
context: Context;
expression: string;
attr: Attr;
extractedAttributeName: string;
previousClasses: string[] = [];
previousStyles: { [key: string]: string } = {};
is: Is = {
sameNameProperty: false,
bound: false,
spread: false,
componentProp: false,
};
constructor({ element, context, attr }: AttributeDirectiveOptions) {
this.element = element;
this.context = context;
this.expression = attr.value;
this.attr = attr;
this.extractedAttributeName = extractAttributeName(attr.name);
this.is = {
sameNameProperty: attr.name.startsWith("{") && attr.name.endsWith("}"),
bound: attr.name.includes(":bind"),
spread: attr.name.startsWith("..."),
componentProp: false,
};
if (this.is.sameNameProperty) {
this.expression = this.extractedAttributeName;
}
if (this.is.spread) {
this.expression = this.extractedAttributeName;
}
console.log("attribute", attr.name, "spread?", this.is.spread)
element.removeAttribute(attr.name);
if (this.is.bound) {
context.effect(this.update.bind(this));
} else {
this.update();
}
}
update() {
let value = evalGet(this.context.scope, this.expression);
if (this.is.spread && typeof value === "object") {
for (const [key, val] of Object.entries(value)) {
this.element.setAttribute(key, String(val));
}
} else if ((typeof value === "object" || Array.isArray(value)) && this.extractedAttributeName === "class") {
value = classNames(value);
const next = value.split(" ");
// If we now have classes that are not already on the element, add them now.
// Remove classes that are no longer on the element.
const diff = next.filter((c: string) => !this.previousClasses.includes(c)).filter(Boolean);
const rm = this.previousClasses.filter((c) => !next.includes(c));
diff.forEach((c: string) => {
this.previousClasses.push(c);
this.element.classList.add(c);
});
rm.forEach((c) => {
this.previousClasses = this.previousClasses.filter((addedClass) => addedClass !== c);
this.element.classList.remove(c);
});
} else if (typeof value === "object" && this.extractedAttributeName === "style") {
const next = Object.keys(value);
const rm = Object.keys(this.previousStyles).filter((style) => !next.includes(style));
next.forEach((style) => {
this.previousStyles[style] = value[style];
// @ts-ignore
this.element.style[style] = value[style];
});
rm.forEach((style) => {
this.previousStyles[style] = "";
// @ts-ignore
this.element.style[style] = "";
});
this.previousStyles = value;
} else {
this.element.setAttribute(this.extractedAttributeName, value);
}
}
}