@arnelirobles/rnxjs
Version:
Minimalist Vanilla JS component system with reactive data binding.
110 lines (94 loc) • 3.64 kB
JavaScript
import { registeredComponents } from './Registry.js';
/**
* Safely evaluate a condition expression with limited scope
* @param {string} expression - The expression to evaluate
* @param {Object} state - The reactive state to use as context
* @returns {boolean} - Result of the expression
*/
function safeEvaluateCondition(expression, state) {
try {
// Create a function with the state as the only accessible variable
// This is safer than eval() as it limits the scope
const fn = new Function('state', `
'use strict';
try {
return Boolean(${expression});
} catch (e) {
console.error('[rnxJS] Error evaluating condition "${expression}":', e.message);
return false;
}
`);
return fn(state);
} catch (error) {
console.error('[rnxJS] Invalid condition expression "${expression}":', error.message);
return false;
}
}
export function loadComponents(root = document, reactiveState = null) {
if (!root || typeof root.querySelectorAll !== 'function') {
console.error('[rnxJS] loadComponents: root must be a valid DOM element');
return;
}
Object.keys(registeredComponents).forEach(tag => {
try {
const elements = root.querySelectorAll(tag);
elements.forEach(el => {
try {
const ComponentFunc = registeredComponents[tag];
// Validate component function
if (typeof ComponentFunc !== 'function') {
console.error(`[rnxJS] Component "${tag}" is not a valid function`);
return;
}
const props = {};
for (let attr of el.attributes) {
const name = attr.name;
const value = attr.value;
if (name.startsWith('on')) {
console.warn(`[rnxJS] "${name}" should be passed as a JS function, not as a string. Skipping...`);
continue;
}
props[name] = value;
}
const children = Array.from(el.childNodes).filter(n => n.nodeType !== 8);
if (children.length) props.children = children;
if (el.getAttribute('visible') === 'false') return;
// Handle conditional rendering with safer evaluation
const condition = el.getAttribute('data-if');
if (condition) {
const shouldRender = safeEvaluateCondition(condition, reactiveState);
if (!shouldRender) return;
}
const comp = ComponentFunc(props);
if (!comp) {
console.error(`[rnxJS] Component "${tag}" did not return a valid element`);
return;
}
el.replaceWith(comp);
loadComponents(comp, reactiveState);
} catch (error) {
console.error(`[rnxJS] Error loading component "${tag}":`, error);
// Create error placeholder
const errorEl = document.createElement('div');
errorEl.style.cssText = 'color: red; padding: 10px; border: 1px solid red; margin: 5px;';
errorEl.textContent = `Error loading component "${tag}": ${error.message}`;
el.replaceWith(errorEl);
}
});
} catch (error) {
console.error(`[rnxJS] Error processing component "${tag}":`, error);
}
});
// Apply data binding after components are loaded (lazy import)
if (reactiveState) {
import('./DataBinder.js').then(({ bindData }) => {
try {
bindData(root, reactiveState);
} catch (error) {
console.error('[rnxJS] Error in bindData:', error);
}
}).catch(err => {
console.error('[rnxJS] Failed to load DataBinder:', err);
});
}
}