native-document 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/native-document.dev.js +2346 -0
- package/dist/native-document.min.js +1 -0
- package/elements.js +2 -0
- package/index.js +11 -0
- package/package.json +16 -0
- package/readme.md +495 -0
- package/rollup.config.js +29 -0
- package/router.js +9 -0
- package/src/data/MemoryManager.js +60 -0
- package/src/data/Observable.js +162 -0
- package/src/data/ObservableChecker.js +24 -0
- package/src/data/ObservableItem.js +101 -0
- package/src/data/Store.js +74 -0
- package/src/elements/content-formatter.js +32 -0
- package/src/elements/control/for-each.js +110 -0
- package/src/elements/control/show-if.js +86 -0
- package/src/elements/control/switch.js +88 -0
- package/src/elements/description-list.js +5 -0
- package/src/elements/form.js +71 -0
- package/src/elements/html5-semantics.js +12 -0
- package/src/elements/img.js +45 -0
- package/src/elements/index.js +21 -0
- package/src/elements/interactive.js +7 -0
- package/src/elements/list.js +6 -0
- package/src/elements/medias.js +8 -0
- package/src/elements/meta-data.js +9 -0
- package/src/elements/table.js +14 -0
- package/src/errors/ArgTypesError.js +7 -0
- package/src/errors/NativeDocumentError.js +8 -0
- package/src/errors/RouterError.js +9 -0
- package/src/router/Route.js +102 -0
- package/src/router/RouteGroupHelper.js +52 -0
- package/src/router/Router.js +232 -0
- package/src/router/RouterComponent.js +37 -0
- package/src/router/link.js +27 -0
- package/src/router/modes/HashRouter.js +83 -0
- package/src/router/modes/HistoryRouter.js +66 -0
- package/src/router/modes/MemoryRouter.js +71 -0
- package/src/utils/args-types.js +100 -0
- package/src/utils/debug-manager.js +34 -0
- package/src/utils/helpers.js +37 -0
- package/src/utils/prototypes.js +16 -0
- package/src/utils/validator.js +96 -0
- package/src/wrappers/AttributesWrapper.js +94 -0
- package/src/wrappers/DocumentObserver.js +51 -0
- package/src/wrappers/HtmlElementEventsWrapper.js +77 -0
- package/src/wrappers/HtmlElementWrapper.js +174 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var NativeDocument=function(t){"use strict";function n(e,t,n){return e.addEventListener(t,n),e}const r="production"===process.env.NODE_ENV,o={enabled:!r,enable(){this.enabled=!0,console.log("🔍 NativeDocument Debug Mode enabled")},disable(){this.enabled=!1},log:r?()=>{}:function(e,t,n){this.enabled&&(console.group(`🔍 [${e}] ${t}`),n&&console.log(n),console.trace(),console.groupEnd())},warn:r?()=>{}:function(e,t,n){this.enabled&&console.warn(`⚠️ [${e}] ${t}`,n)},error:r?()=>{}:function(e,t,n){console.error(`❌ [${e}] ${t}`,n)}},s=function(){let e=0;const t=new Map,n=new FinalizationRegistry(e=>{o.log("MemoryManager","🧹 Auto-cleanup observable:",e),e.listeners.splice(0)});return{register(r,o){const s=++e,i={id:s,listeners:o};return n.register(r,i),t.set(s,new WeakRef(r)),s},cleanup(){for(const[e,n]of t){const e=n.deref();e&&e.cleanup()}t.clear()},cleanObservables(e){if(t.size<e)return;let n=0;for(const[e,r]of t)r.deref()||(t.delete(e),n++);n>0&&o.log("Memory Auto Clean",`🧹 Cleaned ${n} orphaned observables`)}}}();class i extends Error{constructor(e,t={}){super(e),this.name="NativeDocumentError",this.context=t,this.timestamp=(new Date).toISOString()}}function a(e,t){this.observable=e,this.checker=t,this.subscribe=function(n){return e.subscribe(e=>{n&&n(t(e))})},this.val=function(){return t&&t(e.val())},this.cleanup=function(){return e.cleanup()}}function u(e){if(void 0===e)throw new i("ObservableItem requires an initial value");if(e instanceof u)throw new i("ObservableItem cannot be an Observable");const t="object"==typeof e?JSON.parse(JSON.stringify(e)):e;let n=e,r=e,c=!1;const l=[];s.register(this,l),this.trigger=()=>{l.forEach(e=>{try{e(r,n)}catch(t){o.error("Listener Undefined","Error in observable listener:",t),this.unsubscribe(e)}})},this.originalValue=()=>t,this.set=e=>{const t="function"==typeof e?e(r):e;r!==t&&(n=r,r=t,this.trigger())},this.val=()=>r,this.cleanup=function(){l.splice(0),c=!0},this.subscribe=e=>{if(c)return o.warn("Observable subscription","⚠️ Attempted to subscribe to a cleaned up observable."),()=>{};if("function"!=typeof e)throw new i("Callback must be a function");return l.push(e),()=>this.unsubscribe(e)},this.unsubscribe=e=>{const t=l.indexOf(e);t>-1&&l.splice(t,1)},this.check=function(e){return new a(this,e)}}const c={isObservable:e=>e instanceof u||e instanceof a,isProxy:e=>e?.__isProxy__,isObservableChecker:e=>e instanceof a,isArray:e=>Array.isArray(e),isString:e=>"string"==typeof e,isNumber:e=>"number"==typeof e,isBoolean:e=>"boolean"==typeof e,isFunction:e=>"function"==typeof e,isObject:e=>"object"==typeof e,isJson:e=>"object"==typeof e&&null!==e&&!Array.isArray(e),isElement:e=>e instanceof HTMLElement||e instanceof DocumentFragment||e instanceof Text,isFragment:e=>e instanceof DocumentFragment,isStringOrObservable(e){return this.isString(e)||this.isObservable(e)},isValidChild(e){return null===e||this.isElement(e)||this.isObservable(e)||["string","number","boolean"].includes(typeof e)},isValidChildren(e){Array.isArray(e)||(e=[e]);return 0===e.filter(e=>!this.isValidChild(e)).length},validateChildren(e){Array.isArray(e)||(e=[e]);const t=e.filter(e=>!this.isValidChild(e));if(t.length>0)throw new i(`Invalid children detected: ${t.map(e=>typeof e).join(", ")}`);return e},validateAttributes(e){if(!e||"object"!=typeof e)return e;const t=[],n=Object.keys(e).filter(e=>t.includes(e));return n.length>0&&o.warn("Validator",`Reserved attributes found: ${n.join(", ")}`),e},validateEventCallback(e){if("function"!=typeof e)throw new i("Event callback must be a function")}},l=function(e,t,n){n?e.classList.add(t):e.classList.remove(t)};function d(e,t){for(let n in t){const r=t[n];c.isObservable(r)?(l(e,n,r.val()),r.subscribe(t=>l(e,n,t))):l(e,n,r)}}function h(e,t){for(let n in t){const r=t[n];c.isObservable(r)?(e.style[n]=r.val(),r.subscribe(t=>{e.style[n]=t})):e.style[n]=r}}const p=function(e,t,n={}){let r=null,o=0;const{leading:s=!0,trailing:i=!0,debounce:a=!1}=n;return function(...n){const u=Date.now();if(a)return clearTimeout(r),void(r=setTimeout(()=>e.apply(this,n),t));s&&u-o>=t&&(e.apply(this,n),o=u),i&&!r&&(r=setTimeout(()=>{e.apply(this,n),o=Date.now(),r=null},t-(u-o)))}},f=function(e,t){return e.replace(new RegExp(`^[${t}]+|[${t}]+$`,"g"),"")},m={elements:new Map,observer:null,checkMutation:p(function(){for(const[e,t]of m.elements.entries()){const n=document.body.contains(e);n&&!t.inDom?(t.inDom=!0,t.mounted.forEach(t=>t(e))):!n&&t.inDom&&(t.inDom=!1,t.unmounted.forEach(t=>t(e)))}},10,{debounce:!0}),watch:function(e){let t={};if(m.elements.has(e))t=m.elements.get(e);else{const n=document.body.contains(e);t={inDom:n,mounted:new Set,unmounted:new Set},m.elements.set(e,t)}return{watch:()=>m.elements.set(e,t),disconnect:()=>m.elements.delete(e),mounted:e=>t.mounted.add(e),unmounted:e=>t.unmounted.add(e)}}};m.observer=new MutationObserver(m.checkMutation),m.observer.observe(document.body,{childList:!0,subtree:!0});const b=function(e,t){const n=document.createTextNode("");return t.subscribe(e=>n.textContent=String(e)),n.textContent=t.val(),e&&e.appendChild(n),n},v=function(e,t){const n=document.createTextNode("");return n.textContent=String(t),e&&e.appendChild(n),n},y=function(e){return c.isObservable(e)?b(null,e):v(null,e)},g={createElement:e=>e?document.createElement(e):document.createDocumentFragment(),processChildren(e,t){if(null===e)return;(Array.isArray(e)?e:[e]).forEach(e=>{null!==e&&(c.isElement(e)?t.appendChild(e):c.isObservable(e)?b(t,e):e&&v(t,e))})},processAttributes(e,t){c.isFragment(e)||t&&function(e,t){if(c.validateAttributes(t),!c.isObject(t))throw console.log(t),new i("Attributes must be an object");for(let n in t){const r=t[n];c.isObservable(r)?(r.subscribe(t=>e.setAttribute(n,t)),e.setAttribute(n,r.val()),"value"===n&&(["checkbox","radio"].includes(e.type)?e.addEventListener("input",()=>r.set(e.checked)):e.addEventListener("input",()=>r.set(e.value)))):"class"===n&&c.isJson(r)?d(e,r):"style"===n&&c.isJson(r)?h(e,r):e.setAttribute(n,r)}}(e,t)},setup(e,t,r){e.nd={},function(e){e.nd||(e.nd={}),e.nd.on=function(t){for(const r in t){const o=t[r];n(e,r,o)}return e},e.nd.on.prevent=function(t){for(const r in t){const o=t[r];n(e,r,t=>(t.preventDefault(),o&&o(t),e))}return e};const t={click:t=>n(e,"click",t),focus:t=>n(e,"focus",t),blur:t=>n(e,"blur",t),input:t=>n(e,"input",t),change:t=>n(e,"change",t),keyup:t=>n(e,"keyup",t),keydown:t=>n(e,"keydown",t),beforeInput:t=>n(e,"beforeinput",t),mouseOver:t=>n(e,"mouseover",t),mouseOut:t=>n(e,"mouseout",t),mouseDown:t=>n(e,"mousedown",t),mouseUp:t=>n(e,"mouseup",t),mouseMove:t=>n(e,"mousemove",t),hover:(t,n)=>{e.addEventListener("mouseover",t),e.addEventListener("mouseout",n)},dropped:t=>n(e,"drop",t),submit:t=>n(e,"submit",t),dragEnd:t=>n(e,"dragend",t),dragStart:t=>n(e,"dragstart",t),drop:t=>n(e,"drop",t),dragOver:t=>n(e,"dragover",t),dragEnter:t=>n(e,"dragenter",t),dragLeave:t=>n(e,"dragleave",t)};for(let r in t)e.nd.on[r]=t[r],e.nd.on.prevent[r]=function(t){return n(e,r.toLowerCase(),e=>{e.preventDefault(),t&&t(e)}),e}}(e);const o="function"==typeof r?r(e):e;return function(e){e.nd.wrap=t=>{if(!c.isFunction(t))throw new i("Callback must be a function");return t&&t(e),e},e.nd.ref=(t,n)=>(t[n]=e,e);let t=null;e.nd.lifecycle=function(n){return t=t||m.watch(e),n.mounted&&t.mounted(n.mounted),n.unmounted&&t.unmounted(n.unmounted),e},e.nd.mounted=n=>(t=t||m.watch(e),t.mounted(n),e),e.nd.unmounted=n=>(t=t||m.watch(e),t.unmounted(n),e)}(o),o}};function w(e,t){const n=e.toLowerCase().trim(),r=function(e,r=null){try{if(c.isValidChildren(e)){const t=r;r=e,e=t}const o=g.createElement(n);return g.processAttributes(o,e),g.processChildren(r,o),g.setup(o,e,t)}catch(e){o.error("ElementCreation",`Error creating ${n}`,e)}};return r.hold=(e,t)=>()=>r(e,t),r}class C extends Error{constructor(e,t){super(`${e}\n\n${t.join("\n")}\n\n`)}}const E={string:e=>({name:e,type:"string",validate:e=>c.isString(e)}),number:e=>({name:e,type:"number",validate:e=>c.isNumber(e)}),boolean:e=>({name:e,type:"boolean",validate:e=>c.isBoolean(e)}),observable:e=>({name:e,type:"observable",validate:e=>c.isObservable(e)}),element:e=>({name:e,type:"element",validate:e=>c.isElement(e)}),function:e=>({name:e,type:"function",validate:e=>c.isFunction(e)}),object:e=>({name:e,type:"object",validate:e=>c.isObject(e)}),objectNotNull:e=>({name:e,type:"object",validate:e=>c.isObject(e)&&null!==e}),children:e=>({name:e,type:"children",validate:e=>c.validateChildren(e)}),attributes:e=>({name:e,type:"attributes",validate:e=>c.validateAttributes(e)}),optional:e=>({...e,optional:!0}),oneOf:(e,...t)=>({name:e,type:"oneOf",types:t,validate:e=>t.some(t=>t.validate(e))})},O=(e,t,n="Function")=>{if(!c.isArray(t))throw new i("withValidation : argSchema must be an array");return function(...r){return((e,t,n="Function")=>{if(!t)return;const r=[],o=t.filter(e=>!e.optional).length;if(e.length<o&&r.push(`${n}: Expected at least ${o} arguments, got ${e.length}`),t.forEach((t,o)=>{const s=o+1,i=e[o];if(void 0!==i){if(!t.validate(i)){const e=i?.constructor?.name||typeof i;r.push(`${n}: Invalid argument '${t.name}' at position ${s}, expected ${t.type}, got ${e}`)}}else t.optional||r.push(`${n}: Missing required argument '${t.name}' at position ${s}`)}),r.length>0)throw new C("Argument validation failed",r)})(r,t,e.name||n),e.apply(this,r)}};function S(e){return new u(e)}Function.prototype.args=function(...e){return O(this,e)},Function.prototype.errorBoundary=function(e){return(...t)=>{try{return this.apply(this,t)}catch(t){return e(t)}}},S.computed=function(e,t=[]){const n=new u(e()),r=p(()=>n.set(e()),10,{debounce:!0});return t.forEach(e=>e.subscribe(r)),n},S.cleanup=function(e){e.cleanup()},S.value=function(e){if(c.isObservable(e))return e.val();if(c.isProxy(e))return e.$val();if(c.isArray(e)){const t=[];return e.forEach(e=>{t.push(S.value(e))}),t}return e},S.init=function(e){const t={};for(const n in e){const r=e[n];c.isJson(r)?(console.log(r),t[n]=S.init(r)):t[n]=S(r)}const n=function(){const e={};for(const n in t){const r=t[n];c.isObservable(r)?e[n]=r.val():c.isProxy(r)?e[n]=r.$val():e[n]=r}return e};return new Proxy(t,{get:(e,t)=>"__isProxy__"===t||("$val"===t?n:void 0!==e[t]?e[t]:void 0),set(e,t,n){void 0!==e[t]&&e[t].set(n)}})},S.object=S.init,S.array=function(e){if(!Array.isArray(e))throw new i("Observable.array : target must be an array");const t=S(e);["push","pop","shift","unshift","reverse","sort","splice"].forEach(e=>{t[e]=function(...n){const r=t.val(),o=r[e].apply(r,arguments);return t.trigger(),o}});return["map","filter","reduce","some","every","find"].forEach(e=>{t[e]=function(n){return t.val()[e](n)}}),t},S.autoCleanup=function(e=!1,t={}){if(!e)return;const{interval:n=6e4,threshold:r=100}=t;window.addEventListener("beforeunload",()=>{s.cleanup()}),setInterval(()=>s.cleanObservables(r),n)};const F=function(){const e=new Map;return{use(t){const{observer:n,followers:r}=e.get(t),o=S(n.val()),s=n.subscribe(e=>o.set(e)),i=o.subscribe(e=>n.set(e));return o.destroy=()=>{s(),i(),o.cleanup()},r.add(o),o},follow(e){return this.use(e)},create(t,n){const r=S(n);return e.set(t,{observer:r,followers:new Set}),r},get(t){const n=e.get(t);return n?n.observer:null},getWithFollowers:t=>e.get(t),delete(t){const n=e.get(t);n&&(n.observer.cleanup(),n.followers.forEach(e=>e.destroy()),n.observer.clear())}}}();const k=function(e,t,n){if(!c.isObservable(e))return o.warn("ShowIf","ShowIf : condition must be an Observable / "+n,e);const r=document.createDocumentFragment(),s=document.createComment("Show if : "+(n||"")),i=document.createComment("Show if : "+(n||""));r.appendChild(s),r.appendChild(i);let a=null;const u=()=>a||(a="function"==typeof t?t():t,c.isStringOrObservable(a)&&(a=y(a)),a);return e.val()&&r.appendChild(u()),e.subscribe(e=>{const t=i.parentNode;e&&t?t.insertBefore(u(),i):c.isElement(a)&&a.remove()}),r},A=function(e,t,n){if(!c.isObservable(e))throw new i("Toggle : condition must be an Observable");const r=document.createComment("Toggle Start"),o=document.createComment("Toggle End"),s=document.createDocumentFragment();s.appendChild(r),s.appendChild(o);const a={onTrueNode:c.isFunction(t)?null:t,onFalseNode:c.isFunction(n)?null:n};c.isStringOrObservable(a.onTrueNode)&&(a.onTrueNode=y(a.onTrueNode)),c.isStringOrObservable(a.onFalseNode)&&(a.onFalseNode=y(a.onFalseNode));const u=e=>{const r=o.parentNode;r&&(e?(!a.onTrueNode&&c.isFunction(t)&&(a.onTrueNode=t()),a.onFalseNode&&a.onFalseNode.remove(),r.insertBefore(a.onTrueNode,o)):(!a.onFalseNode&&c.isFunction(n)&&(a.onFalseNode=n()),a.onTrueNode&&a.onTrueNode.remove(),r.insertBefore(a.onFalseNode,o)))};return e.subscribe(u),u(e.val()),s},$=w("div"),T=w("span"),I=w("span"),q=w("p"),N=q,x=w("strong"),D=w("h1"),R=w("h2"),L=w("h3"),j=w("h4"),M=w("h5"),H=w("h6"),_=w("br"),B=w("a"),P=w("pre"),V=w("code"),U=w("blockquote"),W=w("hr"),J=w("em"),z=w("small"),K=w("mark"),Q=w("del"),G=w("ins"),X=w("sub"),Y=w("sup"),Z=w("abbr"),ee=w("cite"),te=w("q"),ne=w("dl"),re=w("dt"),oe=w("dd"),se=w("form",function(e){return e.submit=function(t){return"function"==typeof t?(e.on.submit(e=>{e.preventDefault(),t(e)}),e):(this.setAttribute("action",t),e)},e.multipartFormData=function(){return this.setAttribute("enctype","multipart/form-data"),e},e.post=function(t){return this.setAttribute("method","post"),this.setAttribute("action",t),e},e.get=function(e){this.setAttribute("method","get"),this.setAttribute("action",e)},e}),ie=w("input"),ae=w("textarea"),ue=ae,ce=w("select"),le=w("fieldset"),de=w("option"),he=w("legend"),pe=w("datalist"),fe=w("output"),me=w("progress"),be=w("meter"),ve=w("button"),ye=w("main"),ge=w("section"),we=w("article"),Ce=w("aside"),Ee=w("nav"),Oe=w("figure"),Se=w("figcaption"),Fe=w("header"),ke=w("footer"),Ae=w("img"),$e=function(e,t){return Ae({src:e,...t})},Te=w("details"),Ie=w("summary"),qe=w("dialog"),Ne=w("menu"),xe=w("ol"),De=w("ul"),Re=w("li"),Le=w("audio"),je=w("video"),Me=w("source"),He=w("track"),_e=w("canvas"),Be=w("svg"),Pe=w("time"),Ve=w("data"),Ue=w("address"),We=w("kbd"),Je=w("samp"),ze=w("var"),Ke=w("wbr"),Qe=w("caption"),Ge=w("table"),Xe=w("thead"),Ye=w("tfoot"),Ze=w("tbody"),et=w("tr"),tt=et,nt=w("th"),rt=nt,ot=nt,st=w("td"),it=st,at=w("");var ut=Object.freeze({__proto__:null,Abbr:Z,Address:Ue,Article:we,Aside:Ce,AsyncImg:function(e,t,n,r){const o=$e(t||e,n),s=new Image;return s.onload=()=>{c.isFunction(r)&&r(null,o),o.src=e},s.onerror=()=>{c.isFunction(r)&&r(new i("Image not found"))},c.isObservable(e)&&e.subscribe(e=>{s.src=e}),s.src=e,o},Audio:Le,BaseImage:Ae,Blockquote:U,Br:_,Button:ve,Canvas:_e,Caption:Qe,Checkbox:e=>ie({type:"checkbox",...e}),Cite:ee,Code:V,ColorInput:e=>ie({type:"color",...e}),Data:Ve,Datalist:pe,DateInput:e=>ie({type:"date",...e}),DateTimeInput:e=>ie({type:"datetime-local",...e}),Dd:oe,Del:Q,Details:Te,Dialog:qe,Div:$,Dl:ne,Dt:re,Em:J,EmailInput:e=>ie({type:"email",...e}),FieldSet:le,FigCaption:Se,Figure:Oe,FileInput:e=>ie({type:"file",...e}),Footer:ke,ForEach:function(e,t,n){const r=document.createDocumentFragment(),o=document.createComment("Foreach start"),s=document.createComment("Foreach end");r.appendChild(o),r.appendChild(s);let i=new Map;const a=(e,r)=>{const o=((e,t,n)=>{if(c.isFunction(n))return n(e,t);if(c.isObservable(e)){const r=e.val();return r&&n?r[n]:t}return e[n]??t})(e,r,n);if(i.has(o))i.get(o).indexObserver.set(r);else{const n=S(r);let s=t(e,n);c.isStringOrObservable(s)&&(s=y(s)),i.set(o,{child:s,indexObserver:n})}return o},u=new Set,l=()=>{const t=c.isObservable(e)?e.val():e,n=s.parentNode;if(!n)return;if(u.clear(),Array.isArray(t))t.forEach((e,t)=>u.add(a(e,t)));else for(const e in t)u.add(a(t[e],e));((e,t)=>{for(const[n,{child:r}]of e.entries())t.has(n)||r.remove()})(i,u);let r=s;for(const e of[...u].reverse()){const{child:t}=i.get(e);if(t){if(r&&r.previousSibling===t){r=t;continue}n.insertBefore(t,r),r=t}}};return l(),c.isObservable(e)&&e.subscribe(p((e,t)=>{l()},50,{debounce:!0})),r},Form:se,Fragment:at,H1:D,H2:R,H3:L,H4:j,H5:M,H6:H,Header:Fe,HiddenInput:e=>ie({type:"hidden",...e}),HideIf:function(e,t,n){const r=S(!e.val());return e.subscribe(e=>r.set(!e)),k(r,t,n)},HideIfNot:function(e,t,n){return k(e,t,n)},Hr:W,Img:$e,Input:ie,Ins:G,Kbd:We,Label:I,LazyImg:function(e,t){return $e(e,{...t,loading:"lazy"})},Legend:he,Link:B,ListItem:Re,Main:ye,Mark:K,Menu:Ne,Meter:be,MonthInput:e=>ie({type:"month",...e}),Nav:Ee,NumberInput:e=>ie({type:"number",...e}),Option:de,OrderedList:xe,Output:fe,P:q,Paragraph:N,PasswordInput:e=>ie({type:"password",...e}),Pre:P,Progress:me,Quote:te,Radio:e=>ie({type:"radio",...e}),RangeInput:e=>ie({type:"range",...e}),ReadonlyInput:e=>ie({readonly:!0,...e}),Samp:Je,SearchInput:e=>ie({type:"search",...e}),Section:ge,Select:ce,ShowIf:k,SimpleButton:(e,t)=>ve(e,{type:"button",...t}),Small:z,Source:Me,Span:T,Strong:x,Sub:X,SubmitButton:(e,t)=>ve(e,{type:"submit",...t}),Summary:Ie,Sup:Y,Svg:Be,Switch:A,TBody:Ze,TBodyCell:it,TFoot:Ye,TFootCell:ot,THead:Xe,THeadCell:rt,TRow:tt,Table:Ge,Td:st,TelInput:e=>ie({type:"tel",...e}),TextArea:ae,TextInput:ue,Th:nt,Time:Pe,TimeInput:e=>ie({type:"time",...e}),Tr:et,Track:He,UnorderedList:De,UrlInput:e=>ie({type:"url",...e}),Var:ze,Video:je,Wbr:Ke,WeekInput:e=>ie({type:"week",...e}),When:function(e){let t=null,n=null;return{show(e){if(!c.isElement(e)&&!c.isFunction(e))throw new i("When : onTrue must be a valid Element");return t=e,this},otherwise(r){if(!c.isElement(r)&&!c.isFunction(r))throw new i("When : onFalse must be a valid Element");return n=r,A(e,t,n)}}}});const ct={};function lt(e,t,n={}){e="/"+f(e,"/");let r=null,o=n.name||null;const s=n.middlewares||[],i=n.shouldRebuild||!1,a=n.with||{},u={},c=[],l=e=>{if(!e)return null;const[t,n]=e.split(":");let r=a[t];return!r&&n&&(r=ct[n]),r||(r="[^/]+"),r=r.replace("(","(?:"),{name:t,pattern:`(${r})`}},d=()=>{if(r)return r;const t=e.replace(/\{(.*?)}/gi,(e,t)=>{const n=l(t);return n&&n.pattern?(u[n.name]=n.pattern,c.push(n.name),n.pattern):e});return r=new RegExp("^"+t+"$"),r};this.name=()=>o,this.component=()=>t,this.middlewares=()=>s,this.shouldRebuild=()=>i,this.path=()=>e,this.match=function(e){e="/"+f(e,"/");if(!d().exec(e))return!1;const t={};return d().exec(e).forEach((e,n)=>{if(n<1)return;const r=c[n-1];t[r]=e}),t},this.url=function(t){const n=e.replace(/\{(.*?)}/gi,(e,n)=>{const r=l(n);if(t.params&&t.params[r.name])return t.params[r.name];throw new Error(`Missing parameter '${r.name}'`)}),r="object"==typeof t.query?new URLSearchParams(t.query).toString():null;return(t.basePath?t.basePath:"")+(r?`${n}?${r}`:n)}}class dt extends Error{constructor(e,t){super(e),this.context=t}}const ht=(e,t)=>{const n=[];return e.forEach(e=>{n.push(f(e.suffix,"/"))}),n.push(f(t,"/")),n.join("/")},pt=(e,t)=>{const n=[];return e.forEach(e=>{e.options.middlewares&&n.push(...e.options.middlewares)}),t&&n.push(...t),n},ft=(e,t)=>{const n=[];return e.forEach(e=>{e.options?.name&&n.push(e.options.name)}),t&&n.push(t),n.join(".")};function mt(){const e=[];let t=0;const n=n=>{const o=t+n;if(!e[o])return;t=o;const{route:s,params:i,query:a,path:u}=e[o];r(u)},r=e=>{window.location.replace(`${window.location.pathname}${window.location.search}#${e}`)},o=()=>window.location.hash.slice(1);this.push=function(n){const{route:s,params:i,query:a,path:u}=this.resolve(n);u!==o()&&(e.splice(t+1),e.push({route:s,params:i,query:a,path:u}),t++,r(u))},this.replace=function(n){const{route:r,params:s,query:i,path:a}=this.resolve(n);a!==o()&&(e[t]={route:r,params:s,query:i,path:a})},this.forward=function(){return t<e.length-1&&n(1)},this.back=function(){return t>0&&n(-1)},this.init=function(n){window.addEventListener("hashchange",()=>{const{route:e,params:t,query:n,path:r}=this.resolve(o());this.handleRouteChange(e,t,n,r)});const{route:r,params:s,query:i,path:a}=this.resolve(n||o());e.push({route:r,params:s,query:i,path:a}),t=0,this.handleRouteChange(r,s,i,a)}}function bt(){this.push=function(e){try{const{route:t,path:n,params:r,query:o}=this.resolve(e);if(window.history.state&&window.history.state.path===n)return;window.history.pushState({name:t.name(),params:r,path:n},t.name()||n,n),this.handleRouteChange(t,r,o,n)}catch(e){o.error("HistoryRouter","Error in pushState",e)}},this.replace=function(e){const{route:t,path:n,params:r}=this.resolve(e);try{window.history.replaceState({name:t.name(),params:r,path:n},t.name()||n,n),this.handleRouteChange(t,r,{},n)}catch(e){o.error("HistoryRouter","Error in replaceState",e)}},this.forward=function(){window.history.forward()},this.back=function(){window.history.back()},this.init=function(e){window.addEventListener("popstate",e=>{try{if(!e.state||!e.state.path)return;const t=e.state.path,{route:n,params:r,query:o,path:s}=this.resolve(t);if(!n)return;this.handleRouteChange(n,r,o,s)}catch(e){o.error("HistoryRouter","Error in popstate event",e)}});const{route:t,params:n,query:r,path:s}=this.resolve(e||window.location.pathname+window.location.search);this.handleRouteChange(t,n,r,s)}}function vt(){const e=[];let t=0;const n=n=>{const r=t+n;if(!e[r])return;t=r;const{route:o,params:s,query:i,path:a}=e[r];this.handleRouteChange(o,s,i,a)};this.push=function(n){const{route:r,params:o,query:s,path:i}=this.resolve(n);e[t]&&e[t].path===i||(e.splice(t+1),e.push({route:r,params:o,query:s,path:i}),t++,this.handleRouteChange(r,o,s,i))},this.replace=function(n){const{route:r,params:o,query:s,path:i}=this.resolve(n);e[t]={route:r,params:o,query:s,path:i},this.handleRouteChange(r,o,s,i)},this.forward=function(){return t<e.length-1&&n(1)},this.back=function(){return t>0&&n(-1)},this.init=function(n){const r=n||window.location.pathname+window.location.search,{route:o,params:s,query:i,path:a}=this.resolve(r);e.push({route:o,params:s,query:i,path:a}),t=0,this.handleRouteChange(o,s,i,a)}}const yt="default";function gt(e={}){const t=[],n={},r=[],s=[],i={route:null,params:null,query:null,path:null,hash:null};if("hash"===e.mode)mt.apply(this,[]);else if("history"===e.mode)bt.apply(this,[]);else{if("memory"!==e.mode)throw new dt("Invalid router mode "+e.mode);vt.apply(this,[])}const a=function(e,t){for(const n of s)try{n(e),t&&t(e)}catch(e){o.warn("Route Listener","Error in listener:",e)}};this.routes=()=>[...t],this.currentState=()=>({...i}),this.add=function(e,o,s){const i=new lt(ht(r,e),o,{...s,middlewares:pt(r,s?.middlewares||[]),name:s?.name?ft(r,s.name):null});return t.push(i),i.name()&&(n[i.name()]=i),this},this.group=function(e,t,n){if(!c.isFunction(n))throw new dt("Callback must be a function");return r.push({suffix:e,options:t}),n(),r.pop(),this},this.generateUrl=function(e,t={},r={}){const o=n[e];if(!o)throw new dt(`Route not found for name: ${e}`);return o.url({params:t,query:r})},this.resolve=function(e){if(c.isJson(e)){const t=n[e.name];if(!t)throw new dt(`Route not found for name: ${e.name}`);return{route:t,params:e.params,query:e.query,path:t.url({...e})}}const[r,o]=e.split("?"),s="/"+f(r,"/");let i,a=null;for(const e of t)if(i=e.match(s),i){a=e;break}if(!a)throw new dt(`Route not found for url: ${r}`);const u={};if(o){const e=new URLSearchParams(o).entries();for(const[t,n]of e)u[t]=n}return{route:a,params:i,query:u,path:e}},this.subscribe=function(e){if(!c.isFunction(e))throw new dt("Listener must be a function");return s.push(e),()=>{s.splice(s.indexOf(e),1)}},this.handleRouteChange=function(e,t,n,r){i.route=e,i.params=t,i.query=n,i.path=r,console.log(i.query);const o=[...e.middlewares(),a];let s=0;const u={...i},c=e=>{if(s++,!(s>=o.length))return o[s](e||u,c)};return o[s](u,c)}}function wt(e,t){const n=e.to||e.href;if(c.isString(n)){const r=gt.get();return B({...e,href:n},t).nd.on.prevent.click(()=>{r.push(n)})}const r=gt.get(n.router);if(!r)throw new dt('Router not found "'+n.router+'" for link "'+n.name+'"');const o=r.generateUrl(n.name,n.params,n.query);return B({...e,href:o},t).nd.on.prevent.click(()=>{r.push(o)})}gt.routers={},gt.create=function(t,n){if(!c.isFunction(n))throw o.error("Router","Callback must be a function",e),new dt("Callback must be a function");const r=new gt(t);return gt.routers[t.name||yt]=r,n(r),r.init(t.entry),{mount:e=>{if(c.isString(e)){const t=document.querySelector(e);if(!t)throw new dt(`Container not found for selector: ${e}`);e=t}else if(!c.isElement(e))throw new dt("Container must be a string or an Element");!function(e,t){const n=new Map,r=function(e){t.innerHTML="",t.appendChild(e)},o=function(e){if(!e.route)return;const{route:t,params:o,query:s,path:i}=e;if(n.has(i)){const e=n.get(i);return console.log(e),void r(e)}const a=t.component();console.log({params:o,query:s});const u=a({params:o,query:s});n.set(i,u),r(u)};e.subscribe(o),o(e.currentState())}(r,e)}}},gt.get=function(e){return gt.routers[e||yt]},wt.blank=function(e,t){return B({...e,target:"_blank"},t)};var Ct=Object.freeze({__proto__:null,Link:wt,Router:gt});return t.ArgTypes=E,t.ElementCreator=g,t.HtmlElementWrapper=w,t.Observable=S,t.Store=F,t.elements=ut,t.router=Ct,t.withValidation=O,t}({});
|
package/elements.js
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { default as HtmlElementWrapper, ElementCreator } from './src/wrappers/HtmlElementWrapper'
|
|
2
|
+
|
|
3
|
+
import './src/utils/prototypes.js';
|
|
4
|
+
|
|
5
|
+
export * from './src/utils/args-types';
|
|
6
|
+
export * from './src/data/Observable';
|
|
7
|
+
export * from './src/data/Store';
|
|
8
|
+
import * as elements from './elements';
|
|
9
|
+
import * as router from './router';
|
|
10
|
+
|
|
11
|
+
export { elements, router};
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "native-document",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "rollup --config rollup.config.js --watch"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"description": "",
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@rollup/plugin-terser": "^0.4.4"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# 🚀 NativeDocument
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
|
5
|
+
[](https://github.com/yourusername/nativedocument)
|
|
6
|
+
|
|
7
|
+
> A lightweight, reactive JavaScript framework for building modern web applications with zero dependencies.
|
|
8
|
+
|
|
9
|
+
NativeDocument provides a reactive programming model similar to React but using vanilla JavaScript and modern web APIs. It features automatic memory management, a complete set of HTML elements, routing, state management, and real-time DOM updates.
|
|
10
|
+
|
|
11
|
+
## ✨ Features
|
|
12
|
+
|
|
13
|
+
- 🔄 **Reactive Observables** - Automatic DOM updates when data changes
|
|
14
|
+
- 🧠 **Smart Memory Management** - Automatic cleanup with FinalizationRegistry
|
|
15
|
+
- 🎯 **Zero Dependencies** - Pure vanilla JavaScript
|
|
16
|
+
- 🛣️ **Built-in Router** - Hash, History, and Memory modes
|
|
17
|
+
- 🏪 **Global State Management** - Share state across components
|
|
18
|
+
- 📦 **Complete HTML Elements** - All HTML elements with extended functionality
|
|
19
|
+
- 🎨 **Conditional Rendering** - ShowIf, ToggleView, ForEach components
|
|
20
|
+
- ✅ **Runtime Validation** - Type checking and argument validation
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## 🚀 Quick Start
|
|
24
|
+
|
|
25
|
+
### Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Download the minified version
|
|
29
|
+
curl -o native-document.min.js https://raw.githubusercontent.com/afrocodeur/native-document/refs/heads/main/dist/native-document.min.js
|
|
30
|
+
|
|
31
|
+
# Or include via CDN
|
|
32
|
+
<script src="https://cdn.jsdelivr.net/gh/afrocodeur/native-document@latest/dist/native-document.min.js"></script>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
### Basic Example
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
const { Observable } = NativeDocument;
|
|
40
|
+
const { H1, P, Button } = NativeDocument.elements;
|
|
41
|
+
// Create reactive state
|
|
42
|
+
const count = Observable(0);
|
|
43
|
+
const message = Observable("Hello World!");
|
|
44
|
+
|
|
45
|
+
// Create reactive UI
|
|
46
|
+
const app = Div([
|
|
47
|
+
H1(message),
|
|
48
|
+
P(['Count: ',count]),
|
|
49
|
+
Button("Increment").nd.on.click(() => count.set(count.val() + 1)),
|
|
50
|
+
Button("Reset").nd.on.click(() => count.set(0))
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
// Mount to DOM
|
|
54
|
+
document.body.appendChild(app);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 📚 Core Concepts
|
|
58
|
+
|
|
59
|
+
### Observables
|
|
60
|
+
|
|
61
|
+
Observables are the heart of NativeDocument's reactivity system:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
const { Observable } = NativeDocument;
|
|
65
|
+
// Create observable
|
|
66
|
+
const name = Observable("John");
|
|
67
|
+
|
|
68
|
+
// Get value
|
|
69
|
+
console.log(name.val()); // "John"
|
|
70
|
+
|
|
71
|
+
// Subscribe to changes
|
|
72
|
+
const unsubscribe = name.subscribe((newValue, oldValue) => {
|
|
73
|
+
console.log(`Changed from ${oldValue} to ${newValue}`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Set value
|
|
77
|
+
name.set("Jane");
|
|
78
|
+
|
|
79
|
+
// Cleanup
|
|
80
|
+
unsubscribe();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
### 📦 Elements
|
|
85
|
+
|
|
86
|
+
All HTML elements are available as functions:
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const { Observable } = NativeDocument;
|
|
90
|
+
const { Div, H1, H2, P, Link, Strong, Input, Button, Br } = NativeDocument.elements;
|
|
91
|
+
// Basic elements
|
|
92
|
+
const header = H1("My App");
|
|
93
|
+
const paragraph = P("Welcome to NativeDocument!");
|
|
94
|
+
|
|
95
|
+
// With attributes
|
|
96
|
+
const link = Link({href: "https://example.com", target: "_blank" }, "Click me");
|
|
97
|
+
|
|
98
|
+
const name = Observable('');
|
|
99
|
+
|
|
100
|
+
// With reactive attributes
|
|
101
|
+
const input = Input({
|
|
102
|
+
type: "text",
|
|
103
|
+
value: name, // Observable binding
|
|
104
|
+
placeholder: "Enter name"
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Nested elements
|
|
108
|
+
const card = Div({ class: "card" }, [
|
|
109
|
+
H2("Card Title"),
|
|
110
|
+
Strong(name),
|
|
111
|
+
P("Card content here..."),
|
|
112
|
+
Button("Action")
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
document.body.appendChild(Div([
|
|
116
|
+
input,
|
|
117
|
+
card,
|
|
118
|
+
Br(),
|
|
119
|
+
link
|
|
120
|
+
]));
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
### Conditional Rendering
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
const { Observable } = NativeDocument;
|
|
128
|
+
const { ShowIf, Switch, When, Div, P, Button, Br, H2 } = NativeDocument.elements;
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
const isVisible = Observable(true);
|
|
132
|
+
const user = Observable({ name: "John", age: 15 });
|
|
133
|
+
|
|
134
|
+
// Show/hide elements
|
|
135
|
+
const conditionalContent = Div([
|
|
136
|
+
ShowIf(isVisible, P("This content is visible!")),
|
|
137
|
+
Button('Toggle').nd.on.click(() => isVisible.set((currentValue) => !currentValue))
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const whenContent = Div([
|
|
141
|
+
H2('When'),
|
|
142
|
+
When(user.check(u => u.age >= 18))
|
|
143
|
+
.show(() => P("Adult content"))
|
|
144
|
+
.otherwise(() => P("Minor content")),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
// Toggle between two states
|
|
148
|
+
const toggleContent = Div([
|
|
149
|
+
H2('Switch'),
|
|
150
|
+
Switch(
|
|
151
|
+
user.check(u => u.age >= 18),
|
|
152
|
+
() => P("Adult content"), // use function to create element only if requested
|
|
153
|
+
() => P("Minor content")
|
|
154
|
+
),
|
|
155
|
+
Button('Toggle user age').nd.on.click(() => user.set((currentValue) => ({ ...currentValue, age: currentValue.age === 25 ? 15 :25 })))
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
document.body.appendChild(Div([
|
|
159
|
+
conditionalContent,
|
|
160
|
+
Br(),
|
|
161
|
+
whenContent,
|
|
162
|
+
Br(),
|
|
163
|
+
toggleContent
|
|
164
|
+
]));
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Lists and Iteration
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
const { Observable } = NativeDocument;
|
|
171
|
+
const { ForEach, Div, P, Button, Br, H4 } = NativeDocument.elements;
|
|
172
|
+
|
|
173
|
+
const items = Observable([
|
|
174
|
+
{ id: 1, name: "Apple", price: 1.20 },
|
|
175
|
+
{ id: 2, name: "Banana", price: 0.80 },
|
|
176
|
+
{ id: 3, name: "Orange", price: 1.50 }
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
const itemList = ForEach(items, (item, index) =>
|
|
180
|
+
Div({ class: "item" }, [
|
|
181
|
+
H4(item.name),
|
|
182
|
+
P(`$${item.price}`),
|
|
183
|
+
Button("Remove").nd.on.click(() => {
|
|
184
|
+
items.set((currentItems) => currentItems.filter(i => i.id !== item.id))
|
|
185
|
+
})
|
|
186
|
+
]), 'id');
|
|
187
|
+
|
|
188
|
+
document.body.appendChild(itemList);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Forms
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
|
|
195
|
+
const { EmailInput, Form, Label, Strong, SubmitButton, Input, Div } = NativeDocument.elements;
|
|
196
|
+
const { Observable } = NativeDocument;
|
|
197
|
+
|
|
198
|
+
const formData = Observable.object({ name: "", email: "d.mamadou@miridoo.net" });
|
|
199
|
+
|
|
200
|
+
const FormItem = (title, input) => {
|
|
201
|
+
return Div([
|
|
202
|
+
Label(Strong(title)),
|
|
203
|
+
Div(input)
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const form = Form([
|
|
208
|
+
FormItem("Name", Input({ value: formData.name })),
|
|
209
|
+
FormItem("Email", Input({ value: formData.email })),
|
|
210
|
+
SubmitButton("Submit")
|
|
211
|
+
]).nd.on.prevent.submit((event) => {
|
|
212
|
+
console.log(Observable.value(formData));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
document.body.appendChild(form);
|
|
216
|
+
```
|
|
217
|
+
## 🛣️ Routing
|
|
218
|
+
|
|
219
|
+
NativeDocument includes a powerful routing system:
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
|
|
223
|
+
const { Div, Button, Main } = NativeDocument.elements;
|
|
224
|
+
const { Router, Link } = NativeDocument.router;
|
|
225
|
+
|
|
226
|
+
const CustomMiddleware = (request, next) => {
|
|
227
|
+
console.log('check custom middleware', request);
|
|
228
|
+
// request.params.customValue = true;
|
|
229
|
+
return next(request);
|
|
230
|
+
};
|
|
231
|
+
const AuthMiddleware = (request, next) => {
|
|
232
|
+
console.log('check if user is authenticated');
|
|
233
|
+
return next(request);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
const DefaultLayout = (children) => {
|
|
238
|
+
return Div([
|
|
239
|
+
Main({ class: 'main-container', style: 'padding: 1rem 0' }, children),
|
|
240
|
+
Div({ class: 'navigation-container'}, Div([
|
|
241
|
+
Link({ to: '/' }, 'Home'), // Link from router
|
|
242
|
+
Link({ to: { name: 'profile' } }, 'Profile'),
|
|
243
|
+
Link({ to: { name: 'user.show', params: {id: 1} } }, 'Show User'),
|
|
244
|
+
Link({ to: { name: 'admin.dashboard' } }, 'Show Admin Dashboard'),
|
|
245
|
+
Link({ to: { name: 'admin.users' } }, 'Show Admin User'),
|
|
246
|
+
Button('Product Page').nd.on.click(() => {
|
|
247
|
+
const router = Router.get();
|
|
248
|
+
// Navigate programmatically
|
|
249
|
+
router.push('/product/123?name=ProductName');
|
|
250
|
+
}),
|
|
251
|
+
]))
|
|
252
|
+
]);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const HomePage = () => {
|
|
256
|
+
return DefaultLayout(Div('Home page'));
|
|
257
|
+
};
|
|
258
|
+
const UserPage = ({ params, query }) => {
|
|
259
|
+
return DefaultLayout(Div('User page for '+params.id));
|
|
260
|
+
};
|
|
261
|
+
const ProductPage = ({ params, query }) => {
|
|
262
|
+
return DefaultLayout(Div('Product page '+params.id+' with product name '+query.name));
|
|
263
|
+
};
|
|
264
|
+
const AdminDashboard = () => {
|
|
265
|
+
return DefaultLayout(Div('Admin dashboard'));
|
|
266
|
+
};
|
|
267
|
+
const AdminUsers = () => {
|
|
268
|
+
return DefaultLayout(Div('Admin users'));
|
|
269
|
+
};
|
|
270
|
+
const ProfilePage = () => {
|
|
271
|
+
return DefaultLayout(Div('Profile page'));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
const router = Router.create({ mode: "history" }, (router) => {
|
|
276
|
+
// Basic route
|
|
277
|
+
router.add("/", HomePage, { name: 'home' });
|
|
278
|
+
|
|
279
|
+
// Route with parameters
|
|
280
|
+
router.add("/user/{id}", UserPage, { name: "user.show" });
|
|
281
|
+
|
|
282
|
+
// Route with constraints
|
|
283
|
+
router.add("/product/{id:number}", ProductPage);
|
|
284
|
+
|
|
285
|
+
// Grouped routes with middleware
|
|
286
|
+
router.group("/admin", { middlewares: [AuthMiddleware], name: 'admin' }, () => {
|
|
287
|
+
router.add("/dashboard", AdminDashboard, { name: 'dashboard' }); // name = admin.dashboard
|
|
288
|
+
router.add("/users", AdminUsers, { name: 'users' }); // name = admin.users
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Named routes
|
|
292
|
+
router.add("/profile", ProfilePage, { name: "profile" });
|
|
293
|
+
|
|
294
|
+
router.add(".*", () => {
|
|
295
|
+
return DefaultLayout(
|
|
296
|
+
Div([
|
|
297
|
+
'404',
|
|
298
|
+
Div('Route not found')
|
|
299
|
+
])
|
|
300
|
+
);
|
|
301
|
+
})
|
|
302
|
+
}).mount(document.body);
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## 🏪 State Management
|
|
307
|
+
|
|
308
|
+
### Global Store
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
const { Div, P, Button, ShowIf, When } = NativeDocument.elements;
|
|
312
|
+
const { Store, Observable } = NativeDocument;
|
|
313
|
+
|
|
314
|
+
const $ = Observable.computed;
|
|
315
|
+
|
|
316
|
+
// Create global store
|
|
317
|
+
Store.create("user", {
|
|
318
|
+
name: "Anonymous",
|
|
319
|
+
loginAt: '...',
|
|
320
|
+
isLoggedIn: false
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const Footer = () => {
|
|
324
|
+
const user = Store.use("user");
|
|
325
|
+
const dynamicLoginAtText = $(() => 'Login at '+user.val().loginAt, [user]);
|
|
326
|
+
|
|
327
|
+
return Div({ class: 'footer-container', style: { padding: '1rem 0' }}, [
|
|
328
|
+
ShowIf(user.check(u => u.isLoggedIn), P(dynamicLoginAtText))
|
|
329
|
+
]);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Use in components
|
|
333
|
+
const Header = () => {
|
|
334
|
+
const user = Store.use("user");
|
|
335
|
+
|
|
336
|
+
return Div([
|
|
337
|
+
When(user.check(u => u.isLoggedIn))
|
|
338
|
+
.show(() => {
|
|
339
|
+
const dynamicHelloText = $(() => 'Hello '+user.val().name, [user]);
|
|
340
|
+
|
|
341
|
+
return Div([
|
|
342
|
+
P(dynamicHelloText),
|
|
343
|
+
Button("Logout").nd.on.click(() => {
|
|
344
|
+
user.set({ name: null, isLoggedIn: false })
|
|
345
|
+
})
|
|
346
|
+
]);
|
|
347
|
+
})
|
|
348
|
+
.otherwise(() => {
|
|
349
|
+
return Button('Login').nd.on.click(() => {
|
|
350
|
+
user.set({ name: "John", loginAt: (new Date()).toLocaleString(), isLoggedIn: true })
|
|
351
|
+
});
|
|
352
|
+
})
|
|
353
|
+
]);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const App = () => {
|
|
357
|
+
return Div([
|
|
358
|
+
Header(),
|
|
359
|
+
Footer()
|
|
360
|
+
]);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
document.body.appendChild(App());
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
### Local State
|
|
369
|
+
|
|
370
|
+
```javascript
|
|
371
|
+
const { Div, H2, Button } = NativeDocument.elements;
|
|
372
|
+
const { Observable } = NativeDocument;
|
|
373
|
+
|
|
374
|
+
const Counter = () => {
|
|
375
|
+
const count = Observable(0);
|
|
376
|
+
|
|
377
|
+
const Increment = () => count.set(count.val() + 1);
|
|
378
|
+
const Decrement = () => count.set(currentValue => --currentValue)
|
|
379
|
+
|
|
380
|
+
return Div({}, [
|
|
381
|
+
H2(['Count: ', count]),
|
|
382
|
+
Button("-").nd.on.click(Decrement),
|
|
383
|
+
Button("+").nd.on.click(Increment),
|
|
384
|
+
]);
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
document.body.appendChild(Counter());
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
## 🔧 Advanced Features
|
|
392
|
+
|
|
393
|
+
### Lifecycle Hooks
|
|
394
|
+
|
|
395
|
+
```javascript
|
|
396
|
+
const { Div, Button, ShowIf } = NativeDocument.elements;
|
|
397
|
+
const { Observable } = NativeDocument;
|
|
398
|
+
|
|
399
|
+
const MyComponent = () => {
|
|
400
|
+
const element = Div('Hello my component');
|
|
401
|
+
|
|
402
|
+
return element
|
|
403
|
+
.nd.mounted(() => {
|
|
404
|
+
console.log("Component mounted");
|
|
405
|
+
})
|
|
406
|
+
.nd.unmounted(() => {
|
|
407
|
+
console.log("Component unmounted");
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const App = () => {
|
|
412
|
+
const isActive = Observable(true);
|
|
413
|
+
|
|
414
|
+
return Div([
|
|
415
|
+
ShowIf(isActive, () => MyComponent()),
|
|
416
|
+
Button('Toggle').nd.on.click(() => isActive.set(!isActive.val()))
|
|
417
|
+
]);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
document.body.appendChild(App());
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
### Custom Validation
|
|
425
|
+
|
|
426
|
+
```javascript
|
|
427
|
+
const { withValidation, ArgTypes } = NativeDocument;
|
|
428
|
+
|
|
429
|
+
const createUser = (name, age, email) => {
|
|
430
|
+
// Function implementation
|
|
431
|
+
return { name, age, email };
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const createUserWithArgsValidation = withValidation(createUser, [
|
|
435
|
+
ArgTypes.string("name"),
|
|
436
|
+
ArgTypes.number("age"),
|
|
437
|
+
ArgTypes.string("email")
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
// Usage
|
|
441
|
+
const user = createUserWithArgsValidation("John", 25, "john@example.com");
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Custom Validation with Function prototype (args)
|
|
445
|
+
|
|
446
|
+
```javascript
|
|
447
|
+
|
|
448
|
+
const { withValidation, ArgTypes, Observable } = NativeDocument;
|
|
449
|
+
|
|
450
|
+
const createUser = ((name, age, email) => {
|
|
451
|
+
// Function implementation
|
|
452
|
+
console.log(name, age, email);
|
|
453
|
+
return { name, age, email };
|
|
454
|
+
}).args(ArgTypes.string('name'), ArgTypes.number('age'), ArgTypes.string('email'));
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
// Usage
|
|
458
|
+
const user = createUser('John', 25, "john@example.com");
|
|
459
|
+
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
## 🎨 Styling
|
|
464
|
+
|
|
465
|
+
NativeDocument works great with CSS:
|
|
466
|
+
|
|
467
|
+
```javascript
|
|
468
|
+
const { Div, H2, Button, ShowIf } = NativeDocument.elements;
|
|
469
|
+
const { withValidation, ArgTypes, Observable } = NativeDocument;
|
|
470
|
+
|
|
471
|
+
const StyledComponent = () => {
|
|
472
|
+
const isActive = Observable(false);
|
|
473
|
+
|
|
474
|
+
return Div({
|
|
475
|
+
class: {
|
|
476
|
+
"component": true,
|
|
477
|
+
"active": isActive,
|
|
478
|
+
"inactive": isActive.check(active => !active)
|
|
479
|
+
},
|
|
480
|
+
style: {
|
|
481
|
+
color: isActive.check(active => active ? "white" : "black"),
|
|
482
|
+
backgroundColor: isActive.check(active => active ? "#2980b9" : "#7f8c8d"),
|
|
483
|
+
padding: "20px",
|
|
484
|
+
borderRadius: "4px"
|
|
485
|
+
}
|
|
486
|
+
}, [
|
|
487
|
+
H2("Styled Component"),
|
|
488
|
+
ShowIf(isActive, Div({ style: 'color: white; padding: 1rem 0'}, 'Styled Component is active')),
|
|
489
|
+
Button("Toggle").nd.on.click(() => isActive.set((val) => !val))
|
|
490
|
+
]);
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// Usage
|
|
494
|
+
document.body.appendChild(StyledComponent());
|
|
495
|
+
```
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import terser from '@rollup/plugin-terser';
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
{
|
|
5
|
+
input: {
|
|
6
|
+
main: 'index.js'
|
|
7
|
+
},
|
|
8
|
+
output: {
|
|
9
|
+
dir: 'dist',
|
|
10
|
+
entryFileNames: 'native-document.dev.js',
|
|
11
|
+
format: 'iife',
|
|
12
|
+
name: 'NativeDocument'
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
input: {
|
|
17
|
+
main: 'index.js'
|
|
18
|
+
},
|
|
19
|
+
output: {
|
|
20
|
+
dir: 'dist',
|
|
21
|
+
entryFileNames: 'native-document.min.js',
|
|
22
|
+
format: 'iife',
|
|
23
|
+
name: 'NativeDocument'
|
|
24
|
+
},
|
|
25
|
+
plugins: [
|
|
26
|
+
terser()
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
];
|
package/router.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import DebugManager from "../utils/debug-manager";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const MemoryManager = (function() {
|
|
5
|
+
|
|
6
|
+
let $nexObserverId = 0;
|
|
7
|
+
const $observables = new Map();
|
|
8
|
+
const $registry = new FinalizationRegistry((heldValue) => {
|
|
9
|
+
DebugManager.log('MemoryManager', '🧹 Auto-cleanup observable:', heldValue);
|
|
10
|
+
heldValue.listeners.splice(0);
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
/**
|
|
15
|
+
* Register an observable and return an id.
|
|
16
|
+
*
|
|
17
|
+
* @param {ObservableItem} observable
|
|
18
|
+
* @param {Function[]} listeners
|
|
19
|
+
* @returns {number}
|
|
20
|
+
*/
|
|
21
|
+
register(observable, listeners) {
|
|
22
|
+
const id = ++$nexObserverId;
|
|
23
|
+
const heldValue = {
|
|
24
|
+
id: id,
|
|
25
|
+
listeners
|
|
26
|
+
};
|
|
27
|
+
$registry.register(observable, heldValue);
|
|
28
|
+
$observables.set(id, new WeakRef(observable));
|
|
29
|
+
return id;
|
|
30
|
+
},
|
|
31
|
+
cleanup() {
|
|
32
|
+
for (const [_, weakObservableRef] of $observables) {
|
|
33
|
+
const observable = weakObservableRef.deref();
|
|
34
|
+
if (observable) {
|
|
35
|
+
observable.cleanup();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
$observables.clear();
|
|
39
|
+
},
|
|
40
|
+
/**
|
|
41
|
+
* Clean observables that are not referenced anymore.
|
|
42
|
+
* @param {number} threshold
|
|
43
|
+
*/
|
|
44
|
+
cleanObservables(threshold) {
|
|
45
|
+
if($observables.size < threshold) return;
|
|
46
|
+
let cleanedCount = 0;
|
|
47
|
+
for (const [id, weakObservableRef] of $observables) {
|
|
48
|
+
if (!weakObservableRef.deref()) {
|
|
49
|
+
$observables.delete(id);
|
|
50
|
+
cleanedCount++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (cleanedCount > 0) {
|
|
54
|
+
DebugManager.log('Memory Auto Clean', `🧹 Cleaned ${cleanedCount} orphaned observables`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}());
|
|
59
|
+
|
|
60
|
+
export default MemoryManager;
|