UNPKG

@arnelirobles/rnxjs

Version:

Minimalist Vanilla JS component system with reactive data binding.

162 lines (143 loc) 4.96 kB
export function createComponent(templateFn, initialState = {}, styles = '') { let component; const el = document.createElement('div'); let currentState = { ...initialState }; let effectFn = null; let effectCleanup = null; let unmountCleanup = null; const children = currentState.children || null; function render() { try { el.innerHTML = templateFn(currentState).trim(); component = el.firstElementChild; if (!component) { console.error('[rnxJS] createComponent: templateFn must return valid HTML with a root element'); const errorDiv = document.createElement('div'); errorDiv.textContent = 'Component rendering error'; errorDiv.style.color = 'red'; errorDiv.style.padding = '10px'; errorDiv.style.border = '1px solid red'; return errorDiv; } if (children && component.querySelector('[data-slot]')) { const slot = component.querySelector('[data-slot]'); Array.isArray(children) ? children.forEach(c => slot.appendChild(c)) : slot.appendChild(children); } component.refs = {}; component.querySelectorAll('[data-ref]').forEach(el => { const name = el.getAttribute('data-ref'); if (name) { component.refs[name] = el; } }); if (effectFn) { setTimeout(() => { try { // Run cleanup from previous effect if exists if (effectCleanup && typeof effectCleanup === 'function') { effectCleanup(); } // Run effect and store cleanup function if returned const cleanup = effectFn(component); if (cleanup && typeof cleanup === 'function') { effectCleanup = cleanup; } } catch (error) { console.error('[rnxJS] Error in useEffect:', error); } }, 0); } return component; } catch (error) { console.error('[rnxJS] Error rendering component:', error); const errorDiv = document.createElement('div'); errorDiv.textContent = `Component error: ${error.message}`; errorDiv.style.color = 'red'; errorDiv.style.padding = '10px'; errorDiv.style.border = '1px solid red'; return errorDiv; } } component = render(); component.setState = (newState) => { try { const oldComp = component; currentState = { ...currentState, ...newState }; // Focus preservation logic const activeEl = document.activeElement; let focusRef = null; let selectionStart = 0; let selectionEnd = 0; if (activeEl && oldComp.contains(activeEl)) { focusRef = activeEl.getAttribute('data-ref'); // Save cursor position if it's an input/textarea if (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA') { selectionStart = activeEl.selectionStart || 0; selectionEnd = activeEl.selectionEnd || 0; } } const newComp = render(); oldComp.replaceWith(newComp); component = newComp; // Restore focus if (focusRef && component.refs && component.refs[focusRef]) { const elToFocus = component.refs[focusRef]; requestAnimationFrame(() => { elToFocus.focus(); if (elToFocus.tagName === 'INPUT' || elToFocus.tagName === 'TEXTAREA') { try { elToFocus.setSelectionRange(selectionStart, selectionEnd); } catch (e) { // Silently fail if setSelectionRange is not supported } } }); } } catch (error) { console.error('[rnxJS] Error in setState:', error); } }; component.useEffect = (fn) => { if (typeof fn !== 'function') { console.warn('[rnxJS] useEffect: argument must be a function'); return; } effectFn = fn; }; component.onUnmount = (fn) => { if (typeof fn !== 'function') { console.warn('[rnxJS] onUnmount: argument must be a function'); return; } unmountCleanup = fn; }; component.getState = () => currentState; component.useState = (key, initialValue) => { if (currentState[key] === undefined) currentState[key] = initialValue; const get = () => currentState[key]; const set = (val) => component.setState({ [key]: val }); return [get, set]; }; component.destroy = () => { try { // Run effect cleanup if (effectCleanup && typeof effectCleanup === 'function') { effectCleanup(); } // Run unmount cleanup if (unmountCleanup && typeof unmountCleanup === 'function') { unmountCleanup(); } // Clear references effectCleanup = null; unmountCleanup = null; effectFn = null; if (component.refs) { component.refs = {}; } } catch (error) { console.error('[rnxJS] Error in destroy:', error); } }; return component; }