├── .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 |
77 | ${[1,2,3].map(value => html`${value} `)}
78 |
79 | /* reactive arrays ... as usual remember to wrap in a function ()=>{} */
80 |
81 | ${()=>array.value.map((v,idx) => html`${idx+1} `)}
82 |
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`t.stopPropagation()}" @keyup="${f}">
${c} ${d==="prompt"&&` `}
${l?.map((t,v)=>()=>r.html`m(y,t.onClick)}" tabindex="${v+1}" >${t.label} `)}
`}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``}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` t.stopPropagation()}" @keyup="${m}">
${r} ${u === "prompt" && ` `}
${o?.map((t, p) => () => C` d(v, t.onClick)}" tabindex="${p + 1}" >${t.label} `)}
`;
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``;
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 |
e.stopPropagation()}" @keyup="${handleKey}">
53 |
54 | ${content}
55 | ${type==='prompt' && ` `}
56 |
57 |
58 | ${ buttons?.map((b,i)=> ()=> html`
59 | handleClick(e,b.onClick)}"
61 | tabindex="${i+1}"
62 | >
63 | ${b.label}
64 |
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 |
--------------------------------------------------------------------------------