@razen-core/zenweb
Version:
A minimalist TypeScript framework for building reactive web applications with no virtual DOM
256 lines • 7.99 kB
JavaScript
/**
* ZenWeb DOM Manipulation
* Direct DOM element creation and manipulation (vanilla JavaScript)
* No Virtual DOM - just real DOM elements
*/
import { debugLog } from './debug.js';
/**
* Create a real DOM element (vanilla JavaScript)
* This is the core function that replaces the h() virtual DOM function
*/
export function createElement(tag, props = null, ...children) {
const element = document.createElement(tag);
// Apply props
if (props) {
applyProps(element, props);
}
// Append children
appendChildren(element, children);
debugLog('DOM', `Created element: ${tag}`);
return element;
}
/**
* Create a text node
*/
export function createTextNode(text) {
return document.createTextNode(String(text));
}
/**
* Apply properties to a DOM element
*/
export function applyProps(element, props) {
for (const [key, value] of Object.entries(props)) {
if (value === null || value === undefined) {
continue;
}
// Handle event listeners
if (key.startsWith('on') && typeof value === 'function') {
const eventName = key.slice(2).toLowerCase();
element.addEventListener(eventName, value);
debugLog('DOM', `Added event listener: ${eventName}`);
}
// Handle class/className
else if (key === 'class' || key === 'className') {
element.className = value;
}
// Handle style
else if (key === 'style') {
if (typeof value === 'string') {
element.setAttribute('style', value);
}
else if (typeof value === 'object') {
// Apply styles with proper camelCase to kebab-case conversion
for (const [styleKey, styleValue] of Object.entries(value)) {
if (styleValue !== null && styleValue !== undefined) {
// Convert camelCase to kebab-case for CSS properties
const cssKey = styleKey.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
element.style[styleKey] = styleValue;
}
}
}
}
// Handle hover styles
else if (key === 'onHover' && typeof value === 'object') {
const hoverStyles = value;
element.addEventListener('mouseenter', () => {
for (const [styleKey, styleValue] of Object.entries(hoverStyles)) {
if (styleValue !== null && styleValue !== undefined) {
element.style[styleKey] = styleValue;
}
}
});
// Store original styles to restore on mouse leave
const originalStyles = {};
for (const styleKey of Object.keys(hoverStyles)) {
originalStyles[styleKey] = element.style[styleKey];
}
element.addEventListener('mouseleave', () => {
for (const [styleKey, styleValue] of Object.entries(originalStyles)) {
element.style[styleKey] = styleValue;
}
});
}
// Handle ref
else if (key === 'ref' && typeof value === 'object' && 'current' in value) {
value.current = element;
}
// Handle special properties
else if (key === 'value') {
element.value = value;
}
else if (key === 'checked') {
element.checked = value;
}
// Handle data attributes
else if (key.startsWith('data-')) {
element.setAttribute(key, String(value));
}
// Handle aria attributes
else if (key.startsWith('aria-')) {
element.setAttribute(key, String(value));
}
// Handle other attributes
else {
element.setAttribute(key, String(value));
}
}
}
/**
* Update properties on a DOM element
*/
export function updateProps(element, oldProps, newProps) {
// Remove old props
for (const key in oldProps) {
if (!(key in newProps)) {
removeProp(element, key, oldProps[key]);
}
}
// Add/update new props
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
// Remove old event listener if it's an event
if (key.startsWith('on') && typeof oldProps[key] === 'function') {
const eventName = key.slice(2).toLowerCase();
element.removeEventListener(eventName, oldProps[key]);
}
// Apply new prop
applyProps(element, { [key]: newProps[key] });
}
}
}
/**
* Remove a property from a DOM element
*/
export function removeProp(element, key, value) {
if (key.startsWith('on') && typeof value === 'function') {
const eventName = key.slice(2).toLowerCase();
element.removeEventListener(eventName, value);
}
else if (key === 'class' || key === 'className') {
element.className = '';
}
else if (key === 'style') {
element.removeAttribute('style');
}
else if (key !== 'ref') {
element.removeAttribute(key);
}
}
/**
* Append children to a DOM element
*/
export function appendChildren(parent, children) {
const flatChildren = children.flat(Infinity);
for (const child of flatChildren) {
if (child === null || child === undefined || child === false || child === true) {
continue;
}
if (typeof child === 'string' || typeof child === 'number') {
parent.appendChild(createTextNode(child));
}
else if (child instanceof HTMLElement || child instanceof SVGElement || child instanceof Text) {
parent.appendChild(child);
}
}
}
/**
* Replace all children of an element
*/
export function replaceChildren(parent, children) {
// Clear existing children
while (parent.firstChild) {
parent.removeChild(parent.firstChild);
}
// Append new children
appendChildren(parent, children);
}
/**
* Mount an element to a container
*/
export function mount(element, container) {
container.appendChild(element);
debugLog('DOM', 'Mounted element to container');
}
/**
* Unmount an element from its parent
*/
export function unmount(element) {
if (element.parentElement) {
element.parentElement.removeChild(element);
debugLog('DOM', 'Unmounted element');
}
}
/**
* Create a ref object for accessing DOM elements
*/
export function createRef() {
return { current: null };
}
/**
* Query selector helper
*/
export function $(selector, parent = document) {
return parent.querySelector(selector);
}
/**
* Query selector all helper
*/
export function $$(selector, parent = document) {
return Array.from(parent.querySelectorAll(selector));
}
/**
* Add class to element
*/
export function addClass(element, ...classNames) {
element.classList.add(...classNames);
}
/**
* Remove class from element
*/
export function removeClass(element, ...classNames) {
element.classList.remove(...classNames);
}
/**
* Toggle class on element
*/
export function toggleClass(element, className, force) {
element.classList.toggle(className, force);
}
/**
* Set styles on element
*/
export function setStyle(element, styles) {
Object.assign(element.style, styles);
}
/**
* Set attributes on element
*/
export function setAttributes(element, attrs) {
for (const [key, value] of Object.entries(attrs)) {
element.setAttribute(key, value);
}
}
/**
* Add event listener helper
*/
export function on(element, event, handler, options) {
element.addEventListener(event, handler, options);
return () => element.removeEventListener(event, handler, options);
}
/**
* Remove event listener helper
*/
export function off(element, event, handler) {
element.removeEventListener(event, handler);
}
//# sourceMappingURL=dom.js.map