juris
Version:
JavaScript Unified Reactive Interface Solution - The First and Only Non-blocking Reactive Platform, Architecturally Optimized for Next Generation Cutting-Edge Cross-Platform Application. Transforms web development through comprehensive object-first archit
1,126 lines (1,044 loc) • 167 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.88.2
* 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
* - Component lazy compilation
* - Non-Blocking Rendering
* - Global Non-Reactive State Management
* - SSR (Server-Side Rendering) and CSR (Client-Side Rendering) ready
* - Loading Status templating
* - Web Component support
* - SVG Support
* - Dual rendering mode, fine-grained or batch rendering
* - Dual Template Mode (HTML and Object VDOM)
* - CSS-in-JS Compilation
* - Supports innerHtml in Object VDOM for critical rendering requirements
*
* Performance:
* - Sub 3ms render on simple apps
* - Sub 10ms render on complex or large apps
* - Sub 20ms render on very complex or large apps
* 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.
* example VDOM Convention:
* return {
* {div:{className:'main', //note: static and short should be inline,
* 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
* ]
* }}//div.main
* }//return
*/
'use strict';
const jurisLinesOfCode = 3796; // Total lines of code in Juris
const jurisVersion = '0.88.2'; // Current version of Juris
const jurisMinifiedSize = '75.11 kB'; // Minified version of Juris
// Utilities
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;
};
// the leanest and sophisticated logger
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 },
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 = result?.then ? 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();
// State Manager
class StateManager {
constructor(initialState = {}, middleware = []) {
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();
// Manual batching properties
this.isBatching = false;
this.batchQueue = [];
this.batchedPaths = new Set();
}
reset() {
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 = {}) {
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) {
// Already in a batch, just execute callback
return callback();
}
this.beginBatch();
try {
const result = callback();
// Handle Promise-returning callbacks
if (result && typeof result.then === 'function') {
return result
.then(value => {
this.endBatch();
return value;
})
.catch(error => {
this.endBatch();
throw error;
});
}
// Synchronous callback
this.endBatch();
return result;
} catch (error) {
this.endBatch();
throw error;
}
}
beginBatch() {
console.debug(log.d('Manual batch started', {}, 'framework'));
this.isBatching = true;
this.batchQueue = [];
this.batchedPaths.clear();
}
endBatch() {
if (!this.isBatching) {
console.warn(log.w('endBatch() called without beginBatch()', {}, 'framework'));
return;
}
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) {
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) {
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;
// Collect all parent paths that need notification
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('.'));
}
});
// Notify each parent path only if it has subscribers
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) {
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) {
console.error(log.e('Middleware error', { path, error: error.message, middlewareName: middleware.name || 'anonymous' }, 'application'));
}
}
if (deepEquals(oldValue, finalValue)) {
console.debug(log.d('State unchanged, skipping update', { path }, 'framework'));
return;
}
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) {
console.error(log.e('External subscriber error:', error), 'application');
}
}
});
});
}
_triggerPathSubscribers(path) {
const subs = this.subscribers.get(path);
if (subs && subs.size > 0) {
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) {
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)) {
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();
}
}
// Headless Manager
class HeadlessManager {
constructor(juris) {
console.info(log.i('HeadlessManager initialized', {}, 'framework'));
this.juris = juris;
this.components = new Map();
this.instances = new Map();
this.context = {};
this.initQueue = new Set();
this.lifecycleHooks = new Map();
}
register(name, componentFn, options = {}) {
console.info(log.i('Headless component registered', { name, hasOptions: Object.keys(options).length > 0 }, 'framework'));
this.components.set(name, { fn: componentFn, options });
if (options.autoInit) this.initQueue.add(name);
}
initialize(name, props = {}) {
console.debug(log.d('Initializing headless component', { name, propsKeys: Object.keys(props) }, 'framework'));
const component = this.components.get(name);
if (!component) {
console.error(log.e('Headless component not found', { name }, 'framework'));
return null;
}
try {
const context = this.juris.createHeadlessContext();
const instance = component.fn(props, context);
if (!instance || typeof instance !== 'object') {
console.error(log.e('Invalid headless component instance', { name }, 'framework'));
return null;
}
console.info(log.i('Headless component initialized', { name, hasAPI: !!instance.api, hasHooks: !!instance.hooks }, 'framework'));
this.instances.set(name, instance);
if (instance.hooks) this.lifecycleHooks.set(name, instance.hooks);
if (instance.api) {
this.context[name] = instance.api;
if (!this.juris.headlessAPIs) this.juris.headlessAPIs = {};
this.juris.headlessAPIs[name] = instance.api;
this.juris._updateComponentContexts();
}
instance.hooks?.onRegister?.();
return instance;
} catch (error) {
console.error(log.e('Headless component initialization failed', { name, error: error.message }, 'framework'));
return null;
}
}
initializeQueued() {
this.initQueue.forEach(name => {
if (!this.instances.has(name)) {
const component = this.components.get(name);
this.initialize(name, component.options || {});
}
});
this.initQueue.clear();
}
getInstance(name) { return this.instances.get(name); }
getAPI(name) { return this.context[name]; }
getAllAPIs() { return { ...this.context }; }
reinitialize(name, props = {}) {
const instance = this.instances.get(name);
if (instance?.hooks?.onUnregister) {
try { instance.hooks.onUnregister(); } catch (error) { console.error(log.e(`Error in onUnregister for '${name}':`, error), 'framework'); }
}
if (this.context[name]) delete this.context[name];
if (this.juris.headlessAPIs?.[name]) delete this.juris.headlessAPIs[name];
this.instances.delete(name);
this.lifecycleHooks.delete(name);
return this.initialize(name, props);
}
cleanup() {
console.info(log.i('Cleaning up headless components', { instanceCount: this.instances.size }, 'framework'));
this.instances.forEach((instance, name) => {
if (instance.hooks?.onUnregister) {
try { instance.hooks.onUnregister(); } catch (error) { console.error(log.e(`Error in onUnregister for '${name}':`, error), 'framework'); }
}
});
this.instances.clear();
this.context = {};
this.lifecycleHooks.clear();
if (this.juris.headlessAPIs) this.juris.headlessAPIs = {};
}
getStatus() {
return {
registered: Array.from(this.components.keys()),
initialized: Array.from(this.instances.keys()),
queued: Array.from(this.initQueue),
apis: Object.keys(this.context)
};
}
}
// Component Manager
class ComponentManager {
constructor(juris) {
console.info(log.i('ComponentManager initialized', {}, 'framework'));
this.juris = juris;
this.components = new Map();
this.instances = new WeakMap();
this.componentCounters = new Map();
this.componentStates = new WeakMap();
this.asyncPlaceholders = new WeakMap();
this.asyncPropsCache = new Map();
}
register(name, componentFn) {
console.info(log.i('Component registered', { name }, 'application'));
this.components.set(name, componentFn);
}
create(name, props = {}) {
const componentFn = this.components.get(name);
if (!componentFn) {
console.error(log.e('Component not found', { name }, 'application'));
return null;
}
try {
if (this._hasAsyncProps(props)) {
console.debug(log.d('Component has async props', { name }, 'framework'));
return this._createWithAsyncProps(name, componentFn, props);
}
const { componentId, componentStates } = this._setupComponent(name);
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) {
console.error(log.e('Component creation failed. Did you forgot to use children:?', { name, error: error.message }, 'application'));
return this._createErrorElement(new Error(error.message + ' Did you forgot to use children:[]?'));
}
}
_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;
}
_hasAsyncProps(props) {
return Object.values(props).some(value => value?.then);
}
_createWithAsyncProps(name, componentFn, props) {
console.debug(log.d('Creating component with async props', { name }, 'framework'));
// Create a temporary element with the component name as ID for config lookup
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) {
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 => {
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) {
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 (result && typeof result === 'object') {
if (this._hasLifecycleHooks(result)) {
return this._createLifecycleComponent(result, name, props, componentStates);
}
if (typeof result.render === 'function' && !this._hasLifecycleHooks(result)) {
const container = document.createElement('div');
container.setAttribute('data-juris-reactive-render', name);
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 => {
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) {
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;
}
_handleAsyncRender(renderPromise, name, componentStates, indicator = null) {
const tempElement = document.createElement('div');
tempElement.id = name.toLowerCase().replace(/[^a-z0-9]/g, '-');
const placeholder = indicator ?
this.juris.domRenderer.render(indicator) :
this._createPlaceholder(`Loading ${name}...`, 'juris-async-loading', tempElement);
renderPromise.then(renderResult => {
try {
const element = this.juris.domRenderer.render(renderResult);
if (element && componentStates.size > 0) this.componentStates.set(element, componentStates);
if (placeholder.parentNode) placeholder.parentNode.replaceChild(element, placeholder);
} catch (error) {
this._replaceWithError(placeholder, error);
}
}).catch(error => this._replaceWithError(placeholder, error));
return placeholder;
}
_createLifecycleComponent(componentResult, name, props, componentStates) {
const instance = {
name, props,
hooks: componentResult.hooks || {},
api: componentResult.api || {},
render: componentResult.render
};
const renderResult = instance.render();
if (renderResult?.then) return this._handleAsyncLifecycleRender(promisify(renderResult), instance, componentStates);
const element = this.juris.domRenderer.render(renderResult);
if (element) {
this.instances.set(element, instance);
if (componentStates?.size > 0) this.componentStates.set(element, componentStates);
if (instance.hooks.onMount) {
setTimeout(() => {
try {
const mountResult = instance.hooks.onMount();
if (mountResult?.then) {
promisify(mountResult).catch(error => console.error(log.e(`Async onMount error in ${name}:`, error), 'application'));
}
} catch (error) {
console.error(log.e(`onMount error in ${name}:`, error), 'application');
}
}, 0);
}
}
return element;
}
_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 =>
console.error(log.e(`Async onMount error in ${instance.name}:`, error), 'application')
);
}
} catch (error) {
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;
}
updateInstance(element, newProps) {
const instance = this.instances.get(element);
if (!instance) return;
const oldProps = instance.props;
if (deepEquals(oldProps, newProps)) return;
if (this._hasAsyncProps(newProps)) {
this._resolveAsyncProps(newProps).then(resolvedProps => {
instance.props = resolvedProps;
this._performUpdate(instance, element, oldProps, resolvedProps);
}).catch(error => 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 => console.error(log.e(`Async onUpdate error in ${instance.name}:`, error), 'application'));
}
} catch (error) {
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 => console.error(log.e(`Async re-render error in ${instance.name}:`, error), 'application'));
} else {
this.juris.domRenderer.updateElementContent(element, renderResult);
}
} catch (error) {
console.error(log.e(`Re-render error in ${instance.name}:`, error), 'application');
}
}
cleanup(element) {
const instance = this.instances.get(element);
if (instance) 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 => console.error(log.e(`Async onUnmount error in ${instance.name}:`, error), 'application'));
}
} catch (error) {
console.error(log.e(`onUnmount error in ${instance.name}:`, error), 'application');
}
}
if (element._reactiveSubscriptions) {
element._reactiveSubscriptions.forEach(unsubscribe => {
try { unsubscribe(); } catch (error) {
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
};
}
}
// Enhanced DOMRenderer v0.87.0 with CSS Extraction
// Based on your existing code with CSS extraction seamlessly integrated
class DOMRenderer {
constructor(juris) {
console.info(log.i('DOMRenderer initialized', { renderMode: 'fine-grained' }, 'framework'));
this.juris = juris;
this.subscriptions = new WeakMap();
this.componentStack = [];
// CSS Extraction System - NEW
this.cssCache = new Map(); // styleHash -> { css, className }
this.injectedCSS = new Set(); // Track injected CSS rules
this.styleSheet = null; // Single stylesheet for extracted CSS
this.camelCaseRegex = /([A-Z])/g; // Pre-compiled for performance
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
};
// Pre-allocated reusable objects/arrays for hot paths
this.tempArray = [];
this.tempKeyParts = [];
// Touch handling constants
this.TOUCH_CONFIG = {
moveThreshold: 10,
timeThreshold: 300,
touchAction: 'manipulation',
tapHighlight: 'transparent',
touchCallout: 'none'
};
// Recycling configuration
this.RECYCLE_POOL_SIZE = 100;
}
setRenderMode(mode) {
if (['fine-grained', 'batch'].includes(mode)) {
this.renderMode = mode;
console.info(log.i('Render mode changed', { mode }, 'framework'));
} else {
console.warn(log.w('Invalid render mode', { mode }, 'application'));
}
}
getRenderMode() { return this.renderMode; }
isFineGrained() { return this.renderMode === 'fine-grained'; }
isBatchMode() { return this.renderMode === 'batch'; }
// Enhanced render method with CSS extraction
render(vnode, staticMode = false, componentName = null) {
// Handle primitives
if (typeof vnode === 'string' || typeof vnode === 'number') {
return document.createTextNode(String(vnode));
}
if (!vnode || typeof vnode !== 'object') return null;
if (Array.isArray(vnode)) {
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;
if (props.style && !staticMode && this.cssExtraction) {
const elementName = componentName || tagName;
const extraction = this.extractCSS(elementName, props.style);
if (extraction.className) {
modifiedProps = { ...props };
modifiedProps.className = this.combineClassNames(props.className, extraction.className);
if (extraction.reactiveStyle) {
modifiedProps.style = extraction.reactiveStyle;
} else {
delete modifiedProps.style;
}
}
}
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;
}
return this._createElementOptimized(tagName, modifiedProps, key, inheritedComponentName);
} catch (error) {
this.failureCount++;
if (this.failureCount >= this.maxFailures) this.renderMode =