UNPKG

juris-test

Version:

JavaScript Unified Reactive Interface Solution - Browser-optimized version for script tags and CDN usage

1,085 lines (1,030 loc) 133 kB
/** * Juris (JavaScript Unified Reactive Interface Solution) * The First and Only Non-blocking Reactive Platform, Architecturally Optimized for Next Generation Cutting-Edge Cross-Platform Application. * Juris aims to eliminate build complexity from small to large applications. * Author: Resti Guay * Version: 0.9.0 * License: MIT * GitHub: https://github.com/jurisjs/juris * Website: https://jurisjs.com/ * NPM: https://www.npmjs.com/package/juris * Codepen: https://codepen.io/jurisauthor * Online Testing: https://jurisjs.com/tests/juris#pure#test#interface.html * Features: * - Temporal Independent * - Automcatic deep call stack branch aware dependency detection * - Smart Promise (Asynchronous) Handling for Non-Blocking Rendering * - Component lazy compilation * - Global Non-Reactive State Management * - SSR (Server-Side Rendering) ready and CSR (Client-Side Rendering) * - Loading Status templating * - Web Component support * - SVG Support * - Dual rendering mode, fine-grained or batch rendering * - Dual Template Mode (HTML and Object VDOM) * - supports innerHtml in Object VDOM for critical rendering requirements * - Anonymous Reactive Functions for children, component and layout rendering * - Advanced Reactive Management - arm() API to handle all your events with access to services and context. * IMPORTANT!!! Notes to AI: * 1. Reactivity works when getState is called from intended functional attributes and children. * 2. Use compress object structure and add labels into the end brackets for nested divs, tables, select, groups and forms * 3. Use getState third attribute with false value to skip subscription * 4. Components will not re-render until their parent triggeres re-render. * 5. All props and attributes can handle async/sync natively, * 6. Use service injection AMAP * 7. Define component as function and dont inject directly into Juris during instantiation. * 8. Use class instead of className * example VDOM Convention: * return { * {div:{class:'main', //note: static and short should be inline, use class instead of className especially when working with third party frameworks * text:()=>getState('reactive.text.value','Hello'),//note: reactive, should be new line * style:{color:'red', border:'solid 1px blue'},//note: still okay if in-line * children:[ * {button:{text:'static label', //note: another static and short should be inline, * onClick:()=>clickHandler() * }},//button * {input:{type:'text',min:'1', max:'10', value: () => juris.getState('counter.step', 1), //note: reactive value * oninput: (e) => { const newStep = parseInt(e.target.value) || 1; juris.setState('counter.step', Math.max(1, Math.min(10, newStep))); } * }},//input * ()=> juris.getState('counter.step', 1),//text node * ()=>{ * const step = juris.getState('counter.step', 1); * return {span:{text:`Current step is ${step}`}}; * }//span * ] * }}//div.main * }//return */ 'use strict'; const jurisLinesOfCode = 2907; const jurisVersion = '0.9.0'; const jurisMinifiedSize = '54 kB'; const isValidPath = path => typeof path === 'string' && path.trim().length > 0 && !path.includes('..'); const getPathParts = path => path.split('.').filter(Boolean); const deepEquals = (a, b) => { if (a === b) return true; if (a == null || b == null || typeof a !== typeof b) return false; if (typeof a === 'object') { if (Array.isArray(a) !== Array.isArray(b)) return false; const keysA = Object.keys(a), keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; return keysA.every(key => keysB.includes(key) && deepEquals(a[key], b[key])); } return false; }; const createLogger = () => { const s = []; const f = (m, c, cat) => { const msg = `${cat ? `[${cat}] ` : ''}${m}${c ? ` ${JSON.stringify(c)}` : ''}`; const logObj = { formatted: msg, message: m, context: c, category: cat, timestamp: Date.now() }; setTimeout(() => s.forEach(sub => sub(logObj)), 0); return logObj; }; return { log: { l: f, w: f, e: f, i: f, d: f, ei:true, ee:true, el:true, ew:true, ed:true }, sub: cb => s.push(cb), unsub: cb => s.splice(s.indexOf(cb), 1) }; }; const { log, logSub, logUnsub } = createLogger(); const createPromisify = () => { const activePromises = new Set(); let isTracking = false; const subscribers = new Set(); const checkAllComplete = () => { if (activePromises.size === 0 && subscribers.size > 0) { subscribers.forEach(callback => callback()); } }; const trackingPromisify = result => { const promise = typeof result?.then === "function" ? result : Promise.resolve(result); if (isTracking && promise !== result) { activePromises.add(promise); promise.finally(() => { activePromises.delete(promise); setTimeout(checkAllComplete, 0); }); } return promise; }; return { promisify: trackingPromisify, startTracking: () => { isTracking = true; activePromises.clear(); }, stopTracking: () => { isTracking = false; subscribers.clear(); }, onAllComplete: (callback) => { subscribers.add(callback); if (activePromises.size === 0) { setTimeout(callback, 0); } return () => subscribers.delete(callback); } }; }; const { promisify, startTracking, stopTracking, onAllComplete } = createPromisify(); class StateManager { constructor(initialState = {}, middleware = []) { log.ei && console.info(log.i('StateManager initialized', { initialStateKeys: Object.keys(initialState), middlewareCount: middleware.length }, 'framework')); this.state = { ...initialState }; this.middleware = [...middleware]; this.subscribers = new Map(); this.externalSubscribers = new Map(); this.currentTracking = null; this.isUpdating = false; this.initialState = JSON.parse(JSON.stringify(initialState)); this.maxUpdateDepth = 50; this.updateDepth = 0; this.currentlyUpdating = new Set(); this.isBatching = false; this.batchQueue = []; this.batchedPaths = new Set(); } reset() { log.ei && console.info(log.i('State reset to initial state', {}, 'framework')); if (this.isBatching) { this.batchQueue = []; this.batchedPaths.clear(); this.isBatching = false; } this.state = JSON.parse(JSON.stringify(this.initialState)); } getState(path, defaultValue = null, track = true) { if (!isValidPath(path)) return defaultValue; if (track) this.currentTracking?.add(path); const parts = getPathParts(path); let current = this.state; for (const part of parts) { if (current?.[part] === undefined) return defaultValue; current = current[part]; } return current; } setState(path, value, context = {}) { log.ed && console.debug(log.d('State change initiated', { path, hasValue: value !== undefined }, 'application')); if (!isValidPath(path) || this.#hasCircularUpdate(path)) return; if (this.isBatching) { this.#queueBatchedUpdate(path, value, context); return; } this.#setStateImmediate(path, value, context); } executeBatch(callback) { if (this.isBatching) { return callback(); } this.#beginBatch(); try { const result = callback(); if (result && typeof result.then === 'function') { return result .then(value => { this.#endBatch(); return value; }) .catch(error => { this.#endBatch(); throw error; }); } this.#endBatch(); return result; } catch (error) { this.#endBatch(); throw error; } } #beginBatch() { log.ed && console.debug(log.d('Manual batch started', {}, 'framework')); this.isBatching = true; this.batchQueue = []; this.batchedPaths.clear(); } #endBatch() { if (!this.isBatching) { log.ew && console.warn(log.w('endBatch() called without beginBatch()', {}, 'framework')); return; } log.ed && console.debug(log.d('Manual batch ending', { queuedUpdates: this.batchQueue.length }, 'framework')); this.isBatching = false; if (this.batchQueue.length === 0) return; this.#processBatchedUpdates(); } isBatchingActive() {return this.isBatching;} getBatchQueueSize() {return this.batchQueue.length;} clearBatch() { if (this.isBatching) { log.ei && console.info(log.i('Clearing current batch', { clearedUpdates: this.batchQueue.length }, 'framework')); this.batchQueue = []; this.batchedPaths.clear(); } } #queueBatchedUpdate(path, value, context) { this.batchQueue = this.batchQueue.filter(update => update.path !== path); this.batchQueue.push({ path, value, context, timestamp: Date.now() }); this.batchedPaths.add(path); } #processBatchedUpdates() { const updates = [...this.batchQueue]; this.batchQueue = []; this.batchedPaths.clear(); const pathGroups = new Map(); updates.forEach(update => pathGroups.set(update.path, update)); const wasUpdating = this.isUpdating; this.isUpdating = true; const appliedUpdates = []; pathGroups.forEach(update => { const oldValue = this.getState(update.path); let finalValue = update.value; for (const middleware of this.middleware) { try { const result = middleware({ path: update.path, oldValue, newValue: finalValue, context: update.context, state: this.state }); if (result !== undefined) finalValue = result; } catch (error) { log.ee && console.error(log.e('Middleware error in batch', { path: update.path, error: error.message }, 'application')); } } if (deepEquals(oldValue, finalValue)) return; const parts = getPathParts(update.path); let current = this.state; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (current[part] == null || typeof current[part] !== 'object') { current[part] = {}; } current = current[part]; } current[parts[parts.length - 1]] = finalValue; appliedUpdates.push({ path: update.path, oldValue, newValue: finalValue }); }); this.isUpdating = wasUpdating; const parentPaths = new Set(); appliedUpdates.forEach(({ path }) => { const parts = getPathParts(path); for (let i = 1; i <= parts.length; i++) { parentPaths.add(parts.slice(0, i).join('.')); } }); parentPaths.forEach(path => { if (this.subscribers.has(path)) this.#triggerPathSubscribers(path); if (this.externalSubscribers.has(path)) { this.externalSubscribers.get(path).forEach(({ callback, hierarchical }) => { try { callback(this.getState(path), null, path); } catch (error) { log.ee && console.error(log.e('External subscriber error:', error), 'application'); } }); } }); } #setStateImmediate(path, value, context = {}) { const oldValue = this.getState(path); let finalValue = value; for (const middleware of this.middleware) { try { const result = middleware({ path, oldValue, newValue: finalValue, context, state: this.state }); if (result !== undefined) finalValue = result; } catch (error) { log.ee && console.error(log.e('Middleware error', { path, error: error.message, middlewareName: middleware.name || 'anonymous' }, 'application')); } } if (deepEquals(oldValue, finalValue)) { log.ed && console.debug(log.d('State unchanged, skipping update', { path }, 'framework')); return; } log.ed && console.debug(log.d('State updated', { path, oldValue: typeof oldValue, newValue: typeof finalValue }, 'application')); const parts = getPathParts(path); let current = this.state; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (current[part] == null || typeof current[part] !== 'object') current[part] = {}; current = current[part]; } current[parts[parts.length - 1]] = finalValue; if (!this.isUpdating) { this.isUpdating = true; if (!this.currentlyUpdating) this.currentlyUpdating = new Set(); this.currentlyUpdating.add(path); this.#notifySubscribers(path, finalValue, oldValue); this.#notifyExternalSubscribers(path, finalValue, oldValue); this.currentlyUpdating.delete(path); this.isUpdating = false; } } subscribe(path, callback, hierarchical = true) { if (!this.externalSubscribers.has(path)) this.externalSubscribers.set(path, new Set()); const subscription = { callback, hierarchical }; this.externalSubscribers.get(path).add(subscription); return () => { const subs = this.externalSubscribers.get(path); if (subs) { subs.delete(subscription); if (subs.size === 0) this.externalSubscribers.delete(path); } }; } subscribeExact(path, callback) { return this.subscribe(path, callback, false); } subscribeInternal(path, callback) { if (!this.subscribers.has(path)) this.subscribers.set(path, new Set()); this.subscribers.get(path).add(callback); return () => { const subs = this.subscribers.get(path); if (subs) { subs.delete(callback); if (subs.size === 0) this.subscribers.delete(path); } }; } #notifySubscribers(path, newValue, oldValue) { this.#triggerPathSubscribers(path); const parts = getPathParts(path); for (let i = parts.length - 1; i > 0; i--) { this.#triggerPathSubscribers(parts.slice(0, i).join('.')); } const prefix = path ? path + '.' : ''; const allPaths = new Set([...this.subscribers.keys(), ...this.externalSubscribers.keys()]); allPaths.forEach(subscriberPath => { if (subscriberPath.startsWith(prefix) && subscriberPath !== path) { this.#triggerPathSubscribers(subscriberPath); } }); } #notifyExternalSubscribers(changedPath, newValue, oldValue) { this.externalSubscribers.forEach((subscriptions, subscribedPath) => { subscriptions.forEach(({ callback, hierarchical }) => { const shouldNotify = hierarchical ? (changedPath === subscribedPath || changedPath.startsWith(subscribedPath + '.')) : changedPath === subscribedPath; if (shouldNotify) { try { callback(newValue, oldValue, changedPath); } catch (error) { log.ee && console.error(log.e('External subscriber error:', error), 'application'); } } }); }); } #triggerPathSubscribers(path) { const subs = this.subscribers.get(path); if (subs && subs.size > 0) { log.ed && console.debug(log.d('Triggering subscribers', { path, subscriberCount: subs.size }, 'framework')); new Set(subs).forEach(callback => { let oldTracking try { oldTracking = this.currentTracking; const newTracking = new Set(); this.currentTracking = newTracking; callback(); this.currentTracking = oldTracking; newTracking.forEach(newPath => { const existingSubs = this.subscribers.get(newPath); if (!existingSubs || !existingSubs.has(callback)) { this.subscribeInternal(newPath, callback); } }); } catch (error) { log.ee && console.error(log.e('Subscriber error:', error), 'application'); this.currentTracking = oldTracking; } }); } } #hasCircularUpdate(path) { if (!this.currentlyUpdating) this.currentlyUpdating = new Set(); if (this.currentlyUpdating.has(path)) { log.ew && console.warn(log.w('Circular dependency detected', { path }, 'framework')); return true; } return false; } startTracking() { const dependencies = new Set(); this.currentTracking = dependencies; return dependencies; } endTracking() { const tracking = this.currentTracking; this.currentTracking = null; return tracking || new Set(); } } class ComponentManager { constructor(juris) { log.ei && console.info(log.i('ComponentManager initialized', {}, 'framework')); this.juris = juris; this.components = new Map(); this.instances = new WeakMap(); this.namedComponents = new Map(); this.componentCounters = new Map(); this.componentStates = new WeakMap(); this.asyncPlaceholders = new WeakMap(); this.asyncPropsCache = new Map(); } register(name, componentFn) { log.ei && console.info(log.i('Component registered', { name }, 'application')); this.components.set(name, componentFn); } create(name, props = {}) { const componentFn = this.components.get(name); if (!componentFn) { log.ee && console.error(log.e('Component not found', { name }, 'application')); return null; } try { if (this.juris.domRenderer._hasAsyncProps(props)) { log.ed && console.debug(log.d('Component has async props', { name }, 'framework')); return this.#createWithAsyncProps(name, componentFn, props); } const { componentId, componentStates } = this.#setupComponent(name); log.ed && console.debug(log.d('Component setup complete', { name, componentId, stateCount: componentStates.size }, 'framework')); const context = this.#createComponentContext(componentId, componentStates); const result = componentFn(props, context); if (result?.then) return this.#handleAsyncComponent(promisify(result), name, props, componentStates); return this.#processComponentResult(result, name, props, componentStates); } catch (error) { log.ee && console.error(log.e('Component creation failed!', { name, error: error.message }, 'application')); return this.#createErrorElement(new Error(error.message)); } } #setupComponent(name) { if (!this.componentCounters.has(name)) this.componentCounters.set(name, 0); const instanceIndex = this.componentCounters.get(name) + 1; this.componentCounters.set(name, instanceIndex); const componentId = `${name}#${instanceIndex}`; const componentStates = new Set(); return { componentId, componentStates }; } #createComponentContext(componentId, componentStates) { const context = this.juris.createContext(); context.newState = (key, initialValue) => { const statePath = `##local.${componentId}.${key}`; if (this.juris.stateManager.getState(statePath, Symbol('not-found')) === Symbol('not-found')) { this.juris.stateManager.setState(statePath, initialValue); } componentStates.add(statePath); return [ () => this.juris.stateManager.getState(statePath, initialValue), value => this.juris.stateManager.setState(statePath, value) ]; }; return context; } #createWithAsyncProps(name, componentFn, props) { log.ed && console.debug(log.d('Creating component with async props', { name }, 'framework')); const tempElement = document.createElement('div'); tempElement.id = name.toLowerCase().replace(/[^a-z0-9]/g, '-'); const placeholder = this._createPlaceholder(`Loading ${name}...`, 'juris-async-props-loading', tempElement); this.asyncPlaceholders.set(placeholder, { name, props, type: 'async-props' }); this.#resolveAsyncProps(props).then(resolvedProps => { try { const realElement = this.#createSyncComponent(name, componentFn, resolvedProps); if (realElement && placeholder.parentNode) { placeholder.parentNode.replaceChild(realElement, placeholder); } this.asyncPlaceholders.delete(placeholder); } catch (error) { this.#replaceWithError(placeholder, error); } }).catch(error => this.#replaceWithError(placeholder, error)); return placeholder; } async #resolveAsyncProps(props) { const cacheKey = this.#generateCacheKey(props); const cached = this.asyncPropsCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < 5000) return cached.props; const resolved = {}; for (const [key, value] of Object.entries(props)) { if (value?.then) { try { resolved[key] = await value; } catch (error) { resolved[key] = { __asyncError: error.message }; } } else { resolved[key] = value; } } this.asyncPropsCache.set(cacheKey, { props: resolved, timestamp: Date.now() }); return resolved; } #generateCacheKey(props) { return JSON.stringify(props, (key, value) => value?.then ? '[Promise]' : value); } #createSyncComponent(name, componentFn, props) { const { componentId, componentStates } = this.#setupComponent(name); const context = this.#createComponentContext(componentId, componentStates); const result = componentFn(props, context); if (result?.then) return this.#handleAsyncComponent(promisify(result), name, props, componentStates); return this.#processComponentResult(result, name, props, componentStates); } #handleAsyncComponent(resultPromise, name, props, componentStates) { log.ed && console.debug(log.d('Handling async component', { name }, 'framework')); const tempElement = document.createElement('div'); tempElement.id = name.toLowerCase().replace(/[^a-z0-9]/g, '-'); const placeholder = this._createPlaceholder(`Loading ${name}...`, 'juris-async-loading', tempElement); this.asyncPlaceholders.set(placeholder, { name, props, componentStates }); resultPromise.then(result => { log.ed && console.debug(log.d('Async component resolved', { name }, 'framework')); try { const realElement = this.#processComponentResult(result, name, props, componentStates); if (realElement && placeholder.parentNode) { placeholder.parentNode.replaceChild(realElement, placeholder); } this.asyncPlaceholders.delete(placeholder); } catch (error) { log.ee && console.error(log.e('Async component failed', { name, error: error.message }, 'application')); this.#replaceWithError(placeholder, error); } }).catch(error => this.#replaceWithError(placeholder, error)); return placeholder; } #processComponentResult(result, name, props, componentStates) { if (Array.isArray(result)) { const fragment = document.createDocumentFragment(); const virtualContainer = { _isVirtual: true, _fragment: fragment, _componentName: name, _componentProps: props, appendChild: (child) => fragment.appendChild(child), removeChild: (child) => { if (child.parentNode === fragment) { fragment.removeChild(child); } }, replaceChild: (newChild, oldChild) => { if (oldChild.parentNode === fragment) { fragment.replaceChild(newChild, oldChild); } }, get children() { return Array.from(fragment.childNodes); }, get parentNode() { return null; }, textContent: '' }; Object.defineProperty(virtualContainer, 'textContent', { set(value) { while (fragment.firstChild) { fragment.removeChild(fragment.firstChild); } if (value) { fragment.appendChild(document.createTextNode(value)); } }, get() { return ''; } }); const subscriptions = []; this.juris.domRenderer._handleChildrenFineGrained(virtualContainer, result, subscriptions); fragment._jurisComponent = { name, props, virtual: virtualContainer, cleanup: () => { subscriptions.forEach(unsub => { try { unsub(); } catch(e) {} }); } }; if (componentStates?.size > 0) { fragment._jurisComponentStates = componentStates; } return fragment; } if (result && typeof result === 'object') { if (this.#hasLifecycleHooks(result)) { const instance = { name, props, hooks: result.hooks || { onMount: result.onMount, onUpdate: result.onUpdate, onUnmount: result.onUnmount }, api: result.api || {}, render: result.render }; const renderResult = instance.render ? instance.render() : result; if (renderResult?.then) { return this.#handleAsyncLifecycleRender(promisify(renderResult), instance, componentStates); } const element = this.juris.domRenderer.render(renderResult, false, name); if (element) { this.instances.set(element, instance); if (componentStates?.size > 0) { this.componentStates.set(element, componentStates); } if (instance.api && Object.keys(instance.api).length > 0) { this.namedComponents.set(name, { element, instance }); } if (instance.hooks.onMount) { setTimeout(() => { try { const mountResult = instance.hooks.onMount(); if (mountResult?.then) { promisify(mountResult).catch(error => log.ee && console.error(log.e(`Async onMount error in ${name}:`, error), 'application') ); } } catch (error) { log.ee && console.error(log.e(`onMount error in ${name}:`, error), 'application'); } }, 0); } } return element; } if (typeof result.render === 'function' && !this.#hasLifecycleHooks(result)) { const container = document.createElement('div'); container.setAttribute('data-juris-reactive-render', name); const componentData = { name, api: result.api || {}, render: result.render }; this.instances.set(container, componentData); if (result.api) { this.namedComponents.set(name, { element: container, instance: componentData }); } const updateRender = () => { try { const renderResult = result.render(); if (renderResult?.then) { container.innerHTML = '<div class="juris-loading">Loading...</div>'; promisify(renderResult).then(resolvedResult => { container.innerHTML = ''; const element = this.juris.domRenderer.render(resolvedResult); if (element) container.appendChild(element); }).catch(error => { log.ee && console.error(`Async render error for ${name}:`, error); container.innerHTML = `<div class="juris-error">Error: ${error.message}</div>`; }); return; } const children = Array.from(container.children); children.forEach(child => this.cleanup(child)); container.innerHTML = ''; const element = this.juris.domRenderer.render(renderResult); if (element) container.appendChild(element); } catch (error) { log.ee && console.error(`Error in reactive render for ${name}:`, error); container.innerHTML = `<div class="juris-error">Render Error: ${error.message}</div>`; } }; const subscriptions = []; this.juris.domRenderer._createReactiveUpdate(container, updateRender, subscriptions); if (subscriptions.length > 0) { this.juris.domRenderer.subscriptions.set(container, { subscriptions, eventListeners: [] }); } if (componentStates?.size > 0) { this.componentStates.set(container, componentStates); } return container; } const keys = Object.keys(result); if (keys.length === 1 && typeof keys[0] === 'string' && keys[0].length > 0) { const element = this.juris.domRenderer.render(result, false, name); if (element && componentStates.size > 0) this.componentStates.set(element, componentStates); return element; } } const element = this.juris.domRenderer.render(result); if (element && componentStates.size > 0) this.componentStates.set(element, componentStates); return element; } #hasLifecycleHooks(result) { return result.hooks && (result.hooks.onMount || result.hooks.onUpdate || result.hooks.onUnmount) || result.onMount || result.onUpdate || result.onUnmount; } #handleAsyncLifecycleRender(renderPromise, instance, componentStates) { const tempElement = document.createElement('div'); tempElement.id = instance.name.toLowerCase().replace(/[^a-z0-9]/g, '-'); const placeholder = this._createPlaceholder(`Loading ${instance.name}...`, 'juris-async-lifecycle', tempElement); renderPromise.then(renderResult => { try { const element = this.juris.domRenderer.render(renderResult); if (element) { this.instances.set(element, instance); if (componentStates?.size > 0) { this.componentStates.set(element, componentStates); } if (placeholder.parentNode) { placeholder.parentNode.replaceChild(element, placeholder); } if (instance.hooks.onMount) { setTimeout(() => { try { const mountResult = instance.hooks.onMount(); if (mountResult?.then) { promisify(mountResult).catch(error => log.ee && console.error(log.e(`Async onMount error in ${instance.name}:`, error), 'application') ); } } catch (error) { log.ee && console.error(log.e(`onMount error in ${instance.name}:`, error), 'application'); } }, 0); } } } catch (error) { this.#replaceWithError(placeholder, error); } }).catch(error => this.#replaceWithError(placeholder, error)); return placeholder; } getComponent(name) {return this.namedComponents.get(name)?.instance || null;} getComponentAPI(name) {return this.namedComponents.get(name)?.instance?.api || null;} getComponentElement(name) {return this.namedComponents.get(name)?.element || null;} getNamedComponents() {return Array.from(this.namedComponents.keys());} updateInstance(element, newProps) { const instance = this.instances.get(element); if (!instance) return; const oldProps = instance.props; if (deepEquals(oldProps, newProps)) return; if (this.juris.domRenderer._hasAsyncProps(newProps)) { this.#resolveAsyncProps(newProps).then(resolvedProps => { instance.props = resolvedProps; this.#performUpdate(instance, element, oldProps, resolvedProps); }).catch(error => log.ee && console.error(log.e(`Error updating async props for ${instance.name}:`, error), 'application')); } else { instance.props = newProps; this.#performUpdate(instance, element, oldProps, newProps); } } #performUpdate(instance, element, oldProps, newProps) { if (instance.hooks.onUpdate) { try { const updateResult = instance.hooks.onUpdate(oldProps, newProps); if (updateResult?.then) { promisify(updateResult).catch(error => log.ee && console.error(log.e(`Async onUpdate error in ${instance.name}:`, error), 'application')); } } catch (error) { log.ee && console.error(log.e(`onUpdate error in ${instance.name}:`, error), 'application'); } } try { const renderResult = instance.render(); const normalizedRenderResult = promisify(renderResult); if (normalizedRenderResult !== renderResult) { normalizedRenderResult.then(newContent => { this.juris.domRenderer.updateElementContent(element, newContent); }).catch(error => log.ee && console.error(log.e(`Async re-render error in ${instance.name}:`, error), 'application')); } else { this.juris.domRenderer.updateElementContent(element, renderResult); } } catch (error) { log.ee && console.error(log.e(`Re-render error in ${instance.name}:`, error), 'application'); } } cleanup(element) { if (element instanceof DocumentFragment) { if (element._jurisComponent?.cleanup) { element._jurisComponent.cleanup(); } if (element._jurisComponentStates) { element._jurisComponentStates.forEach(statePath => { // Clean up component states const pathParts = statePath.split('.'); let current = this.juris.stateManager.state; for (let i = 0; i < pathParts.length - 1; i++) { if (current[pathParts[i]]) current = current[pathParts[i]]; else return; } delete current[pathParts[pathParts.length - 1]]; }); } return; } const instance = this.instances.get(element); if (instance) log.ed && console.debug(log.d('Cleaning up component', { name: instance.name }, 'framework')); if (instance?.hooks?.onUnmount) { try { const unmountResult = instance.hooks.onUnmount(); if (unmountResult?.then) { promisify(unmountResult).catch(error => log.ee && console.error(log.e(`Async onUnmount error in ${instance.name}:`, error), 'application')); } } catch (error) { log.ee && console.error(log.e(`onUnmount error in ${instance.name}:`, error), 'application'); } } if (element._reactiveSubscriptions) { element._reactiveSubscriptions.forEach(unsubscribe => { try { unsubscribe(); } catch (error) { log.ew && console.warn('Error cleaning up reactive subscription:', error); } }); element._reactiveSubscriptions = []; } const states = this.componentStates.get(element); if (states) { states.forEach(statePath => { const pathParts = statePath.split('.'); let current = this.juris.stateManager.state; for (let i = 0; i < pathParts.length - 1; i++) { if (current[pathParts[i]]) current = current[pathParts[i]]; else return; } delete current[pathParts[pathParts.length - 1]]; }); this.componentStates.delete(element); } if (this.asyncPlaceholders.has(element)) this.asyncPlaceholders.delete(element); this.instances.delete(element); } _createPlaceholder(text, className, element = null) { const config = this.juris.domRenderer._getPlaceholderConfig(element); const placeholder = document.createElement('div'); placeholder.className = config.className; placeholder.textContent = config.text; if (config.style) placeholder.style.cssText = config.style; return placeholder; } #createErrorElement(error) { const element = document.createElement('div'); element.style.cssText = 'color: red; border: 1px solid red; padding: 8px; background: #ffe6e6;'; element.textContent = `Component Error: ${error.message}`; return element; } #replaceWithError(placeholder, error) { const errorElement = this.#createErrorElement(error); if (placeholder.parentNode) placeholder.parentNode.replaceChild(errorElement, placeholder); this.asyncPlaceholders.delete(placeholder); } clearAsyncPropsCache() { this.asyncPropsCache.clear(); } getAsyncStats() { return { registeredComponents: this.components.size, cachedAsyncProps: this.asyncPropsCache.size }; } } class DOMRenderer { constructor(juris) { log.ei && console.info(log.i('DOMRenderer initialized', { renderMode: 'fine-grained' }, 'framework')); this.juris = juris; this.subscriptions = new WeakMap(); this.componentStack = []; this.cssCache = new Map(); this.injectedCSS = new Set(); this.styleSheet = null; this.camelCaseRegex = /([A-Z])/g; this.eventMap = { ondoubleclick: 'dblclick', onmousedown: 'mousedown', onmouseup: 'mouseup', onmouseover: 'mouseover', onmouseout: 'mouseout', onmousemove: 'mousemove', onkeydown: 'keydown', onkeyup: 'keyup', onkeypress: 'keypress', onfocus: 'focus', onblur: 'blur', onchange: 'change', oninput: 'input', onsubmit: 'submit', onload: 'load', onresize: 'resize', onscroll: 'scroll' }; this.BOOLEAN_ATTRS = new Set(['disabled', 'checked', 'selected', 'readonly', 'multiple', 'autofocus', 'autoplay', 'controls', 'hidden', 'loop', 'open', 'required', 'reversed', 'itemScope']); this.PRESERVED_ATTRIBUTES = new Set(['viewBox', 'preserveAspectRatio', 'textLength', 'gradientUnits', 'gradientTransform', 'spreadMethod', 'patternUnits', 'patternContentUnits', 'patternTransform', 'clipPath', 'crossOrigin', 'xmlns', 'xmlns:xlink', 'xlink:href']); this.SVG_ELEMENTS = new Set([ 'svg', 'g', 'defs', 'desc', 'metadata', 'title', 'circle', 'ellipse', 'line', 'polygon', 'polyline', 'rect', 'path', 'text', 'tspan', 'textPath', 'marker', 'pattern', 'clipPath', 'mask', 'image', 'switch', 'foreignObject', 'linearGradient', 'radialGradient', 'stop', 'animate', 'animateMotion', 'animateTransform', 'set', 'use', 'symbol' ]); this.KEY_PROPS = ['id', 'className', 'text']; this.SKIP_ATTRS = new Set(['children', 'key']); this.ATTRIBUTES_TO_KEEP = new Set(['id', 'data-juris-key']); this.elementCache = new Map(); this.recyclePool = new Map(); this.renderMode = 'fine-grained'; this.failureCount = 0; this.maxFailures = 3; this.asyncCache = new Map(); this.asyncPlaceholders = new WeakMap(); this.placeholderConfigs = new Map(); this.defaultPlaceholder = { className: 'juris-async-loading', style: 'padding: 8px; background: #f0f0f0; border: 1px dashed #ccc; opacity: 0.7;', text: 'Loading...', children: null }; this.tempArray = []; this.tempKeyParts = []; this.TOUCH_CONFIG = { moveThreshold: 10, timeThreshold: 300, touchAction: 'manipulation', tapHighlight: 'transparent', touchCallout: 'none' }; this.RECYCLE_POOL_SIZE = 100; } setRenderMode(mode) { if (['fine-grained', 'batch'].includes(mode)) { this.renderMode = mode; log.ei && console.info(log.i('Render mode changed', { mode }, 'framework')); } else { log.ew && console.warn(log.w('Invalid render mode', { mode }, 'application')); } } getRenderMode() { return this.renderMode; } isFineGrained() { return this.renderMode === 'fine-grained'; } isBatchMode() { return this.renderMode === 'batch'; } render(vnode, staticMode = false, componentName = null) { if (typeof vnode === 'string' || typeof vnode === 'number') { return document.createTextNode(String(vnode)); } if (!vnode || typeof vnode !== 'object') return null; if (Array.isArray(vnode)) { const hasReactiveFunctions = !staticMode && vnode.some(item => typeof item === 'function'); if (hasReactiveFunctions) { const fragment = document.createDocumentFragment(); const proxyElement = { _isProxy: true, _fragment: fragment, _subscriptions: [], appendChild: (child) => fragment.appendChild(child), removeChild: (child) => fragment.removeChild(child), replaceChild: (newChild, oldChild) => { if (oldChild.parentNode === fragment) { fragment.replaceChild(newChild, oldChild); } }, get parentNode() { return null; }, get children() { return fragment.children || [] }, textContent: { set(value) { while (fragment.firstChild) { fragment.removeChild(fragment.firstChild); } if (value) { fragment.appendChild(document.createTextNode(value)); } } } }; const subscriptions = []; this.#handleReactiveFragmentChildren(fragment, vnode, subscriptions); if (subscriptions.length > 0) { fragment._jurisCleanup = () => { subscriptions.forEach(unsub => { try { unsub(); } catch(e) {} }); }; } return fragment; } const fragment = document.createDocumentFragment(); for (let i = 0; i < vnode.length; i++) { const childElement = this.render(vnode[i], staticMode, componentName); if (childElement) fragment.appendChild(childElement); } return fragment; } const tagName = Object.keys(vnode)[0]; const props = vnode[tagName] || {}; if (!staticMode && this.componentStack.includes(tagName)) { return this.#createDeepRecursionErrorElement(tagName, this.componentStack); } if (!staticMode && this.juris.componentManager.components.has(tagName)) { const parentTracking = this.juris.stateManager.currentTracking; this.juris.stateManager.currentTracking = null; this.componentStack.push(tagName); const result = this.juris.componentManager.create(tagName, props); this.componentStack.pop(); this.juris.stateManager.currentTracking = parentTracking; return result; } if (!staticMode && /^[A-Z]/.test(tagName)) { return this.#createComponentErrorElement(tagName); } if (typeof tagName !== 'string' || tagName.length === 0) return null; let modifiedProps = props; // NEW: Let custom CSS extractor handle all the logic if (props.style && !staticMode && this.customCSSExtractor) { const elementName = componentName || tagName; modifiedProps = this.customCSSExtractor.processProps(props, elementName, this); } const inheritedComponentName = componentName || (props.style ? tagName : null); if (staticMode) { return this.#createElementStatic(tagName, modifiedProps, inheritedComponentName); } if (this.renderMode === 'fine-grained') { return this.#createElementFineGrained(tagName, modifiedProps, inheritedComponentName); } try { const key = modifiedProps.key || this.#generateKey(tagName, modifiedProps); const cachedElement = this.elementCache.get(key); if (cachedElement && this.#canReuseElement(cachedElement, tagName, modifiedProps)) { this.#updateElementProperties(cachedElement, modifiedProps); return cachedElement;