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
JavaScript
/**
* 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;