├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── dist ├── components.cjs ├── components.js ├── mini.cjs ├── mini.css ├── mini.js ├── mini_dom-CfPyC6jy.cjs ├── mini_dom-D28UFCQZ.js ├── router.cjs ├── router.js ├── store.cjs └── store.js ├── package.json ├── src ├── components │ ├── alerts.css │ ├── alerts.js │ ├── index.js │ └── virtual.js ├── index.js ├── mini_dom.js ├── mini_dom_fragments.js ├── mini_dom_map.js ├── mini_dom_signal.js ├── mini_html.js ├── router │ └── index.js └── store │ └── index.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | .DS_Store 3 | node_modules/ 4 | package-lock.json 5 | vite.config.js 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 xdadda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [minijs] 2 | 3 | A lightweight (3.3kB minified/gzipped) javascript declarative reactive framework based on 4 | * signals (thanks to milomg https://github.com/milomg/reactively) 5 | * tagged template literals 6 | * granular reactivity 7 | * sync/async components 8 | 9 | 10 | Here's a commented example with most of MiNi features: 11 | 12 | ```js 13 | 14 | import { render, html, reactive, onMount, onUnmount } from 'mini' 15 | 16 | ///// component 17 | function App(){ 18 | 19 | const appname='mini' 20 | 21 | //// signals 22 | const myref = reactive() 23 | const counter = reactive(0) 24 | const double = reactive(() => counter.value*2) 25 | const array = reactive(() => new Array(Math.max(0,counter.value)).fill(null)) 26 | //// effects 27 | reactive(() => { 28 | console.log('EFFECT',counter.value, double.value) 29 | }, {effect:true}) 30 | ///////////////////////////////////////////// 31 | 32 | //// component's lifecycle 33 | onMount(()=>{console.log('APP mounted',myref.value)}) 34 | onUnmount(()=>{console.log('APP unmounted')}) 35 | ///////////////////////////////////////////// 36 | 37 | //// event functions 38 | function handleInc(e){ 39 | //// use signal.value to read/get and write/set 40 | counter.value = counter.value + 1 41 | } 42 | const handleDec = () => counter.value-- 43 | 44 | //// mini's tagged template literals function 45 | return html` 46 | 47 | /* Note: comments wrapped like this will be ignored */ 48 | 49 | /* special :ref attribute to retrieve DOM elements */ 50 |
51 | 52 | /* normal literal variable */ 53 |

${appname}

54 | 55 | /* special @event handler to addEventListener to DOM elements */ 56 | 57 | 58 | 59 |
60 | 61 | /* to enable reactivity wrap a signal read in a function */ 62 |
counter: ${()=>counter.value+'#'}
63 | 64 | /* for reactive attributes add : and a signal read wrapped in a function */ 65 | /* please remember to ALWAYS put quotation marks "" around the function */ 66 |
67 | double: ${ ()=>double.value } 68 |
69 | 70 | /* to show/hide a DOM tree or a component just use logical && or */ 71 | /* a conditional (ternary) operator ?: ... and wrap signal in a function */ 72 |
${() => counter.value>3 && html`counter is above 3`}
73 | 74 | /* arrays' maps are also supported */ 75 | /* static arrays */ 76 | 79 | /* reactive arrays ... as usual remember to wrap in a function ()=>{} */ 80 | 83 | 84 |
85 |
86 | ` 87 | 88 | } 89 | 90 | render(document.getElementById('root'),App) 91 | 92 | ``` 93 | 94 | For further documentation and a playground link: TBD -------------------------------------------------------------------------------- /dist/components.cjs: -------------------------------------------------------------------------------- 1 | "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("./mini_dom-CfPyC6jy.cjs");function w(){return"10000000-1000-4000-8000-100000000000".replace(/[018]/g,c=>(+c^crypto.getRandomValues(new Uint8Array(1))[0]&15>>+c/4).toString(16))}function C({content:c,buttons:l,onCancel:o,onClose:u,type:d,placeholder:s="",width:i}){const e=w();function a(t){t.preventDefault(),t.stopPropagation(),o(document.getElementById(e))}function n(t){if(t.preventDefault(),t.stopPropagation(),u)return u(document.getElementById(e),document.getElementById("_in"+e).value)}function m(t,v){t.preventDefault(),t.stopPropagation(),v(document.getElementById(e),document.getElementById("_in"+e)?.value)}function f(t){t.key==="Escape"?a(t):t.key==="Enter"&&n(t)}return r.onMount(()=>{d==="prompt"?setTimeout(()=>{document.getElementById("_in"+e)?.focus()},10):l&&setTimeout(()=>{document.getElementById("_btn"+e)?.focus()},10)}),r.html`
${c} ${d==="prompt"&&`
`}
${l?.map((t,v)=>()=>r.html``)}
`}async function x(c,l,o){return await new Promise((u,d)=>{const s=document.body.querySelector("div"),i=document.createElement("div");s.appendChild(i);function e(n,m){n.parentElement.remove(),u(m)}function a(n){n.parentElement.remove(),u(!1)}r.render(i,()=>C({content:c,buttons:[{label:"Cancel",onClick:a},{label:"OK",onClick:e,focus:!0}],onClose:e,onCancel:a,type:"prompt",placeholder:o,width:l}))})}async function M(c,l){return await new Promise((o,u)=>{const d=document.body.querySelector("div"),s=document.createElement("div");d.appendChild(s);function i(a){a.parentElement.remove(),o(!0)}function e(a){a.parentElement.remove(),o(!1)}r.render(s,()=>C({content:c,buttons:[{label:"Cancel",onClick:e},{label:"OK",onClick:i,focus:!0}],onCancel:e,type:"confirm",width:l}))})}async function B(c,l){return await new Promise((o,u)=>{const d=document.body.querySelector("div"),s=document.createElement("div");d.appendChild(s);function i(e){e.parentElement.remove(),o(!1)}r.render(s,()=>C({content:c,buttons:[{label:"OK",onClick:i,focus:!0}],onCancel:i,type:"alert",width:l}))})}function I({renderItem:c,itemCount:l,rowHeight:o,nodePadding:u,onUpdateRow:d,onUpdateScroll:s,onMounted:i}){const e=r.reactive();let a,n,m,f;const t=r.reactive([]);l.signal?f=l:typeof l=="function"?f=r.reactive(l):f={value:l};function v(){const p=Math.max(0,Math.floor(n.scrollTop/o)-u),E=n.offsetHeight;let h=Math.ceil(E/o)+2*u;h=Math.min(f.value-p,h);const g=Math.floor(n.scrollTop/o),$=(m||0)+h-1;d&&d(g,$);const _=p*o;e._value.firstElementChild.style.transform=`translateY(${_}px)`,s&&s(n.scrollTop),(m===void 0||m!==p)&&(m=p,t.value=new Array(h||0).fill(null).map((P,k)=>k+p))}function y(){a&&cancelAnimationFrame(a),a=requestAnimationFrame(v)}const b=f.value*o+"px";return r.onMount(()=>{const p=e._value;p&&(n=p.parentElement,n.style.overflowY!=="auto"&&(n.style.overflowY="auto"),n.addEventListener("scroll",y),v(),i&&i())}),r.onUnmount(()=>{n?.removeEventListener("scroll",y)}),r.html`
${r.map(t,c)}
`}exports.alert=B;exports.confirm=M;exports.prompt=x;exports.virtual=I; 2 | -------------------------------------------------------------------------------- /dist/components.js: -------------------------------------------------------------------------------- 1 | import { a as b, h as C, o as $, r as h, m as I, b as M } from "./mini_dom-D28UFCQZ.js"; 2 | function P() { 3 | return "10000000-1000-4000-8000-100000000000".replace( 4 | /[018]/g, 5 | (r) => (+r ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +r / 4).toString(16) 6 | ); 7 | } 8 | function E({ content: r, buttons: o, onCancel: l, onClose: s, type: u, placeholder: c = "", width: a }) { 9 | const e = P(); 10 | function i(t) { 11 | t.preventDefault(), t.stopPropagation(), l(document.getElementById(e)); 12 | } 13 | function n(t) { 14 | if (t.preventDefault(), t.stopPropagation(), s) return s(document.getElementById(e), document.getElementById("_in" + e).value); 15 | } 16 | function d(t, p) { 17 | t.preventDefault(), t.stopPropagation(), p(document.getElementById(e), document.getElementById("_in" + e)?.value); 18 | } 19 | function m(t) { 20 | t.key === "Escape" ? i(t) : t.key === "Enter" && n(t); 21 | } 22 | return $(() => { 23 | u === "prompt" ? setTimeout(() => { 24 | document.getElementById("_in" + e)?.focus(); 25 | }, 10) : o && setTimeout(() => { 26 | document.getElementById("_btn" + e)?.focus(); 27 | }, 10); 28 | }), C`
${r} ${u === "prompt" && `
`}
${o?.map((t, p) => () => C``)}
`; 29 | } 30 | async function A(r, o, l) { 31 | return await new Promise((s, u) => { 32 | const c = document.body.querySelector("div"), a = document.createElement("div"); 33 | c.appendChild(a); 34 | function e(n, d) { 35 | n.parentElement.remove(), s(d); 36 | } 37 | function i(n) { 38 | n.parentElement.remove(), s(!1); 39 | } 40 | b(a, () => E({ 41 | content: r, 42 | buttons: [ 43 | { label: "Cancel", onClick: i }, 44 | { label: "OK", onClick: e, focus: !0 } 45 | ], 46 | onClose: e, 47 | onCancel: i, 48 | type: "prompt", 49 | placeholder: l, 50 | width: o 51 | })); 52 | }); 53 | } 54 | async function F(r, o) { 55 | return await new Promise((l, s) => { 56 | const u = document.body.querySelector("div"), c = document.createElement("div"); 57 | u.appendChild(c); 58 | function a(i) { 59 | i.parentElement.remove(), l(!0); 60 | } 61 | function e(i) { 62 | i.parentElement.remove(), l(!1); 63 | } 64 | b(c, () => E({ 65 | content: r, 66 | buttons: [ 67 | { label: "Cancel", onClick: e }, 68 | { label: "OK", onClick: a, focus: !0 } 69 | ], 70 | onCancel: e, 71 | type: "confirm", 72 | width: o 73 | })); 74 | }); 75 | } 76 | async function K(r, o) { 77 | return await new Promise((l, s) => { 78 | const u = document.body.querySelector("div"), c = document.createElement("div"); 79 | u.appendChild(c); 80 | function a(e) { 81 | e.parentElement.remove(), l(!1); 82 | } 83 | b(c, () => E({ 84 | content: r, 85 | buttons: [{ label: "OK", onClick: a, focus: !0 }], 86 | onCancel: a, 87 | type: "alert", 88 | width: o 89 | })); 90 | }); 91 | } 92 | function S({ 93 | renderItem: r, 94 | //(idx)=>{..} 95 | itemCount: o, 96 | //# of items (can be a number, signal of function) 97 | rowHeight: l, 98 | //in pixels 99 | nodePadding: s, 100 | //number of "padding" items 101 | onUpdateRow: u, 102 | //triggered when virtual list is updated 103 | onUpdateScroll: c, 104 | //triggered when virtual list is updated 105 | onMounted: a 106 | }) { 107 | const e = h(); 108 | let i, n, d, m; 109 | const t = h([]); 110 | o.signal ? m = o : typeof o == "function" ? m = h(o) : m = { value: o }; 111 | function p() { 112 | const f = Math.max(0, Math.floor(n.scrollTop / l) - s), _ = n.offsetHeight; 113 | let y = Math.ceil(_ / l) + 2 * s; 114 | y = Math.min(m.value - f, y); 115 | const k = Math.floor(n.scrollTop / l), w = (d || 0) + y - 1; 116 | u && u(k, w); 117 | const x = f * l; 118 | e._value.firstElementChild.style.transform = `translateY(${x}px)`, c && c(n.scrollTop), (d === void 0 || d !== f) && (d = f, t.value = new Array(y || 0).fill(null).map((T, B) => B + f)); 119 | } 120 | function v() { 121 | i && cancelAnimationFrame(i), i = requestAnimationFrame(p); 122 | } 123 | const g = m.value * l + "px"; 124 | return $(() => { 125 | const f = e._value; 126 | f && (n = f.parentElement, n.style.overflowY !== "auto" && (n.style.overflowY = "auto"), n.addEventListener("scroll", v), p(), a && a()); 127 | }), M(() => { 128 | n?.removeEventListener("scroll", v); 129 | }), C`
${I(t, r)}
`; 130 | } 131 | export { 132 | K as alert, 133 | F as confirm, 134 | A as prompt, 135 | S as virtual 136 | }; 137 | -------------------------------------------------------------------------------- /dist/mini.cjs: -------------------------------------------------------------------------------- 1 | "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./mini_dom-CfPyC6jy.cjs");function o(n,r){const t=()=>r();return t._loader=!0,n._suspense=!0,e.html`${t}${n}`}function a(n,r="default"){return async(...t)=>{const u=(await n())[r];return u?u(...t):console.error(`MiNi lazy: ${n} missing "${r}" export`)}}exports.html=e.html;exports.map=e.map;exports.onMount=e.onMount;exports.onUnmount=e.onUnmount;exports.reactive=e.reactive;exports.render=e.render;exports.untrack=e.untrack;exports.Suspense=o;exports.lazy=a; 2 | -------------------------------------------------------------------------------- /dist/mini.css: -------------------------------------------------------------------------------- 1 | .alert{font-family:inherit;position:fixed;display:flex;z-index:100001;overflow:show;margin:auto;inset:0;text-align:center;color:inherit}.alert:before{content:"";display:block;position:fixed;top:0;left:0;width:100%;height:100%;background:#0009}.alert-message{align-self:center;color:inherit;position:relative;margin:auto;border-radius:20px;padding:10px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.alert-message .msg{display:flex;flex-direction:column;text-align:left;width:310px;padding:3px;margin:auto;overflow:hidden;text-overflow:ellipsis} 2 | -------------------------------------------------------------------------------- /dist/mini.js: -------------------------------------------------------------------------------- 1 | import { h as a } from "./mini_dom-D28UFCQZ.js"; 2 | import { m as c, o as f, b as m, r as p, a as $, u as d } from "./mini_dom-D28UFCQZ.js"; 3 | function o(r, e) { 4 | const n = () => e(); 5 | return n._loader = !0, r._suspense = !0, a`${n}${r}`; 6 | } 7 | function u(r, e = "default") { 8 | return async (...n) => { 9 | const t = (await r())[e]; 10 | return t ? t(...n) : console.error(`MiNi lazy: ${r} missing "${e}" export`); 11 | }; 12 | } 13 | export { 14 | o as Suspense, 15 | a as html, 16 | u as lazy, 17 | c as map, 18 | f as onMount, 19 | m as onUnmount, 20 | p as reactive, 21 | $ as render, 22 | d as untrack 23 | }; 24 | -------------------------------------------------------------------------------- /dist/mini_dom-CfPyC6jy.cjs: -------------------------------------------------------------------------------- 1 | "use strict";function J(e,t,n,i){let l=n.length,r=/[:,@]\S*=/.exec(e);if(!r)return console.error("MiNi: attribute is missing :@ prefix",e),!1;if(r?.length>1)return console.error('MiNi: attribute is missing "${}"',e),!1;if(!e.endsWith(r[0]+'"'))return console.error("MiNi: attribute "+r[0]+' is missing "${}"'),!1;const c=r[0][0],s=r[0].slice(1,-1);n.push({type:c,key:s,v:t});const o=e.lastIndexOf(s);return e=e.substring(0,o-1)+e.substring(o-1).replace(c+s,s+l),e.slice(-1)==='"'?i.push(""):i.push('""'),e}function Q(e,...t){let n=[...e];function i(l){let r=[],c=[],s=!1;for(let f=0;f");s=x!==-1&&x>d?!0:s,s=d!==-1&&d>x?!1:s;const y=c.length;if(typeof h=="function"||h instanceof Promise||h?.signal)if(!s)c.push({type:"node",key:y,v:h}),r[f]=``;else{const m=J(n[f],h,c,r);m?n[f]=m:console.error("MiNi: unknown attribute type",n[f],h)}else Array.isArray(h)?(r[f]="",h.forEach((m,g)=>{typeof m=="function"?(c.push({type:"node",key:y+":"+g,v:m}),r[f]+=``):r[f]+=m})):h===!1||h===void 0?(s&&n[f].slice(-1)==='"'&&(n[f]=n[f].replace(/\s(\S+)$/,'"')),r.push("")):r.push(h)}function o(f,h){let p=f[0];for(let x=0;xr(this._value)),this.cleanups=[]),this._value=this.fn(),v){if(this.removeParentObservers(b),this.sources&&b>0){this.sources.length=b+v.length;for(let r=0;rr===this);i.observers[l]=i.observers[i.observers.length-1],i.observers.pop()}}}function O(e){const t=N,n=v,i=b;N=void 0,v=null,b=0;const l=e();return v=n,N=t,b=i,l}function Y(){for(let e=0;e{q=!1,Y()}))}function L(e){if(e.nodeType){let t=[e];return t.parent=e.parentNode,t.next=e.nextSibling,t.prev=e.previousSibling,t.fragment=!0,t}else return Array.isArray(e)&&!e.fragment?e[0]?(e[0].before(document.createTextNode("")),e[e.length-1].after(document.createTextNode("")),e.parent=e[0].parentNode,e.prev=e[0].previousSibling,e.next=e[e.length-1].nextSibling,e.fragment=!0,e):!1:(console.error("MiNi: unknown input for createFragment"),!1)}function w(e,t){return e.fragment&&Array.isArray(e)&&Array.isArray(t)?(t.prev=e.prev,t.next=e.next,t.parent=e.parent,S(e),t.next?t.next.before(...t):t.parent.append(...t),t.fragment=!0):console.error("MiNi: replaceFragments unknown input",e,t),t}function S(e){if(!e.prev&&!e.next&&e.parent)e.parent.textContent="";else{if(e.prev?.nextSibling===e.next)return e;{let t=e.prev?.nextSibling||e.next?.parentElement?.firstChild||e.parent?.firstChild;if(!t)return;for(;t!==e.next;){if(!t)return console.error("MiNi: clearFragment missing node",e);const n=t.nextSibling;t.remove(),t=n}}}return e.length&&e.splice(0,e.length),e}function ee(e){if(!e.prev&&!e.next)e.splice(0,e.length),e.splice(0,0,...e.parent.childNodes);else{let t=e.prev?.nextSibling||e.parent.firstChild,n=[];for(;t!==e.next;){if(!t)return console.error("MiNi: updateFragment missing node",e);const i=t.nextSibling;n.push(t),t=i}e.length&&e.splice(0,e.length),e.splice(0,0,...n)}return e}function H(e,t,n,i){const l=document.createComment("");t.parent.insertBefore(l,n);const r=Symbol("$item");i[r]={frag:!0};const c=P(l,e,i[r]);return c.prev&&(c.prev.nextElementSibling.myid=r),l}function te(e,t,n){I(t.myid,n);const i=Symbol("$item");n[i]={frag:!0};const l=P(t,e,n[i]);return l.prev&&(l.prev.nextElementSibling.myid=i),l[0]}function j(e,t=[]){return e.unmount&&(t.push(e.unmount),delete e.unmount),Object.getOwnPropertySymbols(e).forEach(n=>{e[n]?.frag&&j(e[n],t)}),t.flat().reverse()}function k(e){Object.getOwnPropertySymbols(e).forEach(t=>{e[t]?.frag&&(e[t].stale=!0,k(e[t]))})}function I(e,t){if(!t||!t[e])return;const n=j(t[e]);n.length&&n.forEach(i=>typeof i=="function"&&i()),k(t[e]),delete t[e]}function W(e){if(!e)return;const t=j(e);t.length&&t.forEach(n=>typeof n=="function"&&n()),k(e),Object.getOwnPropertySymbols(e).forEach(n=>delete e[n])}function U(e,t=[],n=[],i,l){ee(e);let r=e.next,c=e.parent,s=0,o=t?.length,u=0,a=n?.length,f=a,h=null,p=new Array(f),x=!1,d=e;if(f===0){W(l),S(e);return}for(;si(n[u]),e,y,l),u++}else if(a===u)for(;sm-u){const G=d[s];for(;ui(n[u]),e,G,l),u++}else p&&p[u]?c.replaceChild(p[u],d[s]):p[u]=te(()=>i(n[u]),d[s],l),s++,u++}else s++}else I(d[s].myid,l),c.removeChild(d[s++])}}let A=[];function ne(e){A.push(e)}let M=[];function ie(e){M.push(e)}function z(e,t=[]){return e.unmount&&(t.push(e.unmount),delete e.unmount),Object.getOwnPropertySymbols(e).forEach(n=>{e[n]?.frag&&z(e[n],t)}),t.flat().reverse()}function se(e,t){const n=document.createComment("rx");let i=new Array(e.length);i=i.fill(0).map(()=>n.cloneNode()),t.frag=w(t.frag,i);for(let l=0;le[l],t)}function re(e,t){if(t)if(t.hidden&&(t.hidden=!1),e?.html)P(t.frag,e,t),t.mount&&setTimeout(()=>{t.mount?.forEach(n=>n()),t.mount=void 0},0);else if(Array.isArray(e))se(e,t),t.mount&&setTimeout(()=>{t.mount?.forEach(n=>n()),t.mount=void 0},0);else if(e===!1||e==="")S(t.frag),t.hidden=!0;else{let n=t.frag.prev.nextSibling;if(n.nodeType!==3){const i=document.createTextNode("");n.replaceWith(i),n=i}e!==void 0&&n.data!==e&&(n.data=e)}}function le(e){const t=Object.getOwnPropertySymbols(e).filter(i=>e[i]?.loader)?.[0];if(!t)return;const n=e[t].frag;S(n),delete e[t]}function B(e){Object.getOwnPropertySymbols(e).forEach(t=>{e[t]?.frag&&(e[t].stale=!0,B(e[t]),delete e[t])})}function D(e,t,n){const i=Symbol("$comp");if(n[i]={},e.before(document.createTextNode("")),e.after(document.createTextNode("")),n[i].frag=L(e),t._map)return t(n,i);_(async()=>{if(!n[i])return;if(n.stale||n[i].stale)return delete n[i];B(n[i]);const l=A.length,r=M.length,c=z(n[i]);c.length&&c.forEach(o=>typeof o=="function"&&o());let s=t();if(s instanceof Promise&&(s=await s),!!n[i]){if(A.length>l){const o=A.length-l;n[i].mount=A.splice(-o,o)}if(M.length>r){const o=M.length-r;n[i].unmount=M.splice(-o,o)}if(typeof s=="function"&&s?._map)return s(n,i);if(t._loader&&(n[i].loader=!0),t._suspense){if(!n[i])return;n[i].suspense=!0,le(n),delete t._suspense}s=typeof s=="function"&&!s?.html?O(s):s,re(s,n[i])}},{effect:!0})}function ue(e,t){const n=function(...i){const[l,r]=i;S(l[r].frag);let c;_(()=>{if(!l[r])return;if(l.stale||l[r].stale)return delete l[r];const s=e.signal?e.value:e;O(()=>U(l[r].frag,c,s,t,l[r])),c=s},{effect:!0})};return n._map=!0,n}function oe(e,t,n,i){function l(s,o,u){u===!0?s.setAttribute(o,o):u===!1?s.removeAttribute(o):u!==!1&&u!=null&&s.setAttribute(o,u)}const r=Symbol("$attr");i[r]={},i[r].frag=L(e);const c=_(n);_(()=>{if(!i[r])return;if(i.stale||i[r].stale)return delete i[r];let s=c.value;t==="value"?s.signal?e.value=s.value:e.value=s:t==="ref"?(e.removeAttribute(t),n.signal&&(n.value=e)):l(e,t,s)},{effect:!0})}function ce(e,t){return document.createTreeWalker(e,128,{acceptNode:n=>n.textContent===t?1:2}).nextNode()}function P(e,t,n={0:{}}){if(!e)return console.error("MiNi: renderClient missing node element");if(e.nodeType&&(e=L(e)),typeof t=="function"&&!t.html&&(t=O(t)),typeof t=="function"&&t.html&&(t=O(t)),t.html===void 0)return console.error("MiNi: unknown input to renderClient",t);const{html:i,reactarray:l}=t,r=document.createElement("template");r.innerHTML=i;const c=e.prev?.parentNode||e.parent;for(let s=0;s{}"):f.html===!0?o=P(o,f,n):console.error("MiNi: unknown node value",f):console.error("MiNi: cannot find placeholder","rx"+a,c);break;case"@":case":":o=r.content.querySelector(`[${a+s}]`),o?(o.removeAttribute(a+s),u===":"?oe(o,a,f,n):u==="@"?o.addEventListener(a.toLowerCase(),f,a==="onwheel"?{passive:!1}:{}):console.error("MiNi: unknown special attr",u,s)):console.error("MiNi: cannot find attribute",a+s);break}}return S(e),e.next?e.next.before(r.content):c?.appendChild(r.content),e.destroy=()=>{const s=z(n);s.length&&s.forEach(o=>typeof o=="function"&&o()),B(n),S(e)},e}async function fe(e,t,n){if(e.appendChild(document.createElement("div")),typeof t!="function")return console.error("MiNi: render 2nd arg must be a function");let i={0:{}};try{const l=await P(e.children[0],Q`${()=>t()}`,i);return n&&console.log("rootowner",i),l}catch(l){console.error("MiNi: render",l)}}exports.html=Q;exports.map=ue;exports.onMount=ne;exports.onUnmount=ie;exports.reactive=_;exports.render=fe;exports.untrack=O; 2 | -------------------------------------------------------------------------------- /dist/mini_dom-D28UFCQZ.js: -------------------------------------------------------------------------------- 1 | function U(e, t, n, i) { 2 | let l = n.length, r = /[:,@]\S*=/.exec(e); 3 | if (!r) 4 | return console.error("MiNi: attribute is missing :@ prefix", e), !1; 5 | if (r?.length > 1) 6 | return console.error('MiNi: attribute is missing "${}"', e), !1; 7 | if (!e.endsWith(r[0] + '"')) 8 | return console.error("MiNi: attribute " + r[0] + ' is missing "${}"'), !1; 9 | const c = r[0][0], s = r[0].slice(1, -1); 10 | n.push({ type: c, key: s, v: t }); 11 | const o = e.lastIndexOf(s); 12 | return e = e.substring(0, o - 1) + e.substring(o - 1).replace(c + s, s + l), e.slice(-1) === '"' ? i.push("") : i.push('""'), e; 13 | } 14 | function J(e, ...t) { 15 | let n = [...e]; 16 | function i(l) { 17 | let r = [], c = [], s = !1; 18 | for (let f = 0; f < t.length; f++) { 19 | const h = t[f], p = n[f], x = p.lastIndexOf("<"), d = p.lastIndexOf(">"); 20 | s = x !== -1 && x > d ? !0 : s, s = d !== -1 && d > x ? !1 : s; 21 | const y = c.length; 22 | if (typeof h == "function" || h instanceof Promise || h?.signal) 23 | if (!s) 24 | c.push({ type: "node", key: y, v: h }), r[f] = ``; 25 | else { 26 | const m = U(n[f], h, c, r); 27 | m ? n[f] = m : console.error("MiNi: unknown attribute type", n[f], h); 28 | } 29 | else Array.isArray(h) ? (r[f] = "", h.forEach((m, g) => { 30 | typeof m == "function" ? (c.push({ type: "node", key: y + ":" + g, v: m }), r[f] += ``) : r[f] += m; 31 | })) : h === !1 || h === void 0 ? (s && n[f].slice(-1) === '"' && (n[f] = n[f].replace(/\s(\S+)$/, '"')), r.push("")) : r.push(h); 32 | } 33 | function o(f, h) { 34 | let p = f[0]; 35 | for (let x = 0; x < h.length; x++) 36 | p += h[x] + f[x + 1]; 37 | return p.replace(/\s+/g, " "); 38 | } 39 | let u = o(n, r); 40 | return u = u.replace(/(?=\/\*).*?(?:\*\/)/g, ""), { html: u.trim(), reactarray: c }; 41 | } 42 | return i.html = !0, i; 43 | } 44 | let N, v = null, b = 0, M = [], F, q = !1; 45 | const O = 0, R = 1, C = 2; 46 | function P(e, t) { 47 | const n = new X(e, t?.effect, t?.label); 48 | return t?.equals && (n.equals = t.equals), n.signal = !0, n; 49 | } 50 | function K(e, t) { 51 | return e === t; 52 | } 53 | class X { 54 | _value; 55 | fn; 56 | observers = null; 57 | // nodes that have us as sources (down links) 58 | sources = null; 59 | // sources in reference order, not deduplicated (up links) 60 | state; 61 | effect; 62 | label; 63 | cleanups = []; 64 | equals = K; 65 | constructor(t, n, i) { 66 | typeof t == "function" ? (this.fn = t, this._value = void 0, this.effect = n || !1, this.state = C, n && (M.push(this), F?.(this))) : (this.fn = void 0, this._value = t, this.state = O, this.effect = !1), i && (this.label = i); 67 | } 68 | get value() { 69 | return this.get(); 70 | } 71 | set value(t) { 72 | this.set(t); 73 | } 74 | get() { 75 | return N && (!v && N.sources && N.sources[b] == this ? b++ : v ? v.push(this) : v = [this]), this.fn && this.updateIfNecessary(), this._value; 76 | } 77 | set(t) { 78 | if (typeof t == "function") { 79 | const n = t; 80 | n !== this.fn && this.stale(C), this.fn = n; 81 | } else { 82 | this.fn && (this.removeParentObservers(0), this.sources = null, this.fn = void 0); 83 | const n = t; 84 | if (!this.equals(this._value, n)) { 85 | if (this.observers) 86 | for (let i = 0; i < this.observers.length; i++) 87 | this.observers[i].stale(C); 88 | this._value = n; 89 | } 90 | } 91 | } 92 | stale(t) { 93 | if (this.state < t && (this.state === O && this.effect && (M.push(this), F?.(this)), this.state = t, this.observers)) 94 | for (let n = 0; n < this.observers.length; n++) 95 | this.observers[n].stale(R); 96 | } 97 | /** run the computation fn, updating the cached value */ 98 | update() { 99 | const t = this._value, n = N, i = v, l = b; 100 | N = this, v = null, b = 0; 101 | try { 102 | if (this.cleanups.length && (this.cleanups.forEach((r) => r(this._value)), this.cleanups = []), this._value = this.fn(), v) { 103 | if (this.removeParentObservers(b), this.sources && b > 0) { 104 | this.sources.length = b + v.length; 105 | for (let r = 0; r < v.length; r++) 106 | this.sources[b + r] = v[r]; 107 | } else 108 | this.sources = v; 109 | for (let r = b; r < this.sources.length; r++) { 110 | const c = this.sources[r]; 111 | c.observers ? c.observers.push(this) : c.observers = [this]; 112 | } 113 | } else this.sources && b < this.sources.length && (this.removeParentObservers(b), this.sources.length = b); 114 | } finally { 115 | v = i, N = n, b = l; 116 | } 117 | if (!this.equals(t, this._value) && this.observers) 118 | for (let r = 0; r < this.observers.length; r++) { 119 | const c = this.observers[r]; 120 | c.state = C; 121 | } 122 | this.state = O; 123 | } 124 | /** update() if dirty, or a parent turns out to be dirty. */ 125 | updateIfNecessary() { 126 | if (this.state === R) { 127 | for (const t of this.sources) 128 | if (t.updateIfNecessary(), this.state === C) 129 | break; 130 | } 131 | this.state === C && this.update(), this.state = O; 132 | } 133 | removeParentObservers(t) { 134 | if (this.sources) 135 | for (let n = t; n < this.sources.length; n++) { 136 | const i = this.sources[n], l = i.observers.findIndex((r) => r === this); 137 | i.observers[l] = i.observers[i.observers.length - 1], i.observers.pop(); 138 | } 139 | } 140 | } 141 | function T(e) { 142 | const t = N, n = v, i = b; 143 | N = void 0, v = null, b = 0; 144 | const l = e(); 145 | return v = n, N = t, b = i, l; 146 | } 147 | function Y() { 148 | for (let e = 0; e < M.length; e++) 149 | M[e].get(); 150 | M.length = 0; 151 | } 152 | function Z(e = V) { 153 | F = e; 154 | } 155 | Z(); 156 | function V() { 157 | q || (q = !0, queueMicrotask(() => { 158 | q = !1, Y(); 159 | })); 160 | } 161 | function L(e) { 162 | if (e.nodeType) { 163 | let t = [e]; 164 | return t.parent = e.parentNode, t.next = e.nextSibling, t.prev = e.previousSibling, t.fragment = !0, t; 165 | } else return Array.isArray(e) && !e.fragment ? e[0] ? (e[0].before(document.createTextNode("")), e[e.length - 1].after(document.createTextNode("")), e.parent = e[0].parentNode, e.prev = e[0].previousSibling, e.next = e[e.length - 1].nextSibling, e.fragment = !0, e) : !1 : (console.error("MiNi: unknown input for createFragment"), !1); 166 | } 167 | function w(e, t) { 168 | return e.fragment && Array.isArray(e) && Array.isArray(t) ? (t.prev = e.prev, t.next = e.next, t.parent = e.parent, S(e), t.next ? t.next.before(...t) : t.parent.append(...t), t.fragment = !0) : console.error("MiNi: replaceFragments unknown input", e, t), t; 169 | } 170 | function S(e) { 171 | if (!e.prev && !e.next && e.parent) 172 | e.parent.textContent = ""; 173 | else { 174 | if (e.prev?.nextSibling === e.next) return e; 175 | { 176 | let t = e.prev?.nextSibling || e.next?.parentElement?.firstChild || e.parent?.firstChild; 177 | if (!t) return; 178 | for (; t !== e.next; ) { 179 | if (!t) return console.error("MiNi: clearFragment missing node", e); 180 | const n = t.nextSibling; 181 | t.remove(), t = n; 182 | } 183 | } 184 | } 185 | return e.length && e.splice(0, e.length), e; 186 | } 187 | function ee(e) { 188 | if (!e.prev && !e.next) 189 | e.splice(0, e.length), e.splice(0, 0, ...e.parent.childNodes); 190 | else { 191 | let t = e.prev?.nextSibling || e.parent.firstChild, n = []; 192 | for (; t !== e.next; ) { 193 | if (!t) return console.error("MiNi: updateFragment missing node", e); 194 | const i = t.nextSibling; 195 | n.push(t), t = i; 196 | } 197 | e.length && e.splice(0, e.length), e.splice(0, 0, ...n); 198 | } 199 | return e; 200 | } 201 | function H(e, t, n, i) { 202 | const l = document.createComment(""); 203 | t.parent.insertBefore(l, n); 204 | const r = Symbol("$item"); 205 | i[r] = { frag: !0 }; 206 | const c = _(l, e, i[r]); 207 | return c.prev && (c.prev.nextElementSibling.myid = r), l; 208 | } 209 | function te(e, t, n) { 210 | I(t.myid, n); 211 | const i = Symbol("$item"); 212 | n[i] = { frag: !0 }; 213 | const l = _(t, e, n[i]); 214 | return l.prev && (l.prev.nextElementSibling.myid = i), l[0]; 215 | } 216 | function j(e, t = []) { 217 | return e.unmount && (t.push(e.unmount), delete e.unmount), Object.getOwnPropertySymbols(e).forEach((n) => { 218 | e[n]?.frag && j(e[n], t); 219 | }), t.flat().reverse(); 220 | } 221 | function z(e) { 222 | Object.getOwnPropertySymbols(e).forEach((t) => { 223 | e[t]?.frag && (e[t].stale = !0, z(e[t])); 224 | }); 225 | } 226 | function I(e, t) { 227 | if (!t || !t[e]) return; 228 | const n = j(t[e]); 229 | n.length && n.forEach((i) => typeof i == "function" && i()), z(t[e]), delete t[e]; 230 | } 231 | function W(e) { 232 | if (!e) return; 233 | const t = j(e); 234 | t.length && t.forEach((n) => typeof n == "function" && n()), z(e), Object.getOwnPropertySymbols(e).forEach((n) => delete e[n]); 235 | } 236 | function Q(e, t = [], n = [], i, l) { 237 | ee(e); 238 | let r = e.next, c = e.parent, s = 0, o = t?.length, u = 0, a = n?.length, f = a, h = null, p = new Array(f), x = !1, d = e; 239 | if (f === 0) { 240 | W(l), S(e); 241 | return; 242 | } 243 | for (; s < o || u < a; ) 244 | if (o === s) { 245 | const y = a < f ? u && p[u - 1] ? p[u - 1].nextSibling : p[a] : r; 246 | for (; u < a; ) 247 | p && p[u] ? c.insertBefore(p[u], y) : p[u] = H(() => i(n[u]), e, y, l), u++; 248 | } else if (a === u) 249 | for (; s < o; ) 250 | (!h || !h.has(t[s])) && (I(d[s].myid, l), c.removeChild(d[s])), s++; 251 | else if (t[s] === n[u]) 252 | p[u] = d[s], s++, u++; 253 | else if (t[o - 1] === n[a - 1]) 254 | p[a - 1] = d[o - 1], o--, a--; 255 | else if (t[s] === n[a - 1] && n[u] === t[o - 1]) { 256 | const y = d[--o].nextSibling, m = d[s++].nextSibling; 257 | c.insertBefore(d[u++], y), c.insertBefore(d[o], m), a--, t[o] = n[a]; 258 | } else { 259 | if (!h) { 260 | h = /* @__PURE__ */ new Map(); 261 | let m = u; 262 | for (; m < a; ) 263 | h.set(n[m], m++); 264 | } 265 | let y = u + f - a; 266 | if (!x) { 267 | let m = s; 268 | for (; m < o; ) { 269 | const g = h.get(t[m]); 270 | g !== void 0 && (p[g] = d[m], y++), m++; 271 | } 272 | if (x = !0, !y) 273 | return W(l), S(e), Q(e, [], n, i, l); 274 | } 275 | if (h.has(t[s])) { 276 | const m = h.get(t[s]); 277 | if (u < m && m < a) { 278 | let g = s, $ = 1; 279 | for (; ++g < o && g < a && h.get(t[g]) === m + $; ) 280 | $++; 281 | if ($ > m - u) { 282 | const G = d[s]; 283 | for (; u < m; ) 284 | p && p[u] ? c.insertBefore(p[u], G) : p[u] = H(() => i(n[u]), e, G, l), u++; 285 | } else 286 | p && p[u] ? c.replaceChild(p[u], d[s]) : p[u] = te(() => i(n[u]), d[s], l), s++, u++; 287 | } else 288 | s++; 289 | } else 290 | I(d[s].myid, l), c.removeChild(d[s++]); 291 | } 292 | } 293 | let A = []; 294 | function ue(e) { 295 | A.push(e); 296 | } 297 | let E = []; 298 | function oe(e) { 299 | E.push(e); 300 | } 301 | function B(e, t = []) { 302 | return e.unmount && (t.push(e.unmount), delete e.unmount), Object.getOwnPropertySymbols(e).forEach((n) => { 303 | e[n]?.frag && B(e[n], t); 304 | }), t.flat().reverse(); 305 | } 306 | function ne(e, t) { 307 | const n = document.createComment("rx"); 308 | let i = new Array(e.length); 309 | i = i.fill(0).map(() => n.cloneNode()), t.frag = w(t.frag, i); 310 | for (let l = 0; l < e.length; l++) 311 | D(i[l], () => e[l], t); 312 | } 313 | function ie(e, t) { 314 | if (t) 315 | if (t.hidden && (t.hidden = !1), e?.html) 316 | _(t.frag, e, t), t.mount && setTimeout(() => { 317 | t.mount?.forEach((n) => n()), t.mount = void 0; 318 | }, 0); 319 | else if (Array.isArray(e)) 320 | ne(e, t), t.mount && setTimeout(() => { 321 | t.mount?.forEach((n) => n()), t.mount = void 0; 322 | }, 0); 323 | else if (e === !1 || e === "") 324 | S(t.frag), t.hidden = !0; 325 | else { 326 | let n = t.frag.prev.nextSibling; 327 | if (n.nodeType !== 3) { 328 | const i = document.createTextNode(""); 329 | n.replaceWith(i), n = i; 330 | } 331 | e !== void 0 && n.data !== e && (n.data = e); 332 | } 333 | } 334 | function se(e) { 335 | const t = Object.getOwnPropertySymbols(e).filter((i) => e[i]?.loader)?.[0]; 336 | if (!t) return; 337 | const n = e[t].frag; 338 | S(n), delete e[t]; 339 | } 340 | function k(e) { 341 | Object.getOwnPropertySymbols(e).forEach((t) => { 342 | e[t]?.frag && (e[t].stale = !0, k(e[t]), delete e[t]); 343 | }); 344 | } 345 | function D(e, t, n) { 346 | const i = Symbol("$comp"); 347 | if (n[i] = {}, e.before(document.createTextNode("")), e.after(document.createTextNode("")), n[i].frag = L(e), t._map) return t(n, i); 348 | P(async () => { 349 | if (!n[i]) return; 350 | if (n.stale || n[i].stale) return delete n[i]; 351 | k(n[i]); 352 | const l = A.length, r = E.length, c = B(n[i]); 353 | c.length && c.forEach((o) => typeof o == "function" && o()); 354 | let s = t(); 355 | if (s instanceof Promise && (s = await s), !!n[i]) { 356 | if (A.length > l) { 357 | const o = A.length - l; 358 | n[i].mount = A.splice(-o, o); 359 | } 360 | if (E.length > r) { 361 | const o = E.length - r; 362 | n[i].unmount = E.splice(-o, o); 363 | } 364 | if (typeof s == "function" && s?._map) return s(n, i); 365 | if (t._loader && (n[i].loader = !0), t._suspense) { 366 | if (!n[i]) return; 367 | n[i].suspense = !0, se(n), delete t._suspense; 368 | } 369 | s = typeof s == "function" && !s?.html ? T(s) : s, ie(s, n[i]); 370 | } 371 | }, { effect: !0 }); 372 | } 373 | function ce(e, t) { 374 | const n = function(...i) { 375 | const [l, r] = i; 376 | S(l[r].frag); 377 | let c; 378 | P(() => { 379 | if (!l[r]) return; 380 | if (l.stale || l[r].stale) return delete l[r]; 381 | const s = e.signal ? e.value : e; 382 | T(() => Q(l[r].frag, c, s, t, l[r])), c = s; 383 | }, { effect: !0 }); 384 | }; 385 | return n._map = !0, n; 386 | } 387 | function re(e, t, n, i) { 388 | function l(s, o, u) { 389 | u === !0 ? s.setAttribute(o, o) : u === !1 ? s.removeAttribute(o) : u !== !1 && u != null && s.setAttribute(o, u); 390 | } 391 | const r = Symbol("$attr"); 392 | i[r] = {}, i[r].frag = L(e); 393 | const c = P(n); 394 | P(() => { 395 | if (!i[r]) return; 396 | if (i.stale || i[r].stale) return delete i[r]; 397 | let s = c.value; 398 | t === "value" ? s.signal ? e.value = s.value : e.value = s : t === "ref" ? (e.removeAttribute(t), n.signal && (n.value = e)) : l(e, t, s); 399 | }, { effect: !0 }); 400 | } 401 | function le(e, t) { 402 | return document.createTreeWalker(e, 128, { acceptNode: (n) => n.textContent === t ? 1 : 2 }).nextNode(); 403 | } 404 | function _(e, t, n = { 0: {} }) { 405 | if (!e) return console.error("MiNi: renderClient missing node element"); 406 | if (e.nodeType && (e = L(e)), typeof t == "function" && !t.html && (t = T(t)), typeof t == "function" && t.html && (t = T(t)), t.html === void 0) return console.error("MiNi: unknown input to renderClient", t); 407 | const { html: i, reactarray: l } = t, r = document.createElement("template"); 408 | r.innerHTML = i; 409 | const c = e.prev?.parentNode || e.parent; 410 | for (let s = 0; s < l.length; s++) { 411 | let o, { type: u, key: a, v: f } = l[s]; 412 | switch (u) { 413 | case "node": 414 | o = le(r.content, "rx" + a), o ? typeof f == "function" ? D(o, f, n) : f instanceof Promise ? console.error("MiNi: wrap async component in ()=>{}") : f.html === !0 ? o = _(o, f, n) : console.error("MiNi: unknown node value", f) : console.error("MiNi: cannot find placeholder", "rx" + a, c); 415 | break; 416 | case "@": 417 | case ":": 418 | o = r.content.querySelector(`[${a + s}]`), o ? (o.removeAttribute(a + s), u === ":" ? re(o, a, f, n) : u === "@" ? o.addEventListener(a.toLowerCase(), f, a === "onwheel" ? { passive: !1 } : {}) : console.error("MiNi: unknown special attr", u, s)) : console.error("MiNi: cannot find attribute", a + s); 419 | break; 420 | } 421 | } 422 | return S(e), e.next ? e.next.before(r.content) : c?.appendChild(r.content), e.destroy = () => { 423 | const s = B(n); 424 | s.length && s.forEach((o) => typeof o == "function" && o()), k(n), S(e); 425 | }, e; 426 | } 427 | async function fe(e, t, n) { 428 | if (e.appendChild(document.createElement("div")), typeof t != "function") return console.error("MiNi: render 2nd arg must be a function"); 429 | let i = { 0: {} }; 430 | try { 431 | const l = await _(e.children[0], J`${() => t()}`, i); 432 | return n && console.log("rootowner", i), l; 433 | } catch (l) { 434 | console.error("MiNi: render", l); 435 | } 436 | } 437 | export { 438 | fe as a, 439 | oe as b, 440 | J as h, 441 | ce as m, 442 | ue as o, 443 | P as r, 444 | T as u 445 | }; 446 | -------------------------------------------------------------------------------- /dist/router.cjs: -------------------------------------------------------------------------------- 1 | "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("./mini.cjs"),a=require("./store.cjs"),p=require("./mini_dom-CfPyC6jy.cjs");function f(n,i,o={nohistory:!1,replacehistory:!0}){n!==a("$url").value&&(i&&a("$args",i),a("$url").value=n||"/",o.nohistory||(o.replacehistory?window.history.pushState({},null,n):window.history.replaceState({},null,n)))}function m(){return a("$route")}async function d({routes:n,loader:i=!1,handleAuth:o}){if(!a("$route")){let r=function(s){s.preventDefault(),s.stopPropagation();const l=window.location.href.replace(window.location.origin,"");f(l,null,{nohistory:!0})};a("$url",p.reactive(window.location.href.replace(window.location.origin,""))),window.addEventListener("popstate",r)}let t=a("$url").value,e=$(decodeURIComponent(t),n),u=!0;if(!(o&&(u=await o(e),!u)))return e.args=a("$args")||e.args||null,a("$route",e),i?h.Suspense(()=>a("$route").element(e.args),i):()=>p.html`${()=>a("$route").element(e.args)}`}function g(n,i){const o=n.split("/").map((l,c)=>[l.replace(":",""),c,l.charAt(0)===":"]),t=o.filter(([,,l])=>l),e=o.filter(([,,l])=>!l),u=i.split("/"),r=t.map(([l,c])=>[l,u[c]]);let s=!0;return e.forEach(([l,c])=>s=s&l===u[c]),s?Object.fromEntries(r):null}function w(n,i){const o=u=>u.replace(/([.*+?^=!:${}()|[\]/\\])/g,"\\$1"),t=new RegExp("^"+n.split("*").map(o).join("(.*)")+"$"),e=i.match(t);return e&&e[1]!==null?"/"+e[1]:null}function $(n,i){return R(n,null,i)}function R(n,i,o){let t=n||"/";t&&t!=="/"&&t.slice(-1)==="/"&&(t=t.slice(0,-1));let e=t.split("?")[1];e&&(e=new URLSearchParams(e),e=Object.fromEntries([...e]),t=t.split("?")[0]);const u=o?.find(r=>{if(r.path!=="/"&&r.path.slice(-1)==="/"&&(r.path=r.path.slice(0,-1)),r.path===t||r.path.includes("*")&&(t===r.path.slice(0,-2)?r.subpath="/":r.subpath=w(r.path,t),r.subpath))return!0;if(r.path.includes(":")){const s=g(r.path,t);return s?(r.params=s,!0):!1}return!1});return u?(u.url=t,u.query=e,u):!1}exports.Router=d;exports.getRoute=m;exports.setRoute=f; 2 | -------------------------------------------------------------------------------- /dist/router.js: -------------------------------------------------------------------------------- 1 | import { Suspense as c } from "./mini.js"; 2 | import s from "./store.js"; 3 | import { r as f, h } from "./mini_dom-D28UFCQZ.js"; 4 | function m(n, o, i = { nohistory: !1, replacehistory: !0 }) { 5 | n !== s("$url").value && (o && s("$args", o), s("$url").value = n || "/", i.nohistory || (i.replacehistory ? window.history.pushState({}, null, n) : window.history.replaceState({}, null, n))); 6 | } 7 | function v() { 8 | return s("$route"); 9 | } 10 | async function U({ routes: n, loader: o = !1, handleAuth: i }) { 11 | if (!s("$route")) { 12 | let r = function(u) { 13 | u.preventDefault(), u.stopPropagation(); 14 | const l = window.location.href.replace(window.location.origin, ""); 15 | m(l, null, { nohistory: !0 }); 16 | }; 17 | s("$url", f(window.location.href.replace(window.location.origin, ""))), window.addEventListener("popstate", r); 18 | } 19 | let t = s("$url").value, e = g(decodeURIComponent(t), n), a = !0; 20 | if (!(i && (a = await i(e), !a))) 21 | return e.args = s("$args") || e.args || null, s("$route", e), o ? c(() => s("$route").element(e.args), o) : () => h`${() => s("$route").element(e.args)}`; 22 | } 23 | function d(n, o) { 24 | const i = n.split("/").map((l, p) => [l.replace(":", ""), p, l.charAt(0) === ":"]), t = i.filter(([, , l]) => l), e = i.filter(([, , l]) => !l), a = o.split("/"), r = t.map(([l, p]) => [l, a[p]]); 25 | let u = !0; 26 | return e.forEach(([l, p]) => u = u & l === a[p]), u ? Object.fromEntries(r) : null; 27 | } 28 | function w(n, o) { 29 | const i = (a) => a.replace(/([.*+?^=!:${}()|[\]/\\])/g, "\\$1"), t = new RegExp("^" + n.split("*").map(i).join("(.*)") + "$"), e = o.match(t); 30 | return e && e[1] !== null ? "/" + e[1] : null; 31 | } 32 | function g(n, o) { 33 | return $(n, null, o); 34 | } 35 | function $(n, o, i) { 36 | let t = n || "/"; 37 | t && t !== "/" && t.slice(-1) === "/" && (t = t.slice(0, -1)); 38 | let e = t.split("?")[1]; 39 | e && (e = new URLSearchParams(e), e = Object.fromEntries([...e]), t = t.split("?")[0]); 40 | const a = i?.find((r) => { 41 | if (r.path !== "/" && r.path.slice(-1) === "/" && (r.path = r.path.slice(0, -1)), r.path === t || r.path.includes("*") && (t === r.path.slice(0, -2) ? r.subpath = "/" : r.subpath = w(r.path, t), r.subpath)) 42 | return !0; 43 | if (r.path.includes(":")) { 44 | const u = d(r.path, t); 45 | return u ? (r.params = u, !0) : !1; 46 | } 47 | return !1; 48 | }); 49 | return a ? (a.url = t, a.query = e, a) : !1; 50 | } 51 | export { 52 | U as Router, 53 | v as getRoute, 54 | m as setRoute 55 | }; 56 | -------------------------------------------------------------------------------- /dist/store.cjs: -------------------------------------------------------------------------------- 1 | "use strict";const S={};let c;function a(...t){let e;if(!c){const n=window._ctx_||{url:window.location.pathname};c=p(i=>({...S,...n}))}if(e=c,!t||!t.length)return e.getState();if(t.length===1){if(typeof t[0]=="string")return e.getState()[t[0]];if(typeof t[0]=="object")return e.setState(t[0]);console.error("MiNi: unknown store argument")}else if(t.length===2){if(typeof t[0]!="string")return console.error("MiNi: unknown store argument");if(typeof t[1]=="function"){const n=e.getState()[t[0]];t[1]=t[1](n)}return e.setState({[t[0]]:t[1]})}else console.error("MiNi: store has too many arguments")}const l=t=>{let e;const n=(s,u)=>{const o=typeof s=="function"?s(e):s;Object.is(o,e)||(e=u??(typeof o!="object"||o===null)?o:Object.assign({},e,o))},i=()=>e,r={setState:n,getState:i,getInitialState:()=>f},f=e=t(n,i,r);return r},p=t=>t?l(t):l;module.exports=a; 2 | -------------------------------------------------------------------------------- /dist/store.js: -------------------------------------------------------------------------------- 1 | const S = {}; 2 | let c; 3 | function y(...t) { 4 | let e; 5 | if (!c) { 6 | const n = window._ctx_ || { url: window.location.pathname }; 7 | c = a((i) => ({ ...S, ...n })); 8 | } 9 | if (e = c, !t || !t.length) return e.getState(); 10 | if (t.length === 1) { 11 | if (typeof t[0] == "string") return e.getState()[t[0]]; 12 | if (typeof t[0] == "object") return e.setState(t[0]); 13 | console.error("MiNi: unknown store argument"); 14 | } else if (t.length === 2) { 15 | if (typeof t[0] != "string") return console.error("MiNi: unknown store argument"); 16 | if (typeof t[1] == "function") { 17 | const n = e.getState()[t[0]]; 18 | t[1] = t[1](n); 19 | } 20 | return e.setState({ [t[0]]: t[1] }); 21 | } else console.error("MiNi: store has too many arguments"); 22 | } 23 | const r = (t) => { 24 | let e; 25 | const n = (s, u) => { 26 | const o = typeof s == "function" ? s(e) : s; 27 | Object.is(o, e) || (e = u ?? (typeof o != "object" || o === null) ? o : Object.assign({}, e, o)); 28 | }, i = () => e, l = { setState: n, getState: i, getInitialState: () => f }, f = e = t(n, i, l); 29 | return l; 30 | }, a = (t) => t ? r(t) : r; 31 | export { 32 | y as default 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xdadda/mini", 3 | "version": "0.2.15", 4 | "description": "lightweight declarative reactive JS framework based on signals, tagged template literals and components", 5 | "type": "module", 6 | "main": "src/index.js", 7 | "sideEffects": false, 8 | "scripts": { 9 | "build": "vite build" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/xdadda/minijs.git" 14 | }, 15 | "keywords": [ 16 | "reactive", 17 | "UI", 18 | "js", 19 | "javascript", 20 | "signals", 21 | "literals" 22 | ], 23 | "author": "xdadda", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/xdadda/minijs/issues" 27 | }, 28 | "homepage": "https://github.com/xdadda/minijs#readme", 29 | "exports": { 30 | ".": { 31 | "import": "./dist/mini.js", 32 | "require": "./dist/mini.cjs" 33 | }, 34 | "./store": { 35 | "import": "./dist/store.js", 36 | "require": "./dist/store.cjs" 37 | }, 38 | "./router": { 39 | "import": "./dist/router.js", 40 | "require": "./dist/router.cjs" 41 | }, 42 | "./components": { 43 | "import": "./dist/components.js", 44 | "require": "./dist/components.cjs" 45 | }, 46 | "./components.css": "./dist/mini.css", 47 | "./dev": "./src/index.js", 48 | "./dev/store": "./src/store/index.js", 49 | "./dev/router": "./src/router/index.js", 50 | "./dev/components": "./src/components/index.js", 51 | "./dev/components.css": "./src/components/alerts.css" 52 | }, 53 | "devDependencies": { 54 | "@xdadda/vite-plugin-minify-template-literals": "^1.0.0", 55 | "vite": "^6.2.6" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/alerts.css: -------------------------------------------------------------------------------- 1 | 2 | .alert { 3 | font-family: inherit; 4 | position: fixed; 5 | display: flex; 6 | z-index: 100001; 7 | overflow: show; 8 | margin: auto; 9 | top: 0; 10 | left: 0; 11 | bottom: 0; 12 | right: 0; 13 | text-align: center; 14 | color: inherit; 15 | } 16 | 17 | /* Transparent Overlay */ 18 | .alert::before { 19 | content: ''; 20 | display: block; 21 | position: fixed; 22 | top: 0; 23 | left: 0; 24 | width: 100%; 25 | height: 100%; 26 | background: rgba(0, 0, 0, .6); 27 | } 28 | 29 | .alert-message { 30 | align-self: center; 31 | color: inherit; 32 | 33 | position: relative; 34 | margin: auto; 35 | border-radius: 20px; 36 | padding: 10px; 37 | 38 | overflow: hidden; white-space: nowrap; text-overflow: ellipsis; 39 | } 40 | .alert-message .msg { 41 | display: flex; 42 | flex-direction: column; 43 | text-align: left; 44 | width: 310px; 45 | padding: 3px; 46 | margin: auto; 47 | overflow: hidden; 48 | text-overflow: ellipsis; 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/components/alerts.js: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // an async and customizable implementation of window.alert()/.prompt()/.confirm() 4 | // without using signals 5 | 6 | import { html, render, onMount } from '../index.js'; 7 | import './alerts.css'; 8 | 9 | //using this instead of crypto.randomUUID() to be able to work not-localhost in dev mode 10 | function uuidv4() { 11 | return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => 12 | (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) 13 | ); 14 | } 15 | 16 | 17 | //@param buttons [{id,onClick,label}] //note first button will have autofocus 18 | //@param onCancel (id) => callback function 19 | //@param onClose (id,value) => callback function 20 | //@param type prompt || undefined //prompt will show an input element 21 | function _Modal({content, buttons, onCancel, onClose, type, placeholder='',width}){ 22 | const alertid=uuidv4(); 23 | 24 | function handleCancel(e){ 25 | e.preventDefault(); 26 | e.stopPropagation(); 27 | onCancel(document.getElementById(alertid)); 28 | } 29 | function handleClose(e){ 30 | e.preventDefault(); 31 | e.stopPropagation(); 32 | if(onClose) return onClose(document.getElementById(alertid),document.getElementById('_in'+alertid).value); 33 | } 34 | function handleClick(e,fn){ 35 | e.preventDefault(); 36 | e.stopPropagation(); 37 | fn(document.getElementById(alertid),document.getElementById('_in'+alertid)?.value); 38 | } 39 | 40 | function handleKey(e){ 41 | if(e.key==="Escape") handleCancel(e); 42 | else if(e.key==="Enter") handleClose(e); 43 | } 44 | 45 | onMount(()=>{ 46 | if(type==='prompt') setTimeout(()=>{document.getElementById('_in'+alertid)?.focus()},10); 47 | else if(buttons) setTimeout(()=>{document.getElementById('_btn'+alertid)?.focus()},10); 48 | }) 49 | 50 | return html` 51 |
52 |
53 |
54 | ${content} 55 | ${type==='prompt' && `
`} 56 |
57 |
58 | ${ buttons?.map((b,i)=> ()=> html` 59 | 65 | `)} 66 |
67 |
68 |
69 | `; 70 | } 71 | 72 | export async function prompt(msg,width,plc){ 73 | 74 | return await new Promise((resolve,reject) => { 75 | const app = document.body.querySelector('div'); //document.querySelector('.app'); 76 | const div = document.createElement('div'); 77 | app.appendChild(div); 78 | function handleClose(el,value){ el.parentElement.remove(); resolve(value) }; 79 | function handleCancel(el){ el.parentElement.remove(); resolve(false) }; 80 | render(div,()=>_Modal({ 81 | content: msg, 82 | buttons: [ 83 | {label:'Cancel', onClick:handleCancel }, 84 | {label:'OK', onClick:handleClose, focus:true } 85 | ], 86 | onClose: handleClose, 87 | onCancel: handleCancel, 88 | type:'prompt', 89 | placeholder:plc, 90 | width 91 | })); 92 | }); 93 | } 94 | 95 | 96 | export async function confirm(msg,width){ 97 | 98 | return await new Promise((resolve,reject) => { 99 | const app = document.body.querySelector('div'); //document.querySelector('.app'); 100 | const div = document.createElement('div'); 101 | app.appendChild(div); 102 | function handleClose(el){ el.parentElement.remove(); resolve(true) }; 103 | function handleCancel(el){ el.parentElement.remove(); resolve(false) }; 104 | render(div,()=>_Modal({ 105 | content: msg, 106 | buttons: [ 107 | {label:'Cancel', onClick:handleCancel }, 108 | {label:'OK', onClick:handleClose, focus:true } 109 | ], 110 | onCancel: handleCancel, 111 | type:'confirm', 112 | width 113 | })); 114 | }); 115 | } 116 | 117 | 118 | export async function alert(msg,width){ 119 | 120 | return await new Promise((resolve,reject) => { 121 | const app = document.body.querySelector('div'); //document.querySelector('.app'); 122 | const div = document.createElement('div'); 123 | app.appendChild(div); 124 | function handleClose(el){ el.parentElement.remove(); resolve(false) }; 125 | render(div,()=>_Modal({ 126 | content: msg, 127 | buttons: [{label:'OK', onClick:handleClose, focus:true }], 128 | onCancel: handleClose, 129 | type:'alert', 130 | width 131 | })); 132 | }); 133 | } -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { alert, confirm, prompt } from './alerts.js'; 2 | export { virtual } from './virtual.js'; -------------------------------------------------------------------------------- /src/components/virtual.js: -------------------------------------------------------------------------------- 1 | import { html, reactive, onMount, onUnmount, map } from '../index.js' 2 | 3 | 4 | //note parentElement need to have a specific height 5 | export function virtual({ 6 | renderItem, //(idx)=>{..} 7 | itemCount, //# of items (can be a number, signal of function) 8 | rowHeight, //in pixels 9 | nodePadding, //number of "padding" items 10 | onUpdateRow, //triggered when virtual list is updated 11 | onUpdateScroll, //triggered when virtual list is updated 12 | onMounted, 13 | }){ 14 | 15 | const virtualid=reactive(); //=reactive(uuidv4()); //needs to be a signal for hydration 16 | let animationFrame, container; 17 | let _prev_startN, _itemCount; 18 | 19 | const visibleChildren = reactive([]) 20 | 21 | if(itemCount.signal) _itemCount=itemCount; 22 | else if(typeof itemCount==='function') _itemCount=reactive(itemCount); 23 | else _itemCount={value:itemCount}; 24 | 25 | function updateScroll(){ 26 | const _startN = Math.max(0,Math.floor(container.scrollTop / rowHeight) - nodePadding); 27 | const viewportHeight= container.offsetHeight; 28 | let visibleNodesCount = Math.ceil(viewportHeight / rowHeight) + 2 * nodePadding; 29 | visibleNodesCount = Math.min(_itemCount.value - _startN, visibleNodesCount); 30 | 31 | const firstrow = Math.floor(container.scrollTop / rowHeight); 32 | const lastrow=(_prev_startN||0)+visibleNodesCount-1 33 | if(onUpdateRow) onUpdateRow(firstrow,lastrow) 34 | 35 | const offsetY = _startN * rowHeight; 36 | virtualid._value.firstElementChild.style.transform=`translateY(${offsetY}px)` 37 | if(onUpdateScroll) onUpdateScroll(container.scrollTop) 38 | 39 | if(_prev_startN===undefined || _prev_startN!==_startN) { 40 | _prev_startN=_startN; 41 | visibleChildren.value= new Array(visibleNodesCount||0).fill(null).map((_, index) => index+_startN); 42 | } 43 | } 44 | 45 | function _eventListenerFn(){ 46 | if (animationFrame) cancelAnimationFrame(animationFrame); 47 | animationFrame = requestAnimationFrame(updateScroll); 48 | } 49 | 50 | const totalContentHeight = _itemCount.value * rowHeight + 'px'; 51 | 52 | onMount(()=>{ 53 | const el = virtualid._value 54 | if(!el) return 55 | container=el.parentElement 56 | if(container.style.overflowY!=='auto') container.style.overflowY='auto' 57 | container.addEventListener("scroll", _eventListenerFn); 58 | updateScroll() 59 | if(onMounted) onMounted() 60 | 61 | }) 62 | 63 | onUnmount(()=>{ 64 | //console.log('unmount VirtualScroll') 65 | container?.removeEventListener("scroll", _eventListenerFn); 66 | }) 67 | 68 | 69 | return html` 70 |
73 |
74 | ${map(visibleChildren,renderItem)} 75 |
76 |
77 | ` 78 | } 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { html } from './mini_html.js'; 2 | export { reactive, untrack } from './mini_dom_signal.js'; 3 | export { html } 4 | 5 | export { render, onMount, onUnmount, map } from './mini_dom.js'; 6 | 7 | 8 | ////////// SUSPENSE ////////////////////////////// 9 | /* 10 | Note: ideally use Suspense only for lazy chunk loading 11 | const Test = lazy(() => import('./test.js')); 12 | ...code here... 13 | return html`...${ Suspense( ()=>Test({...}), ()=>html`loading...` ) }... ` 14 | 15 | Beta: Suspense for async components ... [it works, but it's not reliable with onMount/ onUnmount ] 16 | */ 17 | 18 | export function Suspense(componentFn,loaderFn){ 19 | const loader = ()=>loaderFn(); 20 | loader._loader=true; 21 | //const wrap = () => componentFn() 22 | componentFn._suspense=true; 23 | return html`${loader}${componentFn}`; 24 | } 25 | 26 | 27 | 28 | export function lazy(factory, mod='default'){ 29 | return async (...args)=>{ 30 | const fn = (await factory())[mod] 31 | if(!fn) return console.error(`MiNi lazy: ${factory} missing "${mod}" export`) 32 | return fn(...args) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/mini_dom.js: -------------------------------------------------------------------------------- 1 | import { reactive, untrack } from './mini_dom_signal.js'; 2 | import { html } from './mini_html.js'; 3 | import { createFragment, replaceFragments, clearFragment } from './mini_dom_fragments.js'; 4 | import { diffArrays } from './mini_dom_map.js'; 5 | 6 | export { render, renderClient, onMount, onUnmount, map }; 7 | 8 | 9 | let mountqueue=[]; 10 | function onMount(fn){ 11 | mountqueue.push(fn); 12 | } 13 | let unmountqueue=[]; 14 | function onUnmount(fn){ 15 | unmountqueue.push(fn); 16 | } 17 | 18 | function _extractunmounts(o, arr=[]) { 19 | if(o.unmount) {arr.push(o.unmount);delete o.unmount;} 20 | Object.getOwnPropertySymbols(o).forEach(k=>{if(o[k]?.frag) {_extractunmounts(o[k],arr)}}); 21 | return arr.flat().reverse(); 22 | } 23 | 24 | 25 | function renderArray(v, owner) { 26 | const tmp = document.createComment('rx'); 27 | let nodesarray= new Array(v.length); 28 | nodesarray=nodesarray.fill(0).map(()=>tmp.cloneNode()); 29 | owner.frag = replaceFragments(owner.frag, nodesarray); 30 | for(let i=0; iv[i], owner); 32 | } 33 | } 34 | 35 | function renderSync(val, owner) { 36 | if(!owner) return; 37 | if(owner.hidden) owner.hidden=false; 38 | if(val?.html){ //render SYNC COMPONENT 39 | renderClient(owner.frag, val, owner); 40 | if(owner.mount) setTimeout(()=>{owner.mount?.forEach(f=>f());owner.mount=undefined},0); //nextTick 41 | } 42 | else if(Array.isArray(val)) { //render ARRAY 43 | renderArray(val, owner); 44 | if(owner.mount) setTimeout(()=>{owner.mount?.forEach(f=>f());owner.mount=undefined},0); //nextTick 45 | } 46 | else if(val===false || val==='') { //hide FRAGMENT 47 | clearFragment(owner.frag); 48 | owner.hidden=true; 49 | } 50 | else { //render VALUE 51 | let node = owner.frag.prev.nextSibling 52 | if(node.nodeType!==3){ 53 | const tmp = document.createTextNode(''); 54 | node.replaceWith(tmp); 55 | node=tmp; 56 | } 57 | if(val!==undefined && node.data!==val) node.data=val; 58 | } 59 | } 60 | 61 | function clearSuspenseLoader(owner){ 62 | //find loader's fragment 63 | const loaderid = Object.getOwnPropertySymbols(owner).filter(k=>owner[k]?.loader)?.[0] 64 | if(!loaderid) return 65 | const loaderfrag = owner[loaderid].frag 66 | clearFragment(loaderfrag) 67 | delete owner[loaderid] 68 | } 69 | 70 | //set all children to stale and remove them to functions' tree! 71 | //this will stop reactivity next time they get called 72 | function _staleChildren(o) { 73 | Object.getOwnPropertySymbols(o).forEach(k=>{if(o[k]?.frag) {o[k].stale=true; _staleChildren(o[k]); delete o[k]; }}) 74 | } 75 | 76 | function renderFunction(placeholder, fn, owner) { 77 | const myid=Symbol('$comp') 78 | owner[myid]={}; 79 | //create fragment markers to allow hide/show switch 80 | placeholder.before(document.createTextNode('')); 81 | placeholder.after(document.createTextNode('')); 82 | owner[myid].frag=createFragment(placeholder); 83 | 84 | //inject or modify owner/myid for internal functions 85 | if(fn._map) return fn(owner,myid) //map() will call diffArray 86 | ////////////////////// 87 | 88 | //console.log('NEW - reactive') 89 | reactive(async()=>{ 90 | 91 | //intercept stale condition to stop reactivity of this function 92 | if(!owner[myid]) return; 93 | if(owner.stale || owner[myid].stale) return delete owner[myid]; 94 | ////////////////////// 95 | 96 | _staleChildren(owner[myid]) 97 | const mountlen = mountqueue.length, unmountlen = unmountqueue.length; 98 | const unmountlist = _extractunmounts(owner[myid]); 99 | if(unmountlist.length) unmountlist.forEach(f=>(typeof f==='function'&&f())); 100 | 101 | //THIS is where the reactivity magic happens 102 | //fn: COMPONENT [()=>Component()], COMPONENT/BOOLEAN ()=>signal && Component(), COMPUTED VALUE ()=>signal.value, ... 103 | let val = fn(); 104 | if(val instanceof Promise) val=await val //an async component 105 | ////////////////////// 106 | if(!owner[myid]) return; 107 | 108 | //intercept mount/unmount 109 | if(mountqueue.length>mountlen) { 110 | const x = mountqueue.length-mountlen; 111 | owner[myid].mount = mountqueue.splice(-x,x); 112 | } 113 | if(unmountqueue.length>unmountlen) { 114 | const x = unmountqueue.length-unmountlen; 115 | owner[myid].unmount = unmountqueue.splice(-x,x); 116 | } 117 | ////////////////////// 118 | 119 | //inject or modify owner/myid for internal functions 120 | if(typeof val === 'function' && val?._map) return val(owner,myid) //map() will call diffArray 121 | 122 | if(fn._loader) owner[myid].loader=true 123 | if(fn._suspense) { 124 | if(!owner[myid]) return 125 | owner[myid].suspense=true 126 | clearSuspenseLoader(owner) 127 | delete fn._suspense 128 | } 129 | ////////////////////// 130 | val = typeof val === 'function' && !val?.html ? untrack(val) : val; 131 | renderSync(val, owner[myid]) 132 | 133 | },{effect:true}) 134 | } 135 | 136 | 137 | function map(array,renderItem) { 138 | const fn = function(...args) { 139 | const [owner,myid] = args 140 | clearFragment(owner[myid].frag); //let's have an empty fragment to begin with 141 | 142 | let oldarray; 143 | reactive(()=>{ 144 | if(!owner[myid]) return; 145 | if(owner.stale || owner[myid].stale) return delete owner[myid]; //this will stop reactive chain 146 | 147 | const newarray = array.signal? array.value : array; 148 | untrack(()=>diffArrays(owner[myid].frag, oldarray, newarray, renderItem, owner[myid])); 149 | oldarray=newarray; 150 | },{effect:true}); 151 | } 152 | fn._map=true 153 | return fn 154 | } 155 | 156 | 157 | function renderAttribute(placeholder, key, v, owner) { 158 | function setAttr(el,key,val) { 159 | if (val === true) el.setAttribute(key, key); 160 | else if (val === false) el.removeAttribute(key); 161 | else if (val !== false && val != null) el.setAttribute(key, val); 162 | } 163 | 164 | const myid=Symbol('$attr') 165 | owner[myid]={}; 166 | owner[myid].frag=createFragment(placeholder); 167 | 168 | const memo = reactive(v); 169 | 170 | reactive(()=>{ 171 | if(!owner[myid]) return; 172 | if(owner.stale || owner[myid].stale) return delete owner[myid]; //this will stop reactive chain 173 | 174 | let val= memo.value; 175 | if(key==='value') { 176 | if(val.signal) placeholder.value=val.value; 177 | else placeholder.value=val; 178 | } 179 | else if (key==='ref') { 180 | placeholder.removeAttribute(key); 181 | if(v.signal) { 182 | v.value=placeholder; 183 | } 184 | } else { 185 | setAttr(placeholder,key,val); 186 | } 187 | }, {effect: true}); 188 | } 189 | 190 | function findPlaceholder(root,commentText) { 191 | //crawl for comments 192 | return document.createTreeWalker(root, 128, { acceptNode: (node) => node.textContent === commentText? 1 : 2 }).nextNode(); 193 | } 194 | 195 | //@param frag: fragment/node to replace with t.html 196 | //@param t: obj {html,reactarray} from html`` 197 | function renderClient(frag, t, _owner={0:{}}) { 198 | //console.log('renderClient >>', t.name||t,) 199 | 200 | if(!frag) return console.error('MiNi: renderClient missing node element'); 201 | if(frag.nodeType) frag=createFragment(frag); 202 | 203 | if(typeof t === 'function' && !t.html) t=untrack(t); 204 | if(typeof t === 'function' && t.html) t=untrack(t); 205 | 206 | if(t.html===undefined) return console.error('MiNi: unknown input to renderClient',t); 207 | //console.log('renderClient ==',frag,t); 208 | 209 | const {html, reactarray} = t; //generated by html`` 210 | const tmplt = document.createElement('template'); 211 | tmplt.innerHTML=html; 212 | 213 | //resolve all variables in reactarray and populate template's reactive placeholders (<--!rx0-->) 214 | const root=frag.prev?.parentNode || frag.parent; 215 | //NOTE: use of prev.parentNode needed when hydrating fragment (frag.prev and .next are moved to the DOM but .parent cannot be updated) 216 | for (let i=0; i>',placeholder,v); 228 | renderFunction(placeholder, v, _owner); 229 | } 230 | else if(v instanceof Promise) { //ALLOWING ${asynccomponent()} WILL BRAKE MOUNT/UNMOUNT 231 | console.error('MiNi: wrap async component in ()=>{}'); 232 | } 233 | else if(v.html === true) { //TEMPLATE: html`` which returns an obj {html,reactarray} 234 | placeholder=renderClient(placeholder, v, _owner,); 235 | } 236 | else console.error('MiNi: unknown node value',v); 237 | } 238 | break; 239 | case '@': 240 | case ':': 241 | placeholder = tmplt.content.querySelector(`[${key+i}]`); 242 | if(!placeholder) console.error('MiNi: cannot find attribute',key+i); 243 | else { 244 | placeholder.removeAttribute(key+i); 245 | if(type===':') { 246 | renderAttribute(placeholder, key, v, _owner); 247 | } 248 | else if(type==='@') placeholder.addEventListener(key.toLowerCase(), v, key==='onwheel'?{ passive: false }:{}); 249 | else console.error('MiNi: unknown special attr',type,i); 250 | } 251 | break; 252 | } 253 | } 254 | 255 | //add template to the DOM 256 | //console.log('add to DOM',frag.next,root); 257 | clearFragment(frag); 258 | if(frag.next) frag.next.before(tmplt.content); 259 | else root?.appendChild(tmplt.content); 260 | frag.destroy=()=>{ 261 | const unmountlist = _extractunmounts(_owner); 262 | if(unmountlist.length) unmountlist.forEach(f=>(typeof f==='function'&&f())); 263 | _staleChildren(_owner) 264 | clearFragment(frag) 265 | } 266 | return frag; 267 | } 268 | 269 | 270 | async function render(root, rootComponent, debug) { 271 | root.appendChild(document.createElement('div')); 272 | if(typeof rootComponent!=='function') return console.error('MiNi: render 2nd arg must be a function') 273 | let rootowner ={0:{}}; 274 | try { 275 | const el = await renderClient(root.children[0], html`${()=>rootComponent()}`,rootowner); // 276 | if(debug) console.log('rootowner',rootowner); 277 | return el 278 | } 279 | catch(err){ 280 | console.error('MiNi: render', err); 281 | } 282 | } 283 | 284 | -------------------------------------------------------------------------------- /src/mini_dom_fragments.js: -------------------------------------------------------------------------------- 1 | export { createFragment, replaceFragments, clearFragment, refreshFragment, extractFragmentFromDom } 2 | 3 | /////////////// FRAGMENTS //////////////////////// 4 | 5 | function createFragment(nodes){ 6 | if(nodes.nodeType) { //wrap single node in frag 7 | let frag=[nodes]; 8 | frag.parent=nodes.parentNode; 9 | frag.next=nodes.nextSibling; 10 | frag.prev=nodes.previousSibling; 11 | frag.fragment=true; 12 | return frag; 13 | } 14 | else if(Array.isArray(nodes) && !nodes.fragment) { //wrap array in frag 15 | if(!nodes[0]) return false; 16 | nodes[0].before(document.createTextNode('')); 17 | nodes[nodes.length-1].after(document.createTextNode('')); 18 | nodes.parent=nodes[0].parentNode; 19 | nodes.prev=nodes[0].previousSibling; 20 | nodes.next=nodes[nodes.length-1].nextSibling; 21 | nodes.fragment=true; 22 | return nodes; 23 | } 24 | else { 25 | console.error('MiNi: unknown input for createFragment'); 26 | return false; 27 | } 28 | } 29 | 30 | function replaceFragments(oldfrag,newfrag) { 31 | if(oldfrag.fragment && Array.isArray(oldfrag) && Array.isArray(newfrag)){ 32 | newfrag.prev=oldfrag.prev; 33 | newfrag.next=oldfrag.next; 34 | newfrag.parent=oldfrag.parent; 35 | clearFragment(oldfrag); 36 | 37 | if(newfrag.next) newfrag.next.before(...newfrag); 38 | else newfrag.parent.append(...newfrag); 39 | 40 | newfrag.fragment=true; 41 | } 42 | else console.error('MiNi: replaceFragments unknown input',oldfrag,newfrag); 43 | return newfrag; 44 | } 45 | 46 | function clearFragment(frag){ 47 | if(!frag.prev&&!frag.next&&frag.parent) { //FASTPATH CLEAR PARENT 48 | frag.parent.textContent=''; //clear parent 49 | } 50 | else if(frag.prev?.nextSibling===frag.next) return frag; 51 | else { 52 | let current = frag.prev?.nextSibling || (frag.next?.parentElement?.firstChild || frag.parent?.firstChild); 53 | if(!current) return; 54 | while(current!==frag.next){ 55 | if(!current) return console.error('MiNi: clearFragment missing node',frag); 56 | const nxt=current.nextSibling; 57 | current.remove(); 58 | current=nxt; 59 | } 60 | } 61 | if(frag.length) frag.splice(0,frag.length); //clear array 62 | return frag; 63 | } 64 | 65 | ////////////////////////////////////////////////// 66 | 67 | function refreshFragment(frag){ 68 | if(!frag.prev&&!frag.next) { //FASTPATH GET PARENT CHILDNODE 69 | frag.splice(0,frag.length); //clear array 70 | frag.splice(0,0,...frag.parent.childNodes); //update array 71 | } 72 | else { 73 | let current = frag.prev?.nextSibling || frag.parent.firstChild; 74 | let tmparr = []; 75 | while(current!==frag.next){ 76 | if(!current) return console.error('MiNi: updateFragment missing node',frag); 77 | const nxt=current.nextSibling; 78 | tmparr.push(current); 79 | current=nxt; 80 | } 81 | if(frag.length) frag.splice(0,frag.length); //clear array 82 | frag.splice(0,0,...tmparr); //update array 83 | } 84 | return frag; 85 | } 86 | 87 | function extractFragmentFromDom(node){ 88 | const id = node.previousSibling?.textContent; 89 | if(!id) return console.error('MiNi: node is not a fragment',node); 90 | let frag = [], tnode = node; 91 | do{ 92 | //console.log('extract>>>',id,tnode, tnode.textContent,tnode.nextSibling) 93 | frag.push(tnode); 94 | tnode=tnode.nextSibling; 95 | if(!tnode) break; 96 | } while(!(tnode.nodeType===8&&tnode.textContent===id)); 97 | frag=createFragment(frag); 98 | return frag; 99 | } 100 | -------------------------------------------------------------------------------- /src/mini_dom_map.js: -------------------------------------------------------------------------------- 1 | ////////// DOM ARRAYS //////////////////////////// 2 | //BASED ON https://github.com/WebReflection/udomdiff 3 | export { diffArrays}; 4 | import { renderClient } from './mini_dom.js'; 5 | import { html } from './mini_html.js'; 6 | import { refreshFragment, clearFragment } from './mini_dom_fragments.js'; 7 | 8 | 9 | //insert item before node 10 | function insertHTML(item,frag,node,owner){ 11 | const t = document.createComment(''); 12 | frag.parent.insertBefore(t,node); 13 | const newid=Symbol('$item'); 14 | owner[newid]={frag:true}; //required for _extractunmounts && _staleChildren 15 | const _frag = renderClient(t,item,owner[newid]); 16 | if(_frag.prev) _frag.prev.nextElementSibling.myid=newid; 17 | return t; 18 | } 19 | 20 | //replace node with item 21 | function replaceHTML(item,node,owner){ 22 | unmountSingleNode(node.myid,owner); 23 | const newid=Symbol('$item'); 24 | owner[newid]={frag:true}; //required for _extractunmounts && _staleChildren 25 | const _frag = renderClient(node,item,owner[newid]); 26 | if(_frag.prev) _frag.prev.nextElementSibling.myid=newid; 27 | //console.log('replaceHTML',_frag,owner[myid][newid]) 28 | return _frag[0]; 29 | } 30 | 31 | 32 | function _extractunmounts(o, arr=[]) { 33 | if(o.unmount) {arr.push(o.unmount);delete o.unmount;} 34 | Object.getOwnPropertySymbols(o).forEach(k=>{if(o[k]?.frag) {_extractunmounts(o[k],arr)}}); 35 | return arr.flat().reverse(); 36 | } 37 | function _staleChildren(o){ 38 | Object.getOwnPropertySymbols(o).forEach(k=>{if(o[k]?.frag) {o[k].stale=true; _staleChildren(o[k])}}) 39 | } 40 | 41 | function unmountSingleNode(nodeid,owner){ 42 | if(!owner || !owner[nodeid]) return 43 | const unmountlist = _extractunmounts(owner[nodeid]); 44 | if(unmountlist.length) unmountlist.forEach(f=>(typeof f==='function'&&f())); 45 | _staleChildren(owner[nodeid]) 46 | delete owner[nodeid] 47 | } 48 | 49 | function unmountAllNodes(owner){ 50 | if(!owner) return 51 | const unmountlist = _extractunmounts(owner); 52 | if(unmountlist.length) unmountlist.forEach(f=>(typeof f==='function'&&f())); 53 | _staleChildren(owner) 54 | Object.getOwnPropertySymbols(owner).forEach(k=>delete owner[k]); 55 | } 56 | 57 | const DEBUGarr = false; 58 | const DEBUGbmk = false; 59 | //modified version of https://github.com/WebReflection/udomdiff/blob/main/esm/index.js 60 | function diffArrays(frag, a=[], b=[], fn, owner) { //a = old, b=new 61 | let stime = DEBUGarr&&Date.now(); 62 | refreshFragment(frag); 63 | DEBUGarr && console.log('diffArrays',owner,frag,a,b); 64 | let before = frag.next; 65 | let parent = frag.parent; 66 | let aStart=0, aEnd=a?.length; 67 | let bStart=0, bEnd=b?.length, bLength=bEnd; 68 | let map=null, temp= new Array(bLength), tempidx=false; 69 | let mapped=frag; 70 | 71 | // fast path for empty array 72 | if(bLength=== 0) { 73 | DEBUGarr && console.log('fast empty',(Date.now()-stime)+'ms'); 74 | unmountAllNodes(owner); 75 | clearFragment(frag); 76 | DEBUGbmk&&console.log('diffArrays',Date.now()-stime+'ms'); 77 | return; 78 | } 79 | 80 | 81 | while (aStart < aEnd || bStart < bEnd) { 82 | // fast path: append head, tail, or nodes in between 83 | if(aEnd===aStart) { 84 | //console.log('append',aEnd,aStart,bStart,bEnd,temp) 85 | const node = bEnd < bLength ? 86 | (bStart && temp[bStart-1] ? 87 | temp[bStart-1].nextSibling : 88 | temp[bEnd] ) : 89 | before; 90 | 91 | while (bStart < bEnd) { 92 | if(temp && temp[bStart]) { 93 | DEBUGarr && console.log('move FAST'); 94 | parent.insertBefore(temp[bStart], node); 95 | } 96 | else { 97 | DEBUGarr && console.log('insert FAST'); 98 | temp[bStart]= insertHTML(()=>fn(b[bStart]), frag, node, owner); 99 | } 100 | bStart++; 101 | } 102 | } 103 | // fast path: remove head or tail 104 | else if (bEnd === bStart) { 105 | while (aStart < aEnd) { 106 | // remove the node only if it's unknown or not live 107 | if (!map || !map.has(a[aStart])) { 108 | DEBUGarr && console.log('remove FAST',parent, mapped[aStart].nextSibling); 109 | unmountSingleNode(mapped[aStart].myid,owner); 110 | parent.removeChild(mapped[aStart]); 111 | } 112 | aStart++; 113 | } 114 | } 115 | //fast path: skip prefix 116 | else if (a[aStart] === b[bStart]) { 117 | temp[bStart]=mapped[aStart]; 118 | aStart++; 119 | bStart++; 120 | DEBUGarr && console.log('skip prefix'); 121 | } 122 | //fast path: skip suffix 123 | else if (a[aEnd - 1] === b[bEnd - 1]) { 124 | temp[bEnd-1]=mapped[aEnd-1]; 125 | aEnd--; 126 | bEnd--; 127 | DEBUGarr && console.log('skip suffix'); 128 | } 129 | //fast path: reverse swap 130 | else if ( 131 | a[aStart] === b[bEnd - 1] && 132 | b[bStart] === a[aEnd - 1] 133 | ) { 134 | DEBUGarr && console.log('swap FAST'); 135 | const node = mapped[--aEnd].nextSibling; 136 | const prev = mapped[aStart++].nextSibling; 137 | parent.insertBefore(mapped[bStart++],node); 138 | parent.insertBefore(mapped[aEnd],prev); 139 | bEnd--; 140 | a[aEnd] = b[bEnd]; 141 | } 142 | // map based fallback, "slow" path 143 | else { 144 | 145 | //create a map of new items indexes 146 | if (!map) { 147 | map = new Map; 148 | let i = bStart; 149 | while (i < bEnd) 150 | map.set(b[i], i++); 151 | } 152 | //create map of existing items in new locations 153 | let reusingNodes = bStart + bLength - bEnd; 154 | if(!tempidx) { 155 | let i = aStart; 156 | while (i < aEnd) { 157 | const index = map.get(a[i]); 158 | if(index!==undefined) { 159 | temp[index]=mapped[i]; 160 | reusingNodes++; 161 | } 162 | i++; 163 | } 164 | tempidx=true; 165 | 166 | //fast path: full replace 167 | if(!reusingNodes) { 168 | //parent.textContent='' //clear parent 169 | unmountAllNodes(owner); 170 | clearFragment(frag); 171 | return diffArrays(frag,[],b,fn,owner); 172 | } 173 | } 174 | 175 | 176 | // if it's a future node, hence it needs some handling 177 | if (map.has(a[aStart])) { 178 | const index = map.get(a[aStart]); 179 | // if node is not already processed, look on demand for the next LCS 180 | if (bStart < index && index < bEnd) { 181 | // counts the amount of nodes that are the same in the future 182 | let i = aStart; 183 | let sequence = 1; 184 | while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence)) 185 | sequence++; 186 | if (sequence > (index - bStart)) { 187 | const node = mapped[aStart]; 188 | while (bStart < index) { 189 | if(temp && temp[bStart]) { 190 | DEBUGarr && console.log('move LCS'); 191 | parent.insertBefore(temp[bStart], node); 192 | } 193 | else { 194 | DEBUGarr && console.log('insert LCS'); 195 | temp[bStart]= insertHTML(()=>fn(b[bStart]), frag, node, owner); 196 | } 197 | bStart++; 198 | } 199 | } 200 | else { 201 | if(temp && temp[bStart]) { 202 | DEBUGarr && console.log('swap'); 203 | parent.replaceChild(temp[bStart],mapped[aStart]); 204 | } 205 | else { 206 | DEBUGarr && console.log('insert'); 207 | temp[bStart]= replaceHTML(()=>fn(b[bStart]), mapped[aStart], owner); 208 | } 209 | aStart++; 210 | bStart++; 211 | } 212 | } 213 | else 214 | aStart++; 215 | } 216 | else { 217 | DEBUGarr && console.log('remove'); 218 | unmountSingleNode(mapped[aStart].myid, owner); 219 | parent.removeChild(mapped[aStart++]); 220 | } 221 | 222 | } 223 | } 224 | DEBUGbmk && console.log('diffArrays',Date.now()-stime+'ms'); 225 | } 226 | 227 | ////////////////////////////////////////////////// 228 | 229 | -------------------------------------------------------------------------------- /src/mini_dom_signal.js: -------------------------------------------------------------------------------- 1 | //REACTIVELY SOURCE: https://github.com/milomg/reactively 2 | 3 | /** current capture context for identifying @reactive sources (other reactive elements) and cleanups 4 | * - active while evaluating a reactive function body */ 5 | let CurrentReaction = undefined 6 | let CurrentGets = null 7 | let CurrentGetsIndex = 0 8 | 9 | /** A list of non-clean 'effect' nodes that will be updated when stabilize() is called */ 10 | let EffectQueue = [] 11 | 12 | let stabilizeFn = undefined // fn to call if there are dirty effect nodes 13 | let stabilizationQueued = false // stabilizeFn() is queued to run after this event loop 14 | 15 | /** reactive nodes are marked dirty when their source values change TBD*/ 16 | const CacheClean = 0 // reactive value is valid, no need to recompute 17 | const CacheCheck = 1 // reactive value might be stale, check parent nodes to decide whether to recompute 18 | const CacheDirty = 2 // reactive value is invalid, parents have changed, value needs to be recomputed 19 | 20 | export function logDirty(_enable) { 21 | // TBD for a debug build 22 | } 23 | 24 | export function reactive(fnOrValue, params) { 25 | const node = new Reactive(fnOrValue, params?.effect, params?.label) 26 | if (params?.equals) { 27 | node.equals = params.equals 28 | } 29 | node.signal = true 30 | return node 31 | } 32 | 33 | function defaultEquality(a, b) { 34 | return a === b 35 | } 36 | 37 | /** A reactive element contains a mutable value that can be observed by other reactive elements. 38 | * 39 | * The property can be modified externally by calling set(). 40 | * 41 | * Reactive elements may also contain a 0-ary function body that produces a new value using 42 | * values from other reactive elements. 43 | * 44 | * Dependencies on other elements are captured dynamically as the 'reactive' function body executes. 45 | * 46 | * The reactive function is re-evaluated when any of its dependencies change, and the result is 47 | * cached. 48 | */ 49 | export class Reactive { 50 | _value; 51 | fn; 52 | observers = null // nodes that have us as sources (down links) 53 | sources = null // sources in reference order, not deduplicated (up links) 54 | 55 | state; 56 | effect; 57 | label; 58 | cleanups = [] 59 | equals = defaultEquality 60 | 61 | constructor(fnOrValue, effect, label) { 62 | if (typeof fnOrValue === "function") { 63 | this.fn = fnOrValue 64 | this._value = undefined 65 | this.effect = effect || false 66 | this.state = CacheDirty 67 | // debugDirty && console.log("initial dirty (fn)", label); 68 | if (effect) { 69 | EffectQueue.push(this) 70 | stabilizeFn?.(this) 71 | } 72 | } else { 73 | this.fn = undefined 74 | this._value = fnOrValue 75 | this.state = CacheClean 76 | this.effect = false 77 | } 78 | if (label) { 79 | this.label = label 80 | } 81 | } 82 | 83 | get value() { 84 | return this.get() 85 | } 86 | 87 | set value(v) { 88 | this.set(v) 89 | } 90 | 91 | get() { 92 | if (CurrentReaction) { 93 | if ( 94 | !CurrentGets && 95 | CurrentReaction.sources && 96 | CurrentReaction.sources[CurrentGetsIndex] == this 97 | ) { 98 | CurrentGetsIndex++ 99 | } else { 100 | if (!CurrentGets) CurrentGets = [this] 101 | else CurrentGets.push(this) 102 | } 103 | } 104 | if (this.fn) this.updateIfNecessary() 105 | return this._value 106 | } 107 | 108 | set(fnOrValue) { 109 | if (typeof fnOrValue === "function") { 110 | const fn = fnOrValue 111 | if (fn !== this.fn) { 112 | this.stale(CacheDirty) 113 | } 114 | this.fn = fn 115 | } else { 116 | if (this.fn) { 117 | this.removeParentObservers(0) 118 | this.sources = null 119 | this.fn = undefined 120 | } 121 | const value = fnOrValue 122 | if (!this.equals(this._value, value)) { 123 | if (this.observers) { 124 | for (let i = 0; i < this.observers.length; i++) { 125 | const observer = this.observers[i] 126 | observer.stale(CacheDirty) 127 | } 128 | } 129 | this._value = value 130 | } 131 | } 132 | } 133 | 134 | stale(state) { 135 | if (this.state < state) { 136 | // If we were previously clean, then we know that we may need to update to get the new value 137 | if (this.state === CacheClean && this.effect) { 138 | EffectQueue.push(this) 139 | stabilizeFn?.(this) 140 | } 141 | 142 | this.state = state 143 | if (this.observers) { 144 | for (let i = 0; i < this.observers.length; i++) { 145 | this.observers[i].stale(CacheCheck) 146 | } 147 | } 148 | } 149 | } 150 | 151 | /** run the computation fn, updating the cached value */ 152 | update() { 153 | const oldValue = this._value 154 | 155 | /* Evalute the reactive function body, dynamically capturing any other reactives used */ 156 | const prevReaction = CurrentReaction 157 | const prevGets = CurrentGets 158 | const prevIndex = CurrentGetsIndex 159 | 160 | CurrentReaction = this 161 | CurrentGets = null // prevent TS from thinking CurrentGets is null below 162 | CurrentGetsIndex = 0 163 | 164 | try { 165 | if (this.cleanups.length) { 166 | this.cleanups.forEach(c => c(this._value)) 167 | this.cleanups = [] 168 | } 169 | this._value = this.fn() 170 | 171 | // if the sources have changed, update source & observer links 172 | if (CurrentGets) { 173 | // remove all old sources' .observers links to us 174 | this.removeParentObservers(CurrentGetsIndex) 175 | // update source up links 176 | if (this.sources && CurrentGetsIndex > 0) { 177 | this.sources.length = CurrentGetsIndex + CurrentGets.length 178 | for (let i = 0; i < CurrentGets.length; i++) { 179 | this.sources[CurrentGetsIndex + i] = CurrentGets[i] 180 | } 181 | } else { 182 | this.sources = CurrentGets 183 | } 184 | 185 | for (let i = CurrentGetsIndex; i < this.sources.length; i++) { 186 | // Add ourselves to the end of the parent .observers array 187 | const source = this.sources[i] 188 | if (!source.observers) { 189 | source.observers = [this] 190 | } else { 191 | source.observers.push(this) 192 | } 193 | } 194 | } else if (this.sources && CurrentGetsIndex < this.sources.length) { 195 | // remove all old sources' .observers links to us 196 | this.removeParentObservers(CurrentGetsIndex) 197 | this.sources.length = CurrentGetsIndex 198 | } 199 | } finally { 200 | CurrentGets = prevGets 201 | CurrentReaction = prevReaction 202 | CurrentGetsIndex = prevIndex 203 | } 204 | 205 | // handles diamond depenendencies if we're the parent of a diamond. 206 | if (!this.equals(oldValue, this._value) && this.observers) { 207 | // We've changed value, so mark our children as dirty so they'll reevaluate 208 | for (let i = 0; i < this.observers.length; i++) { 209 | const observer = this.observers[i] 210 | observer.state = CacheDirty 211 | } 212 | } 213 | 214 | // We've rerun with the latest values from all of our sources. 215 | // This means that we no longer need to update until a signal changes 216 | this.state = CacheClean 217 | } 218 | 219 | /** update() if dirty, or a parent turns out to be dirty. */ 220 | updateIfNecessary() { 221 | // If we are potentially dirty, see if we have a parent who has actually changed value 222 | if (this.state === CacheCheck) { 223 | for (const source of this.sources) { 224 | source.updateIfNecessary() // updateIfNecessary() can change this.state 225 | if (this.state === CacheDirty) { 226 | // Stop the loop here so we won't trigger updates on other parents unnecessarily 227 | // If our computation changes to no longer use some sources, we don't 228 | // want to update() a source we used last time, but now don't use. 229 | break 230 | } 231 | } 232 | } 233 | 234 | // If we were already dirty or marked dirty by the step above, update. 235 | if (this.state === CacheDirty) { 236 | this.update() 237 | } 238 | 239 | // By now, we're clean 240 | this.state = CacheClean 241 | } 242 | 243 | removeParentObservers(index) { 244 | if (!this.sources) return 245 | for (let i = index; i < this.sources.length; i++) { 246 | const source = this.sources[i] // We don't actually delete sources here because we're replacing the entire array soon 247 | const swap = source.observers.findIndex(v => v === this) 248 | source.observers[swap] = source.observers[source.observers.length - 1] 249 | source.observers.pop() 250 | } 251 | } 252 | } 253 | 254 | export function untrack(fn) { 255 | const prevReaction = CurrentReaction 256 | const prevGets = CurrentGets 257 | const prevIndex = CurrentGetsIndex 258 | 259 | CurrentReaction = undefined 260 | CurrentGets = null 261 | CurrentGetsIndex = 0 262 | 263 | const out = fn() 264 | 265 | CurrentGets = prevGets 266 | CurrentReaction = prevReaction 267 | CurrentGetsIndex = prevIndex 268 | 269 | return out 270 | } 271 | 272 | export function onCleanup(fn) { 273 | if (CurrentReaction) { 274 | CurrentReaction.cleanups.push(fn) 275 | } else { 276 | console.error("onCleanup must be called from within a @reactive function") 277 | } 278 | } 279 | 280 | /** run all non-clean effect nodes */ 281 | export function stabilize() { 282 | for (let i = 0; i < EffectQueue.length; i++) { 283 | EffectQueue[i].get() 284 | } 285 | EffectQueue.length = 0 286 | } 287 | 288 | /** run a function for each dirty effect node. */ 289 | export function autoStabilize(fn = deferredStabilize) { 290 | //console.log('autoStabilize') 291 | stabilizeFn = fn 292 | } 293 | 294 | autoStabilize() //DEFAULTS 295 | 296 | /** queue stabilize() to run at the next idle time */ 297 | function deferredStabilize() { 298 | if (!stabilizationQueued) { 299 | stabilizationQueued = true 300 | 301 | queueMicrotask(() => { 302 | stabilizationQueued = false 303 | stabilize() 304 | }) 305 | } 306 | } 307 | 308 | ////////////////////////////////////////////////// -------------------------------------------------------------------------------- /src/mini_html.js: -------------------------------------------------------------------------------- 1 | export { html }; 2 | const DEBUG=false; 3 | 4 | ////////// LITERALS TEMPLATE PARSER ////////////// 5 | function parseAttribute(string, v, reactarray, newvalues){ 6 | //console.log('parseAttribute',string) 7 | let id = reactarray.length; 8 | let ev = /[:,@]\S*=/.exec(string); //works w/
not getting confused by style's : and ! 9 | if(!ev) {console.error('MiNi: attribute is missing :@ prefix',string); return false;} 10 | if(ev?.length>1) {console.error('MiNi: attribute is missing "${}"',string); return false;} 11 | if(!string.endsWith(ev[0]+'"')) {console.error('MiNi: attribute '+ev[0]+' is missing "${}"'); return false;} 12 | const tag = ev[0][0]; 13 | const key = ev[0].slice(1,-1); //remove prefix(@:) & suffix(=) 14 | reactarray.push({type:tag,key,v}); 15 | const pos = string.lastIndexOf(key); 16 | string = (string.substring(0,pos-1)+string.substring(pos-1).replace(tag+key,key+id)); //from @key to keyid 17 | if(string.slice(-1)==='"') newvalues.push(''); 18 | else newvalues.push('""'); 19 | return string; 20 | } 21 | 22 | 23 | //parse template literal and put placehoders if functions/ signals are present 24 | function html(litstrings, ...values) { 25 | let strings = [...litstrings]; 26 | DEBUG && console.log('html',strings,values); 27 | function _html(r) { 28 | let newvalues = [], reactarray=[]; 29 | let id=0; 30 | let isAttribute=false; 31 | for (let i=0; i to understand if we're handling an attribute or a tag 35 | const s = strings[i], open = s.lastIndexOf('<'), close=s.lastIndexOf('>'); 36 | isAttribute = ( open!==-1 && (open > close) ) ? true : isAttribute; 37 | isAttribute = ( close!==-1 && (close> open) ) ? false : isAttribute; 38 | 39 | const id=reactarray.length; 40 | if(typeof v === 'function' || v instanceof Promise || v?.signal ) { 41 | if(!isAttribute) { 42 | reactarray.push({type:'node',key:id,v}); 43 | newvalues[i]=``; 44 | } 45 | else { 46 | const str=parseAttribute(strings[i], v, reactarray, newvalues); 47 | if(str) strings[i]=str; 48 | else console.error('MiNi: unknown attribute type',strings[i],v); 49 | } 50 | } 51 | else if(Array.isArray(v)) { 52 | newvalues[i]=''; 53 | v.forEach((item,ix)=>{ 54 | if(typeof item === 'function') { 55 | reactarray.push({type:'node',key:id+':'+ix,v:item}); 56 | newvalues[i]+=``; 57 | } 58 | else newvalues[i]+=item 59 | }) 60 | } 61 | else if(v===false || v===undefined){ 62 | if(isAttribute && strings[i].slice(-1)==='"') { 63 | strings[i]=strings[i].replace(/\s(\S+)$/,'"'); //remove attribute if false 64 | } 65 | newvalues.push(''); 66 | } 67 | else { 68 | newvalues.push(v); 69 | } 70 | } 71 | 72 | 73 | function concatLit(strings,args){ 74 | let html=strings[0]; 75 | for (let i=0; iRouter({ routes, loader:()=>html`
loading...
` }) }` 15 | 16 | */ 17 | 18 | export { Router, getRoute, setRoute }; 19 | import { reactive, Suspense, html } from '../index.js' 20 | import store from '../store/index.js'; 21 | 22 | //opt {nohistory:false, replacehistory:false} 23 | function setRoute(url,args,opt={nohistory:false, replacehistory:true}){ 24 | if(url!==store('$url').value) { 25 | if(args) store('$args',args) 26 | store('$url').value=url||'/'; 27 | //default update browser history 28 | if(!opt.nohistory) { 29 | if(opt.replacehistory) window.history.pushState({}, null, url); 30 | else window.history.replaceState({}, null, url); 31 | } 32 | } 33 | } 34 | 35 | function getRoute(){ 36 | return store('$route'); 37 | } 38 | 39 | 40 | async function Router({routes, loader=false, handleAuth}){ 41 | if(!store('$route')) { 42 | //console.log('>>INIT Router<<') 43 | store('$url',reactive(window.location.href.replace(window.location.origin,''))) 44 | 45 | // INTERCEPT BROWSER BACK BUTTON in the browser 46 | function updateRoute(e) { 47 | e.preventDefault(); // stop request to server for new html 48 | e.stopPropagation(); 49 | const url = window.location.href.replace(window.location.origin,''); 50 | setRoute(url,null,{nohistory:true}); 51 | } 52 | window.addEventListener('popstate', updateRoute); 53 | } 54 | 55 | let url = store('$url').value 56 | let route = getElementFromURL(decodeURIComponent(url), routes); 57 | 58 | let auth=true; 59 | if(handleAuth) { //handleAuth should return true if ok, error url if failed (eg: /login) 60 | auth = await handleAuth(route); 61 | if(!auth) return 62 | } 63 | route.args = store('$args') || route.args || null 64 | store('$route',route); 65 | 66 | if(loader) return Suspense(()=>store('$route').element(route.args), loader); 67 | else return ()=>html`${()=>store('$route').element(route.args)}`; 68 | 69 | } 70 | 71 | 72 | /////////////// ROUTER UTILS ///////////////////////////////////////////////// 73 | 74 | function removeTrailingSlash(url) { 75 | if(url && url!=='/' && url.slice(-1)==='/') url=url.slice(0,-1); //no trailing / 76 | return url; 77 | } 78 | 79 | //parseUrlParam('/test/:id/:od','/test/22/11') --> '{"id":"12","od":"23"}' 80 | function parseUrlParams(str1, str2) { 81 | const tmp = str1.split("/") 82 | .map((key, idx) => [key.replace(":", ""), idx, key.charAt(0) === ":"]); 83 | //.filter(([,,keep]) => keep); 84 | const keys = tmp.filter(([,,keep]) => keep); 85 | const path = tmp.filter(([,,keep]) => !keep);//.map(([key]) => [key]) 86 | const pathParts = str2.split("/"); 87 | const entries = keys.map(([key, idx]) => [key, pathParts[idx]]); 88 | let ok = true; 89 | path.forEach(([key,idx])=> ok=ok&(key===pathParts[idx])); 90 | if(ok)return Object.fromEntries(entries); 91 | else return null; 92 | } 93 | 94 | //parseUrlWildcard('/test/*','/test/22/11') --> "/22/11" 95 | function parseUrlWildcard(rule, str) { 96 | const escapeRegex = (str) => str.replace(/([.*+?^=!:${}()|[\]/\\])/g, "\\$1"); 97 | const regex = new RegExp("^" + rule.split("*").map(escapeRegex).join("(.*)") + "$"); 98 | const arr = str.match(regex); 99 | if(arr && arr[1]!==null) return '/'+arr[1]; 100 | return null; 101 | } 102 | 103 | //this is for routes 104 | function getElementFromURL(_url, array) { 105 | return getElementFromURLandMethod(_url, null, array); 106 | } 107 | 108 | //this is for apis 109 | function getElementFromURLandMethod(_url, method, array) { 110 | let url = _url||'/'; 111 | if(url && url!=='/' && url.slice(-1)==='/') url=url.slice(0,-1); //no trailing / 112 | 113 | //extract queries 114 | let query = url.split('?')[1]; 115 | if(query) { 116 | query = new URLSearchParams(query); 117 | query = Object.fromEntries([...query]); 118 | url = url.split('?')[0]; 119 | } 120 | 121 | const el = array?.find(e=>{ 122 | if(method && e.method!==method) return false; 123 | if(e.path!=='/' && e.path.slice(-1)==='/') e.path=e.path.slice(0,-1); //no trailing / 124 | if(e.path === url) return true; // simple path matching 125 | if(e.path.includes('*')) { 126 | //check if simple /xxx/ without * is present 127 | if(url===e.path.slice(0,-2)) e.subpath='/'; 128 | else e.subpath = parseUrlWildcard(e.path,url); 129 | if(e.subpath) return true; 130 | } 131 | if(e.path.includes(':')) { 132 | const params = parseUrlParams(e.path,url); 133 | if(!params) return false; 134 | e.params = params; 135 | return true; 136 | } 137 | return false; //throw new Error("no route for: "+url) 138 | }) 139 | if(!el) return false; 140 | el.url=url; 141 | el.query=query; 142 | return el; 143 | } -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | const initstate = {}; 2 | let zstore; 3 | 4 | /////////////////////////////////////////// 5 | // to use: import store from 'mini/store' 6 | // store() get all store values 7 | // store(key) get key value from store 8 | // store(key,value) set key value in store 9 | // store(key,(prev)=>{}) set key value in store (fn with old value as arg) 10 | // store({key:value, key:value}) set keys' values in store 11 | export default function store(...args) { 12 | 13 | let tstore; 14 | if(!zstore) { 15 | const session=(window._ctx_||{url:window.location.pathname}); 16 | zstore = createStore((set) => ({...initstate,...session})); 17 | } 18 | tstore = zstore; 19 | 20 | if(!args || !args.length) return tstore.getState(); //get all states 21 | else if(args.length===1) { 22 | if(typeof args[0]==='string') return tstore.getState()[args[0]]; //get specific state 23 | else if(typeof args[0]==='object') return tstore.setState(args[0]) 24 | else console.error('MiNi: unknown store argument') 25 | } 26 | else if(args.length===2) { //set 27 | if(typeof args[0]!=='string') return console.error('MiNi: unknown store argument') 28 | if(typeof args[1] === 'function') { 29 | const t = tstore.getState()[args[0]]; 30 | args[1]=args[1](t); 31 | } 32 | return tstore.setState({[args[0]]:args[1]}); 33 | } 34 | else console.error('MiNi: store has too many arguments'); 35 | } 36 | 37 | 38 | /////////////////////////////////////////////////////////////// 39 | /* THIS IS A MODIFIED VERSION OF VANILLA ZUSTAND (without subscribers) 40 | https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts 41 | 42 | GET DATA: zstore.getState().appname 43 | RUN METHOD: zstore.getState().incCounter() 44 | PARTIAL UPDATE: zstore.setState((state)=>({appname:state+'x'})) 45 | zstore.setState({appname:'x'}) 46 | */ 47 | 48 | const createStoreImpl = (createState) => { 49 | let state; 50 | 51 | const setState = (partial, replace) => { 52 | const nextState = 53 | typeof partial === 'function' 54 | ? partial(state) 55 | : partial; 56 | if (!Object.is(nextState, state)) { 57 | const previousState = state; 58 | state = 59 | (replace ?? (typeof nextState !== 'object' || nextState === null)) 60 | ? (nextState) 61 | : Object.assign({}, state, nextState); 62 | } 63 | } 64 | 65 | const getState = () => state; 66 | const getInitialState = () => initialState; 67 | 68 | const api = { setState, getState, getInitialState }; 69 | const initialState = (state = createState(setState, getState, api)); 70 | return api; 71 | } 72 | 73 | const createStore = ((createState) => 74 | createState ? createStoreImpl(createState) : createStoreImpl); 75 | /////////////////////////////////////////////////////////////// 76 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { plugin as templateLiteralPlugin } from "@xdadda/vite-plugin-minify-template-literals"; 3 | 4 | import { dirname, resolve } from 'node:path' 5 | import { fileURLToPath } from 'node:url' 6 | 7 | const __dirname = dirname(fileURLToPath(import.meta.url)) 8 | 9 | export default defineConfig(({isSsrBuild, mode})=>{ 10 | 11 | return { 12 | plugins: [ 13 | templateLiteralPlugin() 14 | ], 15 | build: { 16 | target: 'esnext', 17 | minify: true, //in production to reduce size 18 | sourcemap: false, //unless required during development to debug production code artifacts 19 | modulePreload: { polyfill: false }, //not needed for modern browsers 20 | cssCodeSplit:false, //if small enough it's better to have it in one file to avoid flickering during suspend 21 | lib: { 22 | entry: { 23 | mini:resolve(__dirname, 'src/index.js'), 24 | store:resolve(__dirname, 'src/store/index.js'), 25 | router:resolve(__dirname, 'src/router/index.js'), 26 | components:resolve(__dirname, 'src/components/index.js'), 27 | }, 28 | name: 'MiNi', 29 | }, 30 | rollupOptions: { 31 | // make sure to externalize deps that shouldn't be bundled 32 | // into your library 33 | //external: ['mini','mini/store'], 34 | } 35 | } 36 | } 37 | }) 38 | --------------------------------------------------------------------------------