@razen-core/zenweb
Version:
A minimalist TypeScript framework for building reactive web applications with no virtual DOM
214 lines • 6.15 kB
JavaScript
/**
* ZenWeb Virtual DOM Implementation
* Efficient diffing and patching algorithm
*/
/**
* Create a virtual node
*/
export function h(type, props, ...children) {
return {
type,
props: props || {},
children: normalizeChildren(children),
key: props?.key
};
}
/**
* Normalize children to handle various input types
*/
function normalizeChildren(children) {
return children.flat(Infinity).filter(child => {
return child !== null && child !== undefined && child !== false && child !== true;
});
}
/**
* Create a text virtual node
*/
export function createTextVNode(text) {
return {
type: 'text',
props: {},
children: [],
el: undefined
};
}
/**
* Mount a virtual node to the DOM
*/
export function mount(vnode, container) {
const el = createElement(vnode);
vnode.el = el;
container.appendChild(el);
}
/**
* Create a DOM element from a virtual node
*/
function createElement(vnode) {
if (typeof vnode === 'string' || typeof vnode === 'number') {
return document.createTextNode(String(vnode));
}
if (typeof vnode.type === 'function') {
// Component
const componentVNode = vnode.type(vnode.props);
return createElement(componentVNode);
}
// Regular element
const el = document.createElement(vnode.type);
// Set properties
if (vnode.props) {
for (const [key, value] of Object.entries(vnode.props)) {
setAttribute(el, key, value);
}
}
// Mount children
if (vnode.children) {
vnode.children.forEach(child => {
if (child) {
const childEl = createElement(child);
el.appendChild(childEl);
}
});
}
return el;
}
/**
* Set an attribute on a DOM element
*/
function setAttribute(el, key, value) {
if (key.startsWith('on') && typeof value === 'function') {
// Event listener
const eventName = key.slice(2).toLowerCase();
el.addEventListener(eventName, value);
}
else if (key === 'class' || key === 'className') {
el.className = value;
}
else if (key === 'style') {
if (typeof value === 'string') {
el.setAttribute('style', value);
}
else if (typeof value === 'object') {
Object.assign(el.style, value);
}
}
else if (key === 'value') {
el.value = value;
}
else if (key === 'checked') {
el.checked = value;
}
else if (key !== 'key') {
el.setAttribute(key, value);
}
}
/**
* Remove an attribute from a DOM element
*/
function removeAttribute(el, key, oldValue) {
if (key.startsWith('on') && typeof oldValue === 'function') {
const eventName = key.slice(2).toLowerCase();
el.removeEventListener(eventName, oldValue);
}
else if (key === 'class' || key === 'className') {
el.className = '';
}
else if (key === 'style') {
el.removeAttribute('style');
}
else if (key !== 'key') {
el.removeAttribute(key);
}
}
/**
* Patch (update) a DOM element with new virtual node
*/
export function patch(oldVNode, newVNode) {
if (!oldVNode.el)
return;
// Different types, replace entirely
if (oldVNode.type !== newVNode.type) {
const parent = oldVNode.el.parentElement;
if (parent) {
const newEl = createElement(newVNode);
newVNode.el = newEl;
parent.replaceChild(newEl, oldVNode.el);
}
return;
}
// Same type, update props and children
newVNode.el = oldVNode.el;
// Update props
const oldProps = oldVNode.props || {};
const newProps = newVNode.props || {};
// Remove old props
for (const key in oldProps) {
if (!(key in newProps)) {
removeAttribute(oldVNode.el, key, oldProps[key]);
}
}
// Add/update new props
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
setAttribute(oldVNode.el, key, newProps[key]);
}
}
// Update children
patchChildren(oldVNode, newVNode);
}
/**
* Patch children of a virtual node
*/
function patchChildren(oldVNode, newVNode) {
const oldChildren = oldVNode.children || [];
const newChildren = newVNode.children || [];
const el = oldVNode.el;
const commonLength = Math.min(oldChildren.length, newChildren.length);
// Patch common children
for (let i = 0; i < commonLength; i++) {
const oldChild = oldChildren[i];
const newChild = newChildren[i];
if (typeof oldChild === 'string' || typeof oldChild === 'number') {
if (typeof newChild === 'string' || typeof newChild === 'number') {
if (oldChild !== newChild) {
el.childNodes[i].textContent = String(newChild);
}
}
else {
const newEl = createElement(newChild);
el.replaceChild(newEl, el.childNodes[i]);
}
}
else if (typeof newChild === 'string' || typeof newChild === 'number') {
const textNode = document.createTextNode(String(newChild));
el.replaceChild(textNode, el.childNodes[i]);
}
else {
patch(oldChild, newChild);
}
}
// Add new children
if (newChildren.length > oldChildren.length) {
for (let i = commonLength; i < newChildren.length; i++) {
const child = newChildren[i];
if (child) {
const childEl = createElement(child);
el.appendChild(childEl);
}
}
}
// Remove old children
if (oldChildren.length > newChildren.length) {
for (let i = oldChildren.length - 1; i >= commonLength; i--) {
el.removeChild(el.childNodes[i]);
}
}
}
/**
* Unmount a virtual node from the DOM
*/
export function unmount(vnode) {
if (vnode.el && vnode.el.parentElement) {
vnode.el.parentElement.removeChild(vnode.el);
}
}
//# sourceMappingURL=vdom.js.map