eleva
Version:
A minimalist and lightweight, pure vanilla JavaScript frontend runtime framework.
253 lines (225 loc) • 7.71 kB
JavaScript
;
/**
* A regular expression to match hyphenated lowercase letters.
* @private
* @type {RegExp}
*/
const CAMEL_RE = /-([a-z])/g;
/**
* @class 🎯 AttrPlugin
* @classdesc A plugin that provides advanced attribute handling for Eleva components.
* This plugin extends the renderer with sophisticated attribute processing including:
* - ARIA attribute handling with proper property mapping
* - Data attribute management
* - Boolean attribute processing
* - Dynamic property detection and mapping
* - Attribute cleanup and removal
*
* @example
* // Install the plugin
* const app = new Eleva("myApp");
* app.use(AttrPlugin);
*
* // Use advanced attributes in components
* app.component("myComponent", {
* template: (ctx) => `
* <button
* aria-expanded="${ctx.isExpanded.value}"
* data-user-id="${ctx.userId.value}"
* disabled="${ctx.isLoading.value}"
* class="btn ${ctx.variant.value}"
* >
* ${ctx.text.value}
* </button>
* `
* });
*/
export const AttrPlugin = {
/**
* Unique identifier for the plugin
* @type {string}
*/
name: "attr",
/**
* Plugin version
* @type {string}
*/
version: "1.0.0-rc.1",
/**
* Plugin description
* @type {string}
*/
description: "Advanced attribute handling for Eleva components",
/**
* Installs the plugin into the Eleva instance
*
* @param {Object} eleva - The Eleva instance
* @param {Object} options - Plugin configuration options
* @param {boolean} [options.enableAria=true] - Enable ARIA attribute handling
* @param {boolean} [options.enableData=true] - Enable data attribute handling
* @param {boolean} [options.enableBoolean=true] - Enable boolean attribute handling
* @param {boolean} [options.enableDynamic=true] - Enable dynamic property detection
*/
install(eleva, options = {}) {
const {
enableAria = true,
enableData = true,
enableBoolean = true,
enableDynamic = true,
} = options;
/**
* Updates the attributes of an element to match a new element's attributes.
* This method provides sophisticated attribute processing including:
* - ARIA attribute handling with proper property mapping
* - Data attribute management
* - Boolean attribute processing
* - Dynamic property detection and mapping
* - Attribute cleanup and removal
*
* @param {HTMLElement} oldEl - The original element to update
* @param {HTMLElement} newEl - The new element to update
* @returns {void}
*/
const updateAttributes = (oldEl, newEl) => {
const oldAttrs = oldEl.attributes;
const newAttrs = newEl.attributes;
// Process new attributes
for (let i = 0; i < newAttrs.length; i++) {
const { name, value } = newAttrs[i];
// Skip event attributes (handled by event system)
if (name.startsWith("@")) continue;
// Skip if attribute hasn't changed
if (oldEl.getAttribute(name) === value) continue;
// Handle ARIA attributes
if (enableAria && name.startsWith("aria-")) {
const prop =
"aria" + name.slice(5).replace(CAMEL_RE, (_, l) => l.toUpperCase());
oldEl[prop] = value;
oldEl.setAttribute(name, value);
}
// Handle data attributes
else if (enableData && name.startsWith("data-")) {
oldEl.dataset[name.slice(5)] = value;
oldEl.setAttribute(name, value);
}
// Handle other attributes
else {
let prop = name.replace(CAMEL_RE, (_, l) => l.toUpperCase());
// Dynamic property detection
if (
enableDynamic &&
!(prop in oldEl) &&
!Object.getOwnPropertyDescriptor(Object.getPrototypeOf(oldEl), prop)
) {
const elementProps = Object.getOwnPropertyNames(
Object.getPrototypeOf(oldEl)
);
const matchingProp = elementProps.find(
(p) =>
p.toLowerCase() === name.toLowerCase() ||
p.toLowerCase().includes(name.toLowerCase()) ||
name.toLowerCase().includes(p.toLowerCase())
);
if (matchingProp) {
prop = matchingProp;
}
}
const descriptor = Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(oldEl),
prop
);
const hasProperty = prop in oldEl || descriptor;
if (hasProperty) {
// Boolean attribute handling
if (enableBoolean) {
const isBoolean =
typeof oldEl[prop] === "boolean" ||
(descriptor?.get &&
typeof descriptor.get.call(oldEl) === "boolean");
if (isBoolean) {
const boolValue =
value !== "false" &&
(value === "" || value === prop || value === "true");
oldEl[prop] = boolValue;
if (boolValue) {
oldEl.setAttribute(name, "");
} else {
oldEl.removeAttribute(name);
}
} else {
oldEl[prop] = value;
oldEl.setAttribute(name, value);
}
} else {
oldEl[prop] = value;
oldEl.setAttribute(name, value);
}
} else {
oldEl.setAttribute(name, value);
}
}
}
// Remove old attributes that are no longer present
for (let i = oldAttrs.length - 1; i >= 0; i--) {
const name = oldAttrs[i].name;
if (!newEl.hasAttribute(name)) {
oldEl.removeAttribute(name);
}
}
};
// Extend the renderer with the advanced attribute handler
if (eleva.renderer) {
eleva.renderer.updateAttributes = updateAttributes;
// Store the original _patchNode method
const originalPatchNode = eleva.renderer._patchNode;
eleva.renderer._originalPatchNode = originalPatchNode;
// Override the _patchNode method to use our attribute handler
eleva.renderer._patchNode = function (oldNode, newNode) {
if (oldNode?._eleva_instance) return;
if (!this._isSameNode(oldNode, newNode)) {
oldNode.replaceWith(newNode.cloneNode(true));
return;
}
if (oldNode.nodeType === Node.ELEMENT_NODE) {
updateAttributes(oldNode, newNode);
this._diff(oldNode, newNode);
} else if (
oldNode.nodeType === Node.TEXT_NODE &&
oldNode.nodeValue !== newNode.nodeValue
) {
oldNode.nodeValue = newNode.nodeValue;
}
};
}
// Add plugin metadata to the Eleva instance
if (!eleva.plugins) {
eleva.plugins = new Map();
}
eleva.plugins.set(this.name, {
name: this.name,
version: this.version,
description: this.description,
options,
});
// Add utility methods for manual attribute updates
eleva.updateElementAttributes = updateAttributes;
},
/**
* Uninstalls the plugin from the Eleva instance
*
* @param {Object} eleva - The Eleva instance
*/
uninstall(eleva) {
// Restore original _patchNode method if it exists
if (eleva.renderer && eleva.renderer._originalPatchNode) {
eleva.renderer._patchNode = eleva.renderer._originalPatchNode;
delete eleva.renderer._originalPatchNode;
}
// Remove plugin metadata
if (eleva.plugins) {
eleva.plugins.delete(this.name);
}
// Remove utility methods
delete eleva.updateElementAttributes;
},
};