@joist/element
Version:
Intelligently apply styles to WebComponents
131 lines (106 loc) • 3.14 kB
text/typescript
type Updater = () => void;
class Updates extends Set<Updater> {}
type TemplateValueGetter = (key: string) => string;
export interface TemplateOpts {
value?: TemplateValueGetter;
tokenPrefix?: string;
}
export interface RenderOpts {
refresh?: boolean;
}
export function template({ tokenPrefix = "#:", value }: TemplateOpts = {}) {
// Track all nodes that can be updated and their associated property
let updates: Updates | null = null;
return function render<T extends HTMLElement>(
this: T,
opts?: RenderOpts,
): void {
if (!updates || opts?.refresh) {
updates = findUpdates(this, {
tokenPrefix,
value: value ?? ((key: string) => getTemplateValue(this, key)),
});
} else {
for (const update of updates) {
update();
}
}
};
}
function findUpdates(el: HTMLElement, opts: Required<TemplateOpts>): Updates {
const iterator = document.createTreeWalker(
el.shadowRoot ?? el,
NodeFilter.SHOW_ELEMENT,
);
const updates = new Updates();
while (iterator.nextNode()) {
const res = trackElement(iterator.currentNode, updates, opts);
if (res !== null) {
iterator.currentNode = res;
}
}
return updates;
}
/**
* configures and tracks a given Node so that it can be updated in place later.
*/
function trackElement(
node: Node,
updates: Updates,
opts: Required<TemplateOpts>,
): Node | null {
const element = node as Element;
const getter = opts.value;
const tokenPrefix = opts.tokenPrefix;
for (const attr of element.attributes) {
const nodeValue = attr.value.trim();
const realAttributeName = attr.name.replace(tokenPrefix, "");
let update: Updater | null = null;
if (attr.name.startsWith(`${tokenPrefix}bind`)) {
update = () => {
const value = getter(attr.value);
if (element.textContent !== value) {
element.textContent = getter(attr.value);
}
};
} else if (attr.name.startsWith(tokenPrefix)) {
const isBooleanAttr = nodeValue.startsWith("!");
const isPositive = nodeValue.startsWith("!!");
const propertyKey = nodeValue.replaceAll("!", "");
if (isBooleanAttr) {
update = () => {
const value = isPositive
? !!getter(propertyKey)
: !getter(propertyKey);
if (value) {
element.setAttribute(realAttributeName, "");
} else {
element.removeAttribute(realAttributeName);
}
};
} else {
const realAttribute = document.createAttribute(realAttributeName);
element.setAttributeNode(realAttribute);
update = () => {
const value = getter(nodeValue);
if (realAttribute.value !== value) {
realAttribute.value = value;
}
};
}
}
if (update) {
updates.add(update);
update();
}
}
return null;
}
export function getTemplateValue(obj: object, key: string): any {
const parsed = key.split(".");
let pointer: any = obj;
for (const part of parsed) {
pointer = pointer[part];
}
return pointer;
}