x-widget
Version:
Adds the ability to define reusable Widgets (WebComponents) using Alpinejs.
354 lines (346 loc) • 11.7 kB
JavaScript
// src/x-widget-data.mjs
var camelToSnake = (name) => name.replace(/[a-z][A-Z]/g, (m) => m[0] + "-" + m[1].toLowerCase());
function xWidgetData(spec) {
const Alpine = this;
const propDescriptorEntries = Object.entries(Object.getOwnPropertyDescriptors(spec));
return ($el, $data) => {
const widgetEl = findWidget($el);
console.assert(widgetEl, "widget not found");
const boundProps = Array.from(widgetEl.attributes).filter((attr) => attr.name.startsWith("x-prop:")).map(({ name }) => name.substr(7));
const observer = new MutationObserver((changes) => {
changes.forEach(({ attributeName, target }) => {
setProp(attributeName, target.getAttribute(attributeName));
});
});
observer.observe(widgetEl, {
attributes: true,
attributeFilter: Object.keys(spec).map(camelToSnake),
attributeOldValue: false
});
const proplessDescriptors = Object.fromEntries(propDescriptorEntries.filter(([name]) => !boundProps.includes(name)));
const data = Alpine.reactive(Object.assign(Object.create(Object.getPrototypeOf(spec), proplessDescriptors), {
destroy() {
observer.disconnect();
}
}));
const attribs = [...widgetEl.attributes];
for (const name of Object.getOwnPropertyNames(spec)) {
const attrib = attribs.find((attr) => attr.name.match(new RegExp(`^((x-(bind|prop))?:)?${camelToSnake(name)}$`)));
if (!attrib)
continue;
if (attrib.name.startsWith("x-prop:")) {
Object.defineProperty(data, name, {
configurable: true,
enumerable: true,
get() {
return $data[name];
},
set(newValue) {
$data[name] = newValue;
}
});
} else {
setProp(name, widgetEl.getAttribute(camelToSnake(name)));
}
}
function setProp(name, attribValue) {
const defaultValue = spec[name];
if (typeof defaultValue === "boolean") {
if (attribValue === "") {
data[name] = true;
} else if (attribValue === "false") {
data[name] = false;
} else {
data[name] = !!attribValue;
}
} else if (typeof defaultValue === "number") {
data[name] = parseFloat(attribValue);
} else if (typeof defaultValue === "string") {
data[name] = attribValue;
} else {
throw new Error("unsupported static attribute: " + name);
}
}
return data;
};
}
function findWidget(el) {
while (el && !el.tagName.includes("-"))
el = el.parentElement;
return el;
}
// node_modules/alpinejs/src/scope.js
function addScopeToNode(node, data, referenceNode) {
node._x_dataStack = [data, ...closestDataStack(referenceNode || node)];
return () => {
node._x_dataStack = node._x_dataStack.filter((i) => i !== data);
};
}
function closestDataStack(node) {
if (node._x_dataStack)
return node._x_dataStack;
if (typeof ShadowRoot === "function" && node instanceof ShadowRoot) {
return closestDataStack(node.host);
}
if (!node.parentNode) {
return [];
}
return closestDataStack(node.parentNode);
}
function mergeProxies(objects) {
let thisProxy = new Proxy({}, {
ownKeys: () => {
return Array.from(new Set(objects.flatMap((i) => Object.keys(i))));
},
has: (target, name) => {
return objects.some((obj) => obj.hasOwnProperty(name));
},
get: (target, name) => {
return (objects.find((obj) => {
if (obj.hasOwnProperty(name)) {
let descriptor = Object.getOwnPropertyDescriptor(obj, name);
if (descriptor.get && descriptor.get._x_alreadyBound || descriptor.set && descriptor.set._x_alreadyBound) {
return true;
}
if ((descriptor.get || descriptor.set) && descriptor.enumerable) {
let getter = descriptor.get;
let setter = descriptor.set;
let property = descriptor;
getter = getter && getter.bind(thisProxy);
setter = setter && setter.bind(thisProxy);
if (getter)
getter._x_alreadyBound = true;
if (setter)
setter._x_alreadyBound = true;
Object.defineProperty(obj, name, {
...property,
get: getter,
set: setter
});
}
return true;
}
return false;
}) || {})[name];
},
set: (target, name, value) => {
let closestObjectWithKey = objects.find((obj) => obj.hasOwnProperty(name));
if (closestObjectWithKey) {
closestObjectWithKey[name] = value;
} else {
objects[objects.length - 1][name] = value;
}
return true;
}
});
return thisProxy;
}
// node_modules/alpinejs/src/utils/error.js
function tryCatch(el, expression, callback, ...args) {
try {
return callback(...args);
} catch (e) {
handleError(e, el, expression);
}
}
function handleError(error, el, expression = void 0) {
Object.assign(error, { el, expression });
console.warn(`Alpine Expression Error: ${error.message}
${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
setTimeout(() => {
throw error;
}, 0);
}
// src/lazy-evaluator.mjs
function lazyEvaluator(el, expression) {
let dataStack = closestDataStack(el);
let evaluator = generateEvaluatorFromString(dataStack, expression, el);
return tryCatch.bind(null, el, expression, evaluator);
}
function generateEvaluatorFromString(dataStack, expression, el) {
let func = generateFunctionFromString(expression, el);
return (receiver = () => {
}, { scope = {} } = {}) => {
func.result = void 0;
func.finished = false;
let completeScope = mergeProxies([scope, ...dataStack]);
if (typeof func === "function") {
let promise = func(func, completeScope).catch((error) => handleError(error, el, expression));
if (func.finished) {
receiver(bindResult(func.result, completeScope));
func.result = void 0;
} else {
promise.then((result) => bindResult(result, completeScope)).catch((error) => handleError(error, el, expression)).finally(() => func.result = void 0);
}
}
};
}
function bindResult(result, scope) {
return typeof result === "function" ? result.bind(scope) : result;
}
var evaluatorMemo = {};
function generateFunctionFromString(expression, el) {
if (evaluatorMemo[expression]) {
return evaluatorMemo[expression];
}
let AsyncFunction = Object.getPrototypeOf(async function() {
}).constructor;
let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression) || /^(let|const)\s/.test(expression) ? `(() => { ${expression} })()` : expression;
const safeAsyncFunction = () => {
try {
return new AsyncFunction(["__self", "scope"], `with (scope) { __self.result = ${rightSideSafeExpression} }; __self.finished = true; return __self.result;`);
} catch (error) {
handleError(error, el, expression);
return Promise.resolve();
}
};
let func = safeAsyncFunction();
evaluatorMemo[expression] = func;
return func;
}
// src/x-prop-directive.mjs
var snakeToCamel = (name) => name.replace(/-([a-zA-Z])/g, (m) => m[1].toUpperCase() + m.substr(2));
function xPropDirective(el, { value: attribName, expression }, { cleanup }) {
const propName = snakeToCamel(attribName);
const read = lazyEvaluator(expression === propName ? el.parentElement : el, expression);
let unsafeValue;
const setter = safeLeftHandSide(el, expression) ? lazyEvaluator(el.parentElement, `${expression} = __`) : (_, { scope: { __: newValue } }) => unsafeValue = newValue;
const propObj = {};
Object.defineProperty(propObj, propName, {
get() {
let result;
if (typeof unsafeValue !== "undefined") {
return unsafeValue;
}
read((newValue) => result = newValue);
return result;
},
set(newValue) {
if (propName !== "value") {
el[propName] = newValue;
}
setter(() => {
}, { scope: { __: newValue } });
}
});
if (propName !== "value") {
el[propName] = propObj[propName];
}
const removeScope = addScopeToNode(el, propObj);
cleanup(() => removeScope());
}
function safeLeftHandSide(el, lhs) {
try {
let scope = mergeProxies(closestDataStack(el));
new Function("scope", `with(scope) {${lhs} = ${lhs}}`)(scope);
return true;
} catch {
return false;
}
}
// src/x-widget.mjs
function slotsMagic(el) {
while (el && !el._x_slots)
el = el.parentElement;
return el?._x_slots;
}
function xWidgetDirective(el, { expression, modifiers }, { Alpine }) {
const tagName = expression;
if (window.customElements.get(tagName))
return;
if (modifiers[0]) {
const style = document.createElement("style");
style.innerHTML = `${tagName} { display: ${modifiers[0]}}`;
document.head.appendChild(style);
}
if (Alpine._widgets) {
Alpine._widgets.push(tagName);
} else {
Alpine._widgets = [tagName];
}
const templateContent = el.content.firstElementChild;
window.customElements.define(tagName, class extends HTMLElement {
constructor() {
super();
this._slotFills = null;
}
connectedCallback() {
let slotFills;
if (this._slotFills) {
slotFills = this._slotFills;
} else {
slotFills = collectSlotFills(this);
this._slotFills = slotFills;
}
const newEl = templateContent.cloneNode(true);
this._x_slots = Object.fromEntries([...slotFills.entries()].map(([name, value]) => [name, value]));
const targetSlots = findTargetSlots(newEl);
for (const targetSlot of targetSlots) {
const slotName = targetSlot.name || "default";
const fills = slotFills.get(slotName);
if (fills) {
targetSlot.replaceWith(...fills.map((n) => n.cloneNode(true)));
} else {
targetSlot.replaceWith(...[...targetSlot.childNodes]);
}
}
requestAnimationFrame(() => {
while (this.firstChild) {
this.removeChild(this.firstChild);
}
this.appendChild(newEl);
});
this.dispatchEvent(new CustomEvent("x-widget:connected", {
bubbles: true
}));
}
});
}
function findTargetSlots(el) {
let slots = [...el.querySelectorAll("slot")];
if (el.tagName === "SLOT")
slots.unshift(el);
const templates = el.querySelectorAll("template");
for (const template of templates) {
if (template.getAttribute("x-widget"))
continue;
for (const child of template.content.children) {
slots.push(...findTargetSlots(child));
}
}
return slots;
}
function collectSlotFills(el) {
const slots = new Map();
function collectForSlot(slotName, nodes) {
if (slots.has(slotName)) {
slots.get(slotName).push(...nodes);
} else {
slots.set(slotName, nodes);
}
}
for (const child of el.childNodes) {
if (child.tagName === "TEMPLATE") {
const slotName = child.getAttribute("slot");
const isSlotFill = !slotName && (child.getAttribute("x-for") || child.getAttribute("x-if"));
collectForSlot(slotName || "default", isSlotFill ? [child] : [...child.content.childNodes]);
} else if (child.nodeType !== Node.TEXT_NODE || child.textContent.trim()) {
collectForSlot("default", [child]);
}
}
return slots;
}
// src/index.mjs
function src_default(Alpine) {
Alpine.magic("slots", slotsMagic);
Alpine.directive("widget", xWidgetDirective);
Alpine.directive("prop", xPropDirective);
Alpine.data("xWidget", xWidgetData.bind(Alpine));
}
export {
src_default as default,
slotsMagic,
xPropDirective,
xWidgetData,
xWidgetDirective
};