@arnelirobles/rnxjs
Version:
Minimalist Vanilla JS component system with reactive data binding.
172 lines (167 loc) • 19 kB
JavaScript
var rnx=(()=>{var T=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var ce=Object.getOwnPropertyNames;var ie=Object.prototype.hasOwnProperty;var le=(e,t)=>()=>(e&&(t=e(e=0)),t);var I=(e,t)=>{for(var r in t)T(e,r,{get:t[r],enumerable:!0})},ue=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ce(t))!ie.call(e,o)&&o!==r&&T(e,o,{get:()=>t[o],enumerable:!(n=se(t,o))||n.enumerable});return e};var fe=e=>ue(T({},"__esModule",{value:!0}),e);var q={};I(q,{bindData:()=>k,unbindData:()=>pe});function j(e,t){try{return t.split(".").reduce((r,n)=>r?.[n],e)}catch(r){console.error(`[rnxJS] Error getting nested value for path "${t}":`,r);return}}function J(e,t,r){try{let n=t.split("."),o=n.pop(),a=n.reduce((s,i)=>((typeof s[i]!="object"||s[i]===null)&&(s[i]={}),s[i]),e);a[o]=r}catch(n){console.error(`[rnxJS] Error setting nested value for path "${t}":`,n)}}function de(e,t){let r=e.type;try{if(r==="number"){let n=parseFloat(t);return isNaN(n)?0:n}return r==="date"||r==="datetime-local"?t:r==="checkbox"?!!t:t}catch(n){return console.error(`[rnxJS] Error coercing value for type "${r}":`,n),t}}function N(e,t){if(!t)return null;let r=t.split("|");for(let n of r){let[o,a]=n.split(":");if(o==="required"&&(e==null||e===""))return"This field is required";if(o==="email"&&e&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(e)))return"Invalid email address";if(o==="numeric"&&e&&isNaN(Number(e)))return"Must be a number";if(o==="min"){let s=parseFloat(a);if(typeof e=="string"&&e.length<s)return`Must be at least ${s} characters`;if(typeof e=="number"&&e<s)return`Must be at least ${s}`}if(o==="max"){let s=parseFloat(a);if(typeof e=="string"&&e.length>s)return`Must be no more than ${s} characters`;if(typeof e=="number"&&e>s)return`Must be no more than ${s}`}if(o==="pattern")try{let s=new RegExp(a);if(e&&!s.test(String(e)))return"Invalid format"}catch{console.warn("[rnxJS] Invalid regex pattern in validation rule:",a)}}return null}function k(e=document,t=null){if(!e||typeof e.querySelectorAll!="function"){console.error("[rnxJS] bindData: rootElement must be a valid DOM element");return}if(!t){console.warn("[rnxJS] bindData called without a reactive state object. Skipping data binding.");return}if(typeof t.subscribe!="function"){console.error("[rnxJS] bindData: state must be a reactive state object with subscribe method");return}if(!t.errors)try{t.errors={}}catch{console.warn("[rnxJS] Could not initialize state.errors. Validation may not work.")}E.has(e)||E.set(e,[]);let r=E.get(e);e.querySelectorAll("[data-bind]").forEach(o=>{let a=o.getAttribute("data-bind");if(!a||typeof a!="string"){console.warn("[rnxJS] data-bind attribute is empty or invalid on element:",o);return}if(!/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(a)){console.warn(`[rnxJS] Invalid data-bind path "${a}" on element:`,o);return}let s=o.tagName==="INPUT"||o.tagName==="TEXTAREA"||o.tagName==="SELECT";try{if(s){let i=me(o,t,a);i&&r.push(i)}else{let i=be(o,t,a);i&&r.push(i)}}catch(i){console.error(`[rnxJS] Error setting up binding for path "${a}":`,i)}})}function pe(e){if(!E.has(e))return;E.get(e).forEach(r=>{try{r()}catch(n){console.error("[rnxJS] Error unsubscribing:",n)}}),E.delete(e)}function me(e,t,r){let n=e.type,o=e.getAttribute("data-rule"),a=j(t,r);if(a!==void 0&&(D(e,a,n),o&&t.errors)){let d=N(a,o);J(t.errors,r,d||"")}let s=n==="checkbox"||n==="radio"?"change":"input",i=d=>{try{let m=F(d.target);if(m=de(d.target,m),J(t,r,m),o&&t.errors){let c=N(m,o);J(t.errors,r,c||"")}}catch(m){console.error(`[rnxJS] Error handling input for path "${r}":`,m)}};e.addEventListener(s,i);let l=t.subscribe(r,d=>{try{if(F(e)!==d&&(D(e,d,n),o&&t.errors)){let c=N(d,o);J(t.errors,r,c||"")}}catch(m){console.error(`[rnxJS] Error updating input for path "${r}":`,m)}});return()=>{e.removeEventListener(s,i),l()}}function be(e,t,r){let n=j(t,r);return n!==void 0&&(e.textContent=n),t.subscribe(r,o=>{try{e.textContent=o??""}catch(a){console.error(`[rnxJS] Error updating display for path "${r}":`,a)}})}function F(e){let t=e.type;return t==="checkbox"?e.checked:t==="radio"?e.value:e.tagName==="SELECT"&&e.multiple?Array.from(e.selectedOptions).map(r=>r.value):e.value}function D(e,t,r){try{r==="checkbox"?e.checked=!!t:r==="radio"?e.checked=e.value===t:e.tagName==="SELECT"&&e.multiple?Array.from(e.options).forEach(n=>{n.selected=Array.isArray(t)&&t.includes(n.value)}):e.value=t??""}catch(n){console.error("[rnxJS] Error updating input value:",n)}}var E,R=le(()=>{E=new WeakMap});var M={};I(M,{Alert:()=>W,Badge:()=>U,Button:()=>P,Card:()=>Z,Checkbox:()=>H,Col:()=>_,Container:()=>X,FormGroup:()=>G,Input:()=>B,Modal:()=>K,Pagination:()=>z,Radio:()=>Q,Row:()=>Y,Select:()=>ee,Spinner:()=>te,Tabs:()=>re,Textarea:()=>ne,Toast:()=>oe,autoRegisterComponents:()=>V,bindData:()=>k,createComponent:()=>u,createReactiveState:()=>O,loadComponents:()=>L,registerComponent:()=>C});function u(e,t={},r=""){let n,o=document.createElement("div"),a={...t},s=null,i=null,l=null,d=a.children||null;function m(){try{if(o.innerHTML=e(a).trim(),n=o.firstElementChild,!n){console.error("[rnxJS] createComponent: templateFn must return valid HTML with a root element");let c=document.createElement("div");return c.textContent="Component rendering error",c.style.color="red",c.style.padding="10px",c.style.border="1px solid red",c}if(d&&n.querySelector("[data-slot]")){let c=n.querySelector("[data-slot]");Array.isArray(d)?d.forEach(p=>c.appendChild(p)):c.appendChild(d)}return n.refs={},n.querySelectorAll("[data-ref]").forEach(c=>{let p=c.getAttribute("data-ref");p&&(n.refs[p]=c)}),s&&setTimeout(()=>{try{i&&typeof i=="function"&&i();let c=s(n);c&&typeof c=="function"&&(i=c)}catch(c){console.error("[rnxJS] Error in useEffect:",c)}},0),n}catch(c){console.error("[rnxJS] Error rendering component:",c);let p=document.createElement("div");return p.textContent=`Component error: ${c.message}`,p.style.color="red",p.style.padding="10px",p.style.border="1px solid red",p}}return n=m(),n.setState=c=>{try{let p=n;a={...a,...c};let f=document.activeElement,b=null,x=0,y=0;f&&p.contains(f)&&(b=f.getAttribute("data-ref"),(f.tagName==="INPUT"||f.tagName==="TEXTAREA")&&(x=f.selectionStart||0,y=f.selectionEnd||0));let g=m();if(p.replaceWith(g),n=g,b&&n.refs&&n.refs[b]){let h=n.refs[b];requestAnimationFrame(()=>{if(h.focus(),h.tagName==="INPUT"||h.tagName==="TEXTAREA")try{h.setSelectionRange(x,y)}catch{}})}}catch(p){console.error("[rnxJS] Error in setState:",p)}},n.useEffect=c=>{if(typeof c!="function"){console.warn("[rnxJS] useEffect: argument must be a function");return}s=c},n.onUnmount=c=>{if(typeof c!="function"){console.warn("[rnxJS] onUnmount: argument must be a function");return}l=c},n.getState=()=>a,n.useState=(c,p)=>(a[c]===void 0&&(a[c]=p),[()=>a[c],x=>n.setState({[c]:x})]),n.destroy=()=>{try{i&&typeof i=="function"&&i(),l&&typeof l=="function"&&l(),i=null,l=null,s=null,n.refs&&(n.refs={})}catch(c){console.error("[rnxJS] Error in destroy:",c)}},n}function O(e={}){if(typeof e!="object"||e===null)throw new TypeError("[rnxJS] createReactiveState: initialState must be an object");let t=new Map,r=new WeakMap,n=new WeakSet,o=new Set;function a(f,b){if(typeof f!="string"||!f)return console.warn("[rnxJS] subscribe: path must be a non-empty string"),()=>{};if(typeof b!="function")return console.warn("[rnxJS] subscribe: callback must be a function"),()=>{};t.has(f)||t.set(f,new Set),t.get(f).add(b);let x=()=>{let y=t.get(f);y&&(y.delete(b),y.size===0&&t.delete(f)),o.delete(x)};return o.add(x),x}function s(){t.clear(),o.clear()}function i(){s(),r.clear?.()}function l(f,b){try{t.has(f)&&t.get(f).forEach(y=>{try{y(b)}catch(g){console.error(`[rnxJS] Error in subscriber for path "${f}":`,g)}});let x=f.split(".");for(let y=x.length-1;y>0;y--){let g=x.slice(0,y).join(".");if(t.has(g)){let h=d(p,g);t.get(g).forEach($=>{try{$(h)}catch(v){console.error(`[rnxJS] Error in subscriber for path "${g}":`,v)}})}}}catch(x){console.error(`[rnxJS] Error notifying subscribers for path "${f}":`,x)}}function d(f,b){try{return b.split(".").reduce((x,y)=>x?.[y],f)}catch(x){console.error(`[rnxJS] Error getting nested value for path "${b}":`,x);return}}let m=["push","pop","shift","unshift","splice","sort","reverse"];function c(f,b=""){if(typeof f!="object"||f===null)return f;if(r.has(f))return r.get(f);if(n.has(f))return console.warn(`[rnxJS] Circular reference detected at path "${b}". Skipping proxy creation.`),f;n.add(f);let x=Array.isArray(f),y={get(h,$){let v=h[$];if(typeof $!="string")return v;let w=b?`${b}.${$}`:$;return x&&m.includes($)?function(...A){let ae=Array.prototype[$].apply(h,A);return l(b,h),ae}:typeof v=="object"&&v!==null?c(v,w):v},set(h,$,v){if(typeof $!="string")return h[$]=v,!0;let w=h[$],A=b?`${b}.${$}`:$;return w!==v&&(h[$]=v,l(A,v)),!0}},g=new Proxy(f,y);return r.set(f,g),g}let p=c(e);return Object.defineProperty(p,"subscribe",{value:a,enumerable:!1,writable:!1,configurable:!1}),Object.defineProperty(p,"getNestedValue",{value:d,enumerable:!1,writable:!1,configurable:!1}),Object.defineProperty(p,"$unsubscribeAll",{value:s,enumerable:!1,writable:!1,configurable:!1}),Object.defineProperty(p,"$destroy",{value:i,enumerable:!1,writable:!1,configurable:!1}),p}var S={};function C(e,t){S[e]=t}function xe(e,t){try{return new Function("state",`
'use strict';
try {
return Boolean(${e});
} catch (e) {
console.error('[rnxJS] Error evaluating condition "${e}":', e.message);
return false;
}
`)(t)}catch(r){return console.error('[rnxJS] Invalid condition expression "${expression}":',r.message),!1}}function L(e=document,t=null){if(!e||typeof e.querySelectorAll!="function"){console.error("[rnxJS] loadComponents: root must be a valid DOM element");return}Object.keys(S).forEach(r=>{try{e.querySelectorAll(r).forEach(o=>{try{let a=S[r];if(typeof a!="function"){console.error(`[rnxJS] Component "${r}" is not a valid function`);return}let s={};for(let m of o.attributes){let c=m.name,p=m.value;if(c.startsWith("on")){console.warn(`[rnxJS] "${c}" should be passed as a JS function, not as a string. Skipping...`);continue}s[c]=p}let i=Array.from(o.childNodes).filter(m=>m.nodeType!==8);if(i.length&&(s.children=i),o.getAttribute("visible")==="false")return;let l=o.getAttribute("data-if");if(l&&!xe(l,t))return;let d=a(s);if(!d){console.error(`[rnxJS] Component "${r}" did not return a valid element`);return}o.replaceWith(d),L(d,t)}catch(a){console.error(`[rnxJS] Error loading component "${r}":`,a);let s=document.createElement("div");s.style.cssText="color: red; padding: 10px; border: 1px solid red; margin: 5px;",s.textContent=`Error loading component "${r}": ${a.message}`,o.replaceWith(s)}})}catch(n){console.error(`[rnxJS] Error processing component "${r}":`,n)}}),t&&Promise.resolve().then(()=>(R(),q)).then(({bindData:r})=>{try{r(e,t)}catch(n){console.error("[rnxJS] Error in bindData:",n)}}).catch(r=>{console.error("[rnxJS] Failed to load DataBinder:",r)})}function V(){let e=typeof window<"u"&&window.rnx||M;Object.entries(e).forEach(([t,r])=>{typeof r=="function"&&/^[A-Z]/.test(t)&&C(t,r)})}R();function P({label:e="",variant:t="primary",size:r="",block:n=!1,onclick:o,children:a}){let i=u(()=>`
<button
class="btn btn-${t} ${r?"btn-"+r:""} ${n==="true"?"w-100":""}"
type="button"
data-ref="btn"
>
${e}
<span data-slot></span>
</button>
`,{label:e,variant:t,size:r,block:n,children:a});return i.useEffect(()=>{if(o&&i.refs&&i.refs.btn){let l=o;return i.refs.btn.addEventListener("click",l),()=>{i.refs&&i.refs.btn&&i.refs.btn.removeEventListener("click",l)}}}),i}function B({type:e="text",name:t="",value:r="",placeholder:n="",required:o=!1,disabled:a=!1,onchange:s}){let l=u(()=>`
<input
class="form-control"
type="${e}"
name="${t}"
value="${r}"
placeholder="${n}"
${o?"required":""}
${a?"disabled":""}
data-ref="input"
/>
`,{type:e,name:t,value:r,placeholder:n,required:o,disabled:a});return l.useEffect(()=>{s&&l.refs.input.addEventListener("change",s)}),l}function W({message:e="",variant:t="primary",dismissible:r=!1}){return u(()=>`
<div class="alert alert-${t} ${r?"alert-dismissible fade show":""}" role="alert" data-ref="alert">
${e}
<span data-slot></span>
${r?'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>':""}
</div>
`,{message:e,variant:t,dismissible:r})}function U({label:e="",variant:t="secondary",pill:r=!1}){return u(()=>`
<span class="badge ${r==="true"?"rounded-pill":""} bg-${t}">
${e}
</span>
`,{label:e,variant:t,pill:r})}function Z({title:e="",subtitle:t="",footer:r="",children:n=""}){return u(()=>`
<div class="card">
${e||t?`
<div class="card-header">
${e?`<h5 class="card-title mb-0">${e}</h5>`:""}
${t?`<small class="text-muted">${t}</small>`:""}
</div>`:""}
<div class="card-body" data-slot></div>
${r?`<div class="card-footer text-muted">${r}</div>`:""}
</div>
`,{title:e,subtitle:t,footer:r,children:n})}function H({label:e="",name:t="",value:r="",checked:n=!1,disabled:o=!1,required:a=!1,onchange:s}){let l=u(()=>`
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
name="${t}"
value="${r}"
${n==="true"?"checked":""}
${o?"disabled":""}
${a?"required":""}
data-ref="checkbox"
/>
<label class="form-check-label">
${e}
</label>
</div>
`,{label:e,name:t,value:r,checked:n,disabled:o,required:a});return l.useEffect(()=>{s&&l.refs.checkbox.addEventListener("change",s)}),l}function _({size:e="",children:t="",alignSelf:r=""}){return u(()=>`
<div class="col${e?"-"+e:""} ${r?"align-self-"+r:""}" data-slot></div>
`,{size:e,children:t,alignSelf:r})}function X({fluid:e=!1,children:t=""}){return u(()=>`
<div class="${e==="true"?"container-fluid":"container"}" data-slot></div>
`,{fluid:e,children:t})}function G({label:e="",forId:t="",children:r=""}){return u(()=>`
<div class="mb-3">
${e?`<label class="form-label" for="${t}">${e}</label>`:""}
<div data-slot></div>
</div>
`,{label:e,forId:t,children:r})}function K({id:e="",title:t="",dismissable:r=!0,children:n="",footer:o=""}){return u(()=>`
<div class="modal fade" id="${e}" tabindex="-1" aria-labelledby="${e}-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
${t?`
<div class="modal-header">
<h5 class="modal-title" id="${e}-label">${t}</h5>
${r?'<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>':""}
</div>
`:""}
<div class="modal-body" data-slot></div>
${o?`
<div class="modal-footer">
${o}
</div>
`:""}
</div>
</div>
</div>
`,{id:e,title:t,children:n,footer:o})}function z({pages:e="[]",onpageclick:t}){let r=[];try{r=typeof e=="string"?JSON.parse(e):e}catch{}let o=u(()=>`
<nav>
<ul class="pagination">
${r.map(a=>`
<li class="page-item${a.active?" active":""}">
<a class="page-link" href="#" data-page="${a.value}">${a.label}</a>
</li>
`).join("")}
</ul>
</nav>
`,{pages:e});return o.useEffect(()=>{t&&o.querySelectorAll(".page-link").forEach(a=>{a.addEventListener("click",s=>{s.preventDefault(),t(s)})})}),o}function Q({label:e="",name:t="",value:r="",checked:n=!1,disabled:o=!1,required:a=!1,onchange:s}){let l=u(()=>`
<div class="form-check">
<input
class="form-check-input"
type="radio"
name="${t}"
value="${r}"
${n==="true"?"checked":""}
${o?"disabled":""}
${a?"required":""}
data-ref="radio"
/>
<label class="form-check-label">
${e}
</label>
</div>
`,{label:e,name:t,value:r,checked:n,disabled:o,required:a});return l.useEffect(()=>{s&&l.refs.radio.addEventListener("change",s)}),l}function Y({children:e="",justify:t="",align:r="",noGutters:n=!1}){return u(()=>`
<div class="row ${t?"justify-content-"+t:""} ${r?"align-items-"+r:""} ${n==="true"?"g-0":""}" data-slot></div>
`,{children:e,justify:t,align:r,noGutters:n})}function ee({name:e="",options:t="",value:r="",required:n=!1,disabled:o=!1,onchange:a}){let s=[];try{s=typeof t=="string"?JSON.parse(t):t}catch{s=[]}let l=u(()=>`
<select
class="form-select"
name="${e}"
${n?"required":""}
${o?"disabled":""}
data-ref="select"
>
${s.map(d=>`
<option value="${d.value}" ${d.value===r?"selected":""}>
${d.label}
</option>
`).join("")}
</select>
`,{name:e,options:t,value:r,required:n,disabled:o});return l.useEffect(()=>{a&&l.refs.select.addEventListener("change",a)}),l}function te({type:e="border",size:t="",variant:r="primary",label:n=""}){return u(()=>`
<div class="spinner-${e} text-${r} ${t?`spinner-${e}-${t}`:""}" role="status">
<span class="visually-hidden">${n||"Loading..."}</span>
</div>
`,{type:e,size:t,variant:r,label:n})}function re({children:e=""}){let r=u(()=>`
<div>
<ul class="nav nav-tabs" role="tablist" data-ref="nav"></ul>
<div class="tab-content" data-ref="content"></div>
</div>
`,{children:e});return r.useEffect(()=>{let n=r.querySelectorAll("[data-tab]"),o=r.refs.nav,a=r.refs.content;n.forEach((s,i)=>{let l=`tab-${i}`,d=s.getAttribute("title")||`Tab ${i+1}`,m=i===0,c=document.createElement("li");c.className="nav-item",c.innerHTML=`
<button class="nav-link ${m?"active":""}" data-bs-toggle="tab" data-bs-target="#${l}" type="button" role="tab">${d}</button>
`,o.appendChild(c),s.classList.add("tab-pane","fade",m?"show active":""),s.id=l,a.appendChild(s)})}),r}function ne({name:e="",value:t="",rows:r=4,placeholder:n="",required:o=!1,disabled:a=!1,onchange:s}){let l=u(()=>`
<textarea
class="form-control"
name="${e}"
rows="${r}"
placeholder="${n}"
${o?"required":""}
${a?"disabled":""}
data-ref="textarea"
>${t}</textarea>
`,{name:e,value:t,rows:r,placeholder:n,required:o,disabled:a});return l.useEffect(()=>{s&&l.refs.textarea.addEventListener("change",s)}),l}function oe({header:e="",body:t="",autohide:r=!0,delay:n=5e3}){let a=u(()=>`
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="${n}">
<div class="toast-header">
<strong class="me-auto">${e}</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${t}
</div>
</div>
`,{header:e,body:t,autohide:r,delay:n});return a.useEffect(()=>{bootstrap.Toast.getOrCreateInstance(a).show()}),a}return fe(M);})();
//# sourceMappingURL=rnx.global.js.map