@deijose/nix-ionic 0.1.3 → 0.1.5

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.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * ionic-nix/IonRouterOutlet.ts
3
+ *
4
+ * Bridge 100% nativo. Genera exactamente el mismo DOM que Vanilla Ionic.
5
+ * Delegamos el view-caching y los ciclos de vida a los Eventos del DOM de ion-router-outlet.
6
+ */
7
+ import { NixComponent } from "@deijose/nix-js";
8
+ import type { NixTemplate } from "@deijose/nix-js";
9
+ import { type PageLifecycle } from "./lifecycle";
10
+ export declare function useRouter(): {
11
+ navigate: (path: string) => void;
12
+ replace: (path: string) => void;
13
+ back: () => void;
14
+ };
15
+ export declare function IonBackButton(defaultHref?: string): NixTemplate;
16
+ export interface PageContext {
17
+ lc: PageLifecycle;
18
+ params: Record<string, string>;
19
+ }
20
+ export interface RouteDefinition {
21
+ path: string;
22
+ component: (ctx: PageContext) => NixComponent | NixTemplate;
23
+ }
24
+ export declare class IonRouterOutlet extends NixComponent {
25
+ private routes;
26
+ constructor(routes: RouteDefinition[]);
27
+ private _extractParams;
28
+ private _pathToTag;
29
+ private _registerCustomElements;
30
+ render(): NixTemplate;
31
+ }
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let e=require("@deijose/nix-js");function t(){return{willEnter:(0,e.signal)(0),didEnter:(0,e.signal)(0),willLeave:(0,e.signal)(0),didLeave:(0,e.signal)(0)}}var n=class extends e.NixComponent{__lc;constructor(e){super(),this.__lc=e}onInit(){let t=this.__lc;this.ionViewWillEnter&&(0,e.watch)(t.willEnter,this.ionViewWillEnter.bind(this)),this.ionViewDidEnter&&(0,e.watch)(t.didEnter,this.ionViewDidEnter.bind(this)),this.ionViewWillLeave&&(0,e.watch)(t.willLeave,this.ionViewWillLeave.bind(this)),this.ionViewDidLeave&&(0,e.watch)(t.didLeave,this.ionViewDidLeave.bind(this))}};function r(t,n){(0,e.watch)(t.willEnter,n)}function i(t,n){(0,e.watch)(t.didEnter,n)}function a(t,n){(0,e.watch)(t.willLeave,n)}function o(t,n){(0,e.watch)(t.didLeave,n)}var s=null;function c(){if(!s)throw Error("[nix-ionic] useRouter() llamado antes de montar IonRouterOutlet");return s}function l(e){return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-back-button");return e&&i.setAttribute("default-href",e),t.insertBefore(i,n),()=>i.remove()}}}var u=new Map,d=class extends e.NixComponent{routes;ionRouterEl=null;_currentPath=(0,e.signal)(location.pathname);_canGoBack=(0,e.signal)(!1);_params=(0,e.signal)({});constructor(e){super(),this.routes=e,s={navigate:e=>this.navigate(e),replace:e=>this.replace(e),back:()=>this.back(),canGoBack:this._canGoBack,params:this._params,path:this._currentPath},this._registerCustomElements()}onInit(){window.addEventListener("popstate",this._handlePopState)}onMount(){this.ionRouterEl=document.querySelector("ion-router");let e=e=>{let{to:t,from:n}=e.detail;this._handleWillChange(t,n??"")},t=e=>{let{to:t}=e.detail;this._handleDidChange(t)},n=e=>{e.detail.register(100,e=>{this._canGoBack.value?this.back():e()})};return window.addEventListener("ionRouteWillChange",e),window.addEventListener("ionRouteDidChange",t),document.addEventListener("ionBackButton",n),()=>{window.removeEventListener("popstate",this._handlePopState),window.removeEventListener("ionRouteWillChange",e),window.removeEventListener("ionRouteDidChange",t),document.removeEventListener("ionBackButton",n),s=null}}navigate(e){e!==location.pathname&&this.ionRouterEl?.push(e,"forward")}replace(e){this.ionRouterEl?.push(e,"root")}back(){this.ionRouterEl?.back()}_handlePopState=()=>{this._handleWillChange(location.pathname,this._currentPath.peek())};_handleWillChange(e,t){let n=this._matchRoute(e);if(!n)return;if(n.beforeEnter){let i=n.beforeEnter(e,t);if(!1===i)return void this.back();if("string"==typeof i)return void this.navigate(i)}this._currentPath.value=e,this._params.value=this._extractParams(n.path,e),u.get(this._pathToTag(t))?.willLeave.update(e=>e+1);let i=this._pathToTag(e),a=u.get(i);a&&(a.willEnter.update(e=>e+1),a._pendingEnter=!0)}_handleDidChange(e){let t=document.querySelector("ion-nav");t&&"function"==typeof t.canGoBack?t.canGoBack().then(e=>{this._canGoBack.value=e}):this._canGoBack.value=!1,u.forEach((t,n)=>{n!==this._pathToTag(e)&&t.didLeave.update(e=>e+1)});let n=this._pathToTag(e),i=u.get(n);i&&i._pendingEnter&&(i.didEnter.update(e=>e+1),i._pendingEnter=!1)}_matchRoute(e){let t=this.routes.find(t=>t.path===e);if(t)return t;for(let t of this.routes)if(RegExp(`^${t.path.replace(/:[^/]+/g,"([^/]+)")}$`).test(e))return t;return this.routes.find(e=>"*"===e.path)}_extractParams(e,t){let n={},i=e.split("/"),a=t.split("/");for(let e=0;e<i.length;e++)i[e].startsWith(":")&&(n[i[e].slice(1)]=a[e]??"");return n}_pathToTag(e){return e&&"/"!==e?`nix-page-${e.replace(/\/:?[^/]+/g,e=>"-"+e.replace(/\//g,"").replace(/:/g,"")).replace(/^\//,"").replace(/\//g,"-")}`:"nix-page-home"}_registerCustomElements(){for(let e of this.routes){if("*"===e.path)continue;let n=this._pathToTag(e.path),i=e,a=this;customElements.get(n)||customElements.define(n,class extends HTMLElement{_cleanup=null;_lc=null;_isInitialized=!1;connectedCallback(){if(this._isInitialized)return;this._isInitialized=!0;let e=a._extractParams(i.path,location.pathname),o=t();this._lc=o,u.set(n,o),this.classList.add("ion-page");let r=i.component({lc:o,params:e});if("render"in r&&"function"==typeof r.render){let e=r;e.onInit?.();let t=e.render()._render(this,null),n=e.onMount?.();this._cleanup=()=>{e.onUnmount?.(),"function"==typeof n&&n(),t()}}else this._cleanup=r._render(this,null);o.willEnter.update(e=>e+1);let l=e=>{a._pathToTag(e.detail.to)===n&&(o.didEnter.update(e=>e+1),window.removeEventListener("ionRouteDidChange",l))};window.addEventListener("ionRouteDidChange",l)}disconnectedCallback(){let e=this._lc;setTimeout(()=>{this.isConnected||(u.delete(n),e?.willLeave.update(e=>e+1),e?.didLeave.update(e=>e+1),this._cleanup?.(),this._cleanup=null,this._lc=null,this._isInitialized=!1)},100)}})}}render(){let e=this;return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-router");i.setAttribute("use-hash","false"),i.innerHTML=e.routes.filter(e=>"*"!==e.path).map(t=>`<ion-route url="${t.path}" component="${e._pathToTag(t.path)}"></ion-route>`).join("");let a=document.createElement("ion-nav");return t.insertBefore(i,n),t.insertBefore(a,n),e.ionRouterEl=i,()=>{i.remove(),a.remove()}}}}onUnmount(){u.clear(),s=null}};exports.IonBackButton=l,exports.IonPage=n,exports.IonRouterOutlet=d,exports.createPageLifecycle=t,exports.useIonViewDidEnter=i,exports.useIonViewDidLeave=o,exports.useIonViewWillEnter=r,exports.useIonViewWillLeave=a,exports.useRouter=c;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let e=require("@deijose/nix-js");function t(){return{willEnter:(0,e.signal)(0),didEnter:(0,e.signal)(0),willLeave:(0,e.signal)(0),didLeave:(0,e.signal)(0)}}var n=class extends e.NixComponent{__lc;constructor(e){super(),this.__lc=e}onInit(){let t=this.__lc;this.ionViewWillEnter&&(0,e.watch)(t.willEnter,this.ionViewWillEnter.bind(this)),this.ionViewDidEnter&&(0,e.watch)(t.didEnter,this.ionViewDidEnter.bind(this)),this.ionViewWillLeave&&(0,e.watch)(t.willLeave,this.ionViewWillLeave.bind(this)),this.ionViewDidLeave&&(0,e.watch)(t.didLeave,this.ionViewDidLeave.bind(this))}};function r(t,n){(0,e.watch)(t.willEnter,n)}function i(t,n){(0,e.watch)(t.didEnter,n)}function a(t,n){(0,e.watch)(t.willLeave,n)}function o(t,n){(0,e.watch)(t.didLeave,n)}function s(){let e=document.querySelector("ion-router");return{navigate:t=>{e?.push(t,"forward")},replace:t=>{e?.push(t,"root")},back:()=>{e?.back()}}}function c(e){return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-back-button");return e&&i.setAttribute("default-href",e),t.insertBefore(i,n),()=>i.remove()}}}var l=class extends e.NixComponent{routes;constructor(e){super(),this.routes=e,this._registerCustomElements()}_extractParams(e,t){let n={},i=e.split("/"),r=t.split("/");for(let e=0;e<i.length;e++)i[e]?.startsWith(":")&&(n[i[e].slice(1)]=r[e]??"");return n}_pathToTag(e){return e&&"/"!==e?`nix-page-${e.replace(/\/:?[^/]+/g,e=>"-"+e.replace(/\//g,"").replace(/:/g,"")).replace(/^\//,"").replace(/\//g,"-")}`:"nix-page-home"}_registerCustomElements(){let e=this;for(let n of this.routes){if("*"===n.path)continue;let i=this._pathToTag(n.path);customElements.get(i)||customElements.define(i,class extends HTMLElement{_handle=null;connectedCallback(){if(this._handle)return;this.classList.add("ion-page");let i=t();this.addEventListener("ionViewWillEnter",()=>i.willEnter.update(e=>e+1)),this.addEventListener("ionViewDidEnter",()=>i.didEnter.update(e=>e+1)),this.addEventListener("ionViewWillLeave",()=>i.willLeave.update(e=>e+1)),this.addEventListener("ionViewDidLeave",()=>i.didLeave.update(e=>e+1));let r=e._extractParams(n.path,location.pathname),o=n.component({lc:i,params:r});if("render"in o&&"function"==typeof o.render){let e=o;e.onInit?.();let t=e.render()._render(this,null),n=e.onMount?.();this._handle={unmount:()=>{e.onUnmount?.(),"function"==typeof n&&n(),t()}}}else this._handle={unmount:o._render(this,null)}}disconnectedCallback(){this._handle?.unmount(),this._handle=null}})}}render(){let e=this;return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-router");i.setAttribute("use-hash","false"),i.innerHTML=e.routes.filter(e=>"*"!==e.path).map(t=>`<ion-route url="${t.path}" component="${e._pathToTag(t.path)}"></ion-route>`).join("");let r=document.createElement("ion-router-outlet");return t.insertBefore(i,n),t.insertBefore(r,n),()=>{i.remove(),r.remove()}}}}};exports.IonBackButton=c,exports.IonPage=n,exports.IonRouterOutlet=l,exports.createPageLifecycle=t,exports.useIonViewDidEnter=i,exports.useIonViewDidLeave=o,exports.useIonViewWillEnter=r,exports.useIonViewWillLeave=a,exports.useRouter=s;
@@ -1 +1 @@
1
- {"version":3,"file":"nix-ionic.cjs","names":[],"sources":["../../src/lifecycle.ts","../../src/IonRouterOutlet.ts"],"sourcesContent":["/**\n * ionic-nix/lifecycle.ts\n *\n * Sistema de ciclo de vida de navegación, análogo a los hooks de Ionic:\n * ionViewWillEnter / ionViewDidEnter / ionViewWillLeave / ionViewDidLeave\n *\n * Cómo funciona (sin provide/inject):\n * 1. IonRouterOutlet crea un `PageLifecycle` por cada ruta.\n * 2. Lo pasa directamente al factory de la ruta como argumento.\n * 3. El factory llama a `new MiPagina(lc)` o `MiPagina(lc)`.\n * 4. IonPage/composables registran watchers sobre las señales del lc.\n * 5. Cuando el router navega, incrementa las señales → watchers se disparan.\n */\n\nimport { signal, watch } from \"@deijose/nix-js\";\nimport type { Signal } from \"@deijose/nix-js\";\nimport { NixComponent } from \"@deijose/nix-js\";\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageLifecycle {\n willEnter: Signal<number>;\n didEnter: Signal<number>;\n willLeave: Signal<number>;\n didLeave: Signal<number>;\n}\n\n/** Crea un nuevo PageLifecycle con señales en 0. */\nexport function createPageLifecycle(): PageLifecycle {\n return {\n willEnter: signal(0),\n didEnter: signal(0),\n willLeave: signal(0),\n didLeave: signal(0),\n };\n}\n\n// --------------------------------------------------------------------------\n// IonPage — clase base para páginas con hooks de navegación\n// --------------------------------------------------------------------------\n//\n// Uso:\n// class HomePage extends IonPage {\n// constructor(lc: PageLifecycle) { super(lc); }\n//\n// ionViewWillEnter() { /* fetch de datos frescos */ }\n// render() { return html`...`; }\n// }\n\nexport abstract class IonPage extends NixComponent {\n private __lc: PageLifecycle;\n\n constructor(lc: PageLifecycle) {\n super();\n this.__lc = lc;\n }\n\n override onInit(): void {\n const lc = this.__lc;\n // watch no corre en init (immediate: false), así que ionViewWillEnter\n // solo se llama cuando el outlet incrementa la señal, no al construir.\n if (this.ionViewWillEnter) watch(lc.willEnter, this.ionViewWillEnter.bind(this));\n if (this.ionViewDidEnter) watch(lc.didEnter, this.ionViewDidEnter.bind(this));\n if (this.ionViewWillLeave) watch(lc.willLeave, this.ionViewWillLeave.bind(this));\n if (this.ionViewDidLeave) watch(lc.didLeave, this.ionViewDidLeave.bind(this));\n }\n\n ionViewWillEnter?(): void;\n ionViewDidEnter?(): void;\n ionViewWillLeave?(): void;\n ionViewDidLeave?(): void;\n}\n\n// --------------------------------------------------------------------------\n// Composables para function components\n// --------------------------------------------------------------------------\n//\n// Uso:\n// function ProfilePage(lc: PageLifecycle): NixTemplate {\n// useIonViewWillEnter(lc, () => { /* fetch */ });\n// return html`...`;\n// }\n\nexport function useIonViewWillEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willEnter, fn);\n}\n\nexport function useIonViewDidEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didEnter, fn);\n}\n\nexport function useIonViewWillLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willLeave, fn);\n}\n\nexport function useIonViewDidLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didLeave, fn);\n}","/**\n * ionic-nix/IonRouterOutlet.ts\n *\n * Bridge entre Nix.js e ion-router (API pública oficial para vanilla JS).\n *\n * Arquitectura:\n * 1. Por cada ruta registramos un Custom Element (nix-page-home, etc.)\n * 2. ion-router activa el custom element según la URL\n * 3. ion-router-outlet gestiona: caché, animaciones, back button, swipe back\n * 4. connectedCallback del custom element monta el componente Nix adentro\n * 5. El flag _mounted evita destruir el componente cuando ion-router-outlet\n * desconecta el elemento temporalmente (caché). Solo se destruye si\n * el elemento no vuelve a conectarse en los siguientes 100ms.\n */\n\nimport { NixComponent, signal } from \"@deijose/nix-js\";\nimport type { NixTemplate, Signal } from \"@deijose/nix-js\";\nimport { createPageLifecycle, type PageLifecycle } from \"./lifecycle\";\n\n// --------------------------------------------------------------------------\n// Router store singleton\n// --------------------------------------------------------------------------\n\ninterface RouterStore {\n navigate: (path: string) => void;\n replace: (path: string) => void;\n back: () => void;\n canGoBack: Signal<boolean>;\n params: Signal<Record<string, string>>;\n path: Signal<string>;\n}\n\nlet _router: RouterStore | null = null;\n\nexport function useRouter(): RouterStore {\n if (!_router) throw new Error(\"[nix-ionic] useRouter() llamado antes de montar IonRouterOutlet\");\n return _router;\n}\n\n// --------------------------------------------------------------------------\n// IonBackButton\n// --------------------------------------------------------------------------\n\nexport function IonBackButton(defaultHref?: string): NixTemplate {\n return {\n __isNixTemplate: true as const,\n mount(container) {\n const el = typeof container === \"string\"\n ? document.querySelector(container)! as Element\n : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n const btn = document.createElement(\"ion-back-button\") as HTMLElement;\n \n // Solo asigna si existe. Si no, Ionic lo oculta de forma inteligente.\n if (defaultHref) {\n btn.setAttribute(\"default-href\", defaultHref);\n }\n\n parent.insertBefore(btn, before);\n return () => btn.remove();\n },\n };\n}\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageContext {\n lc: PageLifecycle;\n params: Record<string, string>;\n}\n\nexport interface RouteDefinition {\n path: string;\n component: (ctx: PageContext) => NixComponent | NixTemplate;\n beforeEnter?: (to: string, from: string) => boolean | string | void;\n}\n\n// --------------------------------------------------------------------------\n// Mapa global de lifecycles activos por tag\n// --------------------------------------------------------------------------\n\nconst _lifecycleRegistry = new Map<string, PageLifecycle>();\n\n// --------------------------------------------------------------------------\n// IonRouterOutlet\n// --------------------------------------------------------------------------\n\nexport class IonRouterOutlet extends NixComponent {\n private routes: RouteDefinition[];\n private ionRouterEl: HTMLElement | null = null;\n\n private _currentPath = signal(location.pathname);\n private _canGoBack = signal(false);\n private _params = signal<Record<string, string>>({});\n\n constructor(routes: RouteDefinition[]) {\n super();\n this.routes = routes;\n\n _router = {\n navigate: (path) => this.navigate(path),\n replace: (path) => this.replace(path),\n back: () => this.back(),\n canGoBack: this._canGoBack,\n params: this._params,\n path: this._currentPath,\n };\n\n this._registerCustomElements();\n }\n\n override onInit(): void {\n window.addEventListener(\"popstate\", this._handlePopState);\n }\n\n override onMount(): () => void {\n this.ionRouterEl = document.querySelector(\"ion-router\");\n\n const onWillChange = (ev: Event) => {\n const { to, from } = (ev as CustomEvent).detail as { to: string; from: string };\n this._handleWillChange(to, from ?? \"\");\n };\n const onDidChange = (ev: Event) => {\n const { to } = (ev as CustomEvent).detail as { to: string };\n this._handleDidChange(to);\n };\n const onIonBack = (ev: Event) => {\n (ev as CustomEvent).detail.register(100, (next: () => void) => {\n if (this._canGoBack.value) this.back(); else next();\n });\n };\n\n window.addEventListener(\"ionRouteWillChange\", onWillChange);\n window.addEventListener(\"ionRouteDidChange\", onDidChange);\n document.addEventListener(\"ionBackButton\", onIonBack);\n\n return () => {\n window.removeEventListener(\"popstate\", this._handlePopState);\n window.removeEventListener(\"ionRouteWillChange\", onWillChange);\n window.removeEventListener(\"ionRouteDidChange\", onDidChange);\n document.removeEventListener(\"ionBackButton\", onIonBack);\n _router = null;\n };\n }\n\n // -------------------------------------------------------------------------\n // API pública\n // -------------------------------------------------------------------------\n\n navigate(path: string): void {\n if (path === location.pathname) return;\n (this.ionRouterEl as any)?.push(path, \"forward\");\n }\n\n replace(path: string): void {\n (this.ionRouterEl as any)?.push(path, \"root\");\n }\n\n back(): void {\n (this.ionRouterEl as any)?.back();\n }\n\n // -------------------------------------------------------------------------\n // Handlers privados\n // -------------------------------------------------------------------------\n\n private _handlePopState = (): void => {\n this._handleWillChange(location.pathname, this._currentPath.peek());\n };\n\n private _handleWillChange(to: string, from: string): void {\n const route = this._matchRoute(to);\n if (!route) return;\n\n if (route.beforeEnter) {\n const result = route.beforeEnter(to, from);\n if (result === false) { this.back(); return; }\n if (typeof result === \"string\") { this.navigate(result); return; }\n }\n\n this._currentPath.value = to;\n this._params.value = this._extractParams(route.path, to);\n\n // Despide a la vista que sale\n _lifecycleRegistry.get(this._pathToTag(from))?.willLeave.update((n) => n + 1);\n\n // Si la vista a la que vamos ya existe en el registro, significa que ESTÁ EN CACHÉ.\n const toTag = this._pathToTag(to);\n const toLc = _lifecycleRegistry.get(toTag);\n if (toLc) {\n toLc.willEnter.update((n) => n + 1);\n (toLc as any)._pendingEnter = true; // Flag para que el didChange dispare el didEnter\n }\n }\n\n private _handleDidChange(to: string): void {\n const nav = document.querySelector(\"ion-nav\") as any;\n \n // ion-nav.canGoBack() retorna una Promesa, la resolvemos para tu señal\n if (nav && typeof nav.canGoBack === \"function\") {\n nav.canGoBack().then((res: boolean) => {\n this._canGoBack.value = res;\n });\n } else {\n this._canGoBack.value = false;\n }\n\n _lifecycleRegistry.forEach((lc, tag) => {\n if (tag !== this._pathToTag(to)) lc.didLeave.update((n) => n + 1);\n });\n\n // Avisa a la vista cacheada que la transición ya terminó\n const toTag = this._pathToTag(to);\n const toLc = _lifecycleRegistry.get(toTag);\n if (toLc && (toLc as any)._pendingEnter) {\n toLc.didEnter.update((n) => n + 1);\n (toLc as any)._pendingEnter = false;\n }\n }\n\n // -------------------------------------------------------------------------\n // Helpers\n // -------------------------------------------------------------------------\n\n private _matchRoute(path: string): RouteDefinition | undefined {\n const exact = this.routes.find((r) => r.path === path);\n if (exact) return exact;\n for (const route of this.routes) {\n const re = new RegExp(`^${route.path.replace(/:[^/]+/g, \"([^/]+)\")}$`);\n if (re.test(path)) return route;\n }\n return this.routes.find((r) => r.path === \"*\");\n }\n\n private _extractParams(routePath: string, realPath: string): Record<string, string> {\n const params: Record<string, string> = {};\n const rP = routePath.split(\"/\");\n const uP = realPath.split(\"/\");\n for (let i = 0; i < rP.length; i++) {\n if (rP[i].startsWith(\":\")) params[rP[i].slice(1)] = uP[i] ?? \"\";\n }\n return params;\n }\n\n private _pathToTag(path: string): string {\n if (!path || path === \"/\") return \"nix-page-home\";\n const clean = path\n .replace(/\\/:?[^/]+/g, (m) => \"-\" + m.replace(/\\//g, \"\").replace(/:/g, \"\"))\n .replace(/^\\//, \"\")\n .replace(/\\//g, \"-\");\n return `nix-page-${clean}`;\n }\n\n // -------------------------------------------------------------------------\n // Registro de Custom Elements\n // -------------------------------------------------------------------------\n\n private _registerCustomElements(): void {\n for (const route of this.routes) {\n if (route.path === \"*\") continue;\n\n const tag = this._pathToTag(route.path);\n const routeDef = route;\n const self = this;\n\n if (customElements.get(tag)) continue;\n\n customElements.define(tag, class extends HTMLElement {\n private _cleanup: (() => void) | null = null;\n private _lc: PageLifecycle | null = null;\n private _isInitialized = false; // Solo necesitamos este flag\n\n connectedCallback(): void {\n if (this._isInitialized) return;\n this._isInitialized = true;\n\n const params = self._extractParams(routeDef.path, location.pathname);\n const lc = createPageLifecycle();\n this._lc = lc;\n\n _lifecycleRegistry.set(tag, lc);\n this.classList.add(\"ion-page\");\n\n const pageNode = routeDef.component({ lc, params });\n\n if (\"render\" in pageNode && typeof (pageNode as NixComponent).render === \"function\") {\n const comp = pageNode as NixComponent;\n comp.onInit?.();\n const renderCleanup = comp.render()._render(this, null);\n const mountRet = comp.onMount?.();\n this._cleanup = () => {\n comp.onUnmount?.();\n if (typeof mountRet === \"function\") mountRet();\n renderCleanup();\n };\n } else {\n this._cleanup = (pageNode as NixTemplate)._render(this, null);\n }\n\n // Disparar hooks de la primera vez\n lc.willEnter.update((n) => n + 1);\n \n // Esperar al cambio de ruta para disparar didEnter de forma segura\n const onReady = (e: Event) => {\n if (self._pathToTag((e as CustomEvent).detail.to) === tag) {\n lc.didEnter.update((n) => n + 1);\n window.removeEventListener(\"ionRouteDidChange\", onReady);\n }\n };\n window.addEventListener(\"ionRouteDidChange\", onReady);\n }\n\n disconnectedCallback(): void {\n const lc = this._lc;\n \n setTimeout(() => {\n // Si después de 100ms sigue conectado, fue solo una animación de Ionic\n if (this.isConnected) return; \n\n // Destrucción real (se sacó del stack)\n _lifecycleRegistry.delete(tag);\n lc?.willLeave.update((n) => n + 1);\n lc?.didLeave.update((n) => n + 1);\n this._cleanup?.();\n this._cleanup = null;\n this._lc = null;\n this._isInitialized = false; // Permite que se recree en el futuro\n }, 100);\n }\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Render\n // -------------------------------------------------------------------------\n\n override render(): NixTemplate {\n const self = this;\n return {\n __isNixTemplate: true as const,\n mount(container: Element | string): { unmount(): void } {\n const el = typeof container === \"string\"\n ? (document.querySelector(container) as Element)\n : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n const routerEl = document.createElement(\"ion-router\");\n routerEl.setAttribute(\"use-hash\", \"false\");\n routerEl.innerHTML = self.routes\n .filter((r) => r.path !== \"*\")\n .map((r) => `<ion-route url=\"${r.path}\" component=\"${self._pathToTag(r.path)}\"></ion-route>`)\n .join(\"\");\n\n // 🚀 EL FIX MÁGICO: ion-nav mantiene los nodos en el DOM nativamente\n const navEl = document.createElement(\"ion-nav\");\n\n parent.insertBefore(routerEl, before);\n parent.insertBefore(navEl, before);\n\n self.ionRouterEl = routerEl;\n\n return () => { routerEl.remove(); navEl.remove(); };\n },\n };\n }\n\n override onUnmount(): void {\n _lifecycleRegistry.clear();\n _router = null;\n }\n}"],"mappings":"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"}
1
+ {"version":3,"file":"nix-ionic.cjs","names":[],"sources":["../../src/lifecycle.ts","../../src/IonRouterOutlet.ts"],"sourcesContent":["/**\n * ionic-nix/lifecycle.ts\n *\n * Sistema de ciclo de vida de navegación, análogo a los hooks de Ionic:\n * ionViewWillEnter / ionViewDidEnter / ionViewWillLeave / ionViewDidLeave\n *\n * Cómo funciona (sin provide/inject):\n * 1. IonRouterOutlet crea un `PageLifecycle` por cada ruta.\n * 2. Lo pasa directamente al factory de la ruta como argumento.\n * 3. El factory llama a `new MiPagina(lc)` o `MiPagina(lc)`.\n * 4. IonPage/composables registran watchers sobre las señales del lc.\n * 5. Cuando el router navega, incrementa las señales → watchers se disparan.\n */\n\nimport { signal, watch } from \"@deijose/nix-js\";\nimport type { Signal } from \"@deijose/nix-js\";\nimport { NixComponent } from \"@deijose/nix-js\";\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageLifecycle {\n willEnter: Signal<number>;\n didEnter: Signal<number>;\n willLeave: Signal<number>;\n didLeave: Signal<number>;\n}\n\n/** Crea un nuevo PageLifecycle con señales en 0. */\nexport function createPageLifecycle(): PageLifecycle {\n return {\n willEnter: signal(0),\n didEnter: signal(0),\n willLeave: signal(0),\n didLeave: signal(0),\n };\n}\n\n// --------------------------------------------------------------------------\n// IonPage — clase base para páginas con hooks de navegación\n// --------------------------------------------------------------------------\n//\n// Uso:\n// class HomePage extends IonPage {\n// constructor(lc: PageLifecycle) { super(lc); }\n//\n// ionViewWillEnter() { /* fetch de datos frescos */ }\n// render() { return html`...`; }\n// }\n\nexport abstract class IonPage extends NixComponent {\n private __lc: PageLifecycle;\n\n constructor(lc: PageLifecycle) {\n super();\n this.__lc = lc;\n }\n\n override onInit(): void {\n const lc = this.__lc;\n // watch no corre en init (immediate: false), así que ionViewWillEnter\n // solo se llama cuando el outlet incrementa la señal, no al construir.\n if (this.ionViewWillEnter) watch(lc.willEnter, this.ionViewWillEnter.bind(this));\n if (this.ionViewDidEnter) watch(lc.didEnter, this.ionViewDidEnter.bind(this));\n if (this.ionViewWillLeave) watch(lc.willLeave, this.ionViewWillLeave.bind(this));\n if (this.ionViewDidLeave) watch(lc.didLeave, this.ionViewDidLeave.bind(this));\n }\n\n ionViewWillEnter?(): void;\n ionViewDidEnter?(): void;\n ionViewWillLeave?(): void;\n ionViewDidLeave?(): void;\n}\n\n// --------------------------------------------------------------------------\n// Composables para function components\n// --------------------------------------------------------------------------\n//\n// Uso:\n// function ProfilePage(lc: PageLifecycle): NixTemplate {\n// useIonViewWillEnter(lc, () => { /* fetch */ });\n// return html`...`;\n// }\n\nexport function useIonViewWillEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willEnter, fn);\n}\n\nexport function useIonViewDidEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didEnter, fn);\n}\n\nexport function useIonViewWillLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willLeave, fn);\n}\n\nexport function useIonViewDidLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didLeave, fn);\n}","/**\n * ionic-nix/IonRouterOutlet.ts\n *\n * Bridge 100% nativo. Genera exactamente el mismo DOM que Vanilla Ionic.\n * Delegamos el view-caching y los ciclos de vida a los Eventos del DOM de ion-router-outlet.\n */\n\nimport { NixComponent } from \"@deijose/nix-js\";\nimport type { NixTemplate } from \"@deijose/nix-js\";\nimport { createPageLifecycle, type PageLifecycle } from \"./lifecycle\";\nimport { Components } from \"@ionic/core\";\n\n// --------------------------------------------------------------------------\n// Router store singleton\n// --------------------------------------------------------------------------\n\nexport function useRouter() {\n const router = <Components.IonRouter>document.querySelector(\"ion-router\");\n return {\n navigate: (path: string) => {\n router?.push(path, \"forward\");\n },\n replace: (path: string) => {\n router?.push(path, \"root\");\n },\n back: () => {\n router?.back();\n }\n };\n}\n\n// --------------------------------------------------------------------------\n// IonBackButton (Sin listeners, 100% nativo)\n// --------------------------------------------------------------------------\n\nexport function IonBackButton(defaultHref?: string): NixTemplate {\n return {\n __isNixTemplate: true as const,\n mount(container) {\n const el = typeof container === \"string\" ? document.querySelector(container)! : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n const btn = document.createElement(\"ion-back-button\") as HTMLElement;\n if (defaultHref) btn.setAttribute(\"default-href\", defaultHref);\n parent.insertBefore(btn, before);\n return () => btn.remove();\n },\n };\n}\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageContext {\n lc: PageLifecycle;\n params: Record<string, string>;\n}\n\nexport interface RouteDefinition {\n path: string;\n component: (ctx: PageContext) => NixComponent | NixTemplate;\n}\n\n// --------------------------------------------------------------------------\n// IonRouterOutlet (El Bridge Maestro)\n// --------------------------------------------------------------------------\n\nexport class IonRouterOutlet extends NixComponent {\n private routes: RouteDefinition[];\n\n constructor(routes: RouteDefinition[]) {\n super();\n this.routes = routes;\n this._registerCustomElements();\n }\n\n // -------------------------------------------------------------------------\n // Helpers\n // -------------------------------------------------------------------------\n\n private _extractParams(routePath: string, realPath: string): Record<string, string> {\n const params: Record<string, string> = {};\n const rP = routePath.split(\"/\");\n const uP = realPath.split(\"/\");\n for (let i = 0; i < rP.length; i++) {\n if (rP[i]?.startsWith(\":\")) params[rP[i].slice(1)] = uP[i] ?? \"\";\n }\n return params;\n }\n\n private _pathToTag(path: string): string {\n if (!path || path === \"/\") return \"nix-page-home\";\n const clean = path\n .replace(/\\/:?[^/]+/g, (m) => \"-\" + m.replace(/\\//g, \"\").replace(/:/g, \"\"))\n .replace(/^\\//, \"\")\n .replace(/\\//g, \"-\");\n return `nix-page-${clean}`;\n }\n\n // -------------------------------------------------------------------------\n // Registro de Custom Elements estilo Vanilla JS\n // -------------------------------------------------------------------------\n\n private _registerCustomElements(): void {\n const self = this;\n for (const route of this.routes) {\n if (route.path === \"*\") continue;\n\n const tag = this._pathToTag(route.path);\n if (customElements.get(tag)) continue;\n\n customElements.define(tag, class extends HTMLElement {\n private _handle: { unmount(): void } | null = null;\n\n connectedCallback(): void {\n // Prevenir doble montaje si Ionic reconecta el nodo\n if (this._handle) return; \n\n this.classList.add(\"ion-page\");\n\n // 1. Escuchar los eventos NATIVOS del DOM que dispara ion-router-outlet\n const lc = createPageLifecycle();\n this.addEventListener(\"ionViewWillEnter\", () => lc.willEnter.update((n) => n + 1));\n this.addEventListener(\"ionViewDidEnter\", () => lc.didEnter.update((n) => n + 1));\n this.addEventListener(\"ionViewWillLeave\", () => lc.willLeave.update((n) => n + 1));\n this.addEventListener(\"ionViewDidLeave\", () => lc.didLeave.update((n) => n + 1));\n\n // 2. Extraer parámetros\n const params = self._extractParams(route.path, location.pathname);\n\n // 3. Montar componente Nix\n const pageNode = route.component({ lc, params });\n\n if (\"render\" in pageNode && typeof (pageNode as NixComponent).render === \"function\") {\n const comp = pageNode as NixComponent;\n comp.onInit?.();\n const renderCleanup = comp.render()._render(this, null);\n const mountRet = comp.onMount?.();\n\n this._handle = {\n unmount: () => {\n comp.onUnmount?.();\n if (typeof mountRet === \"function\") mountRet();\n renderCleanup();\n }\n };\n } else {\n const renderCleanup = (pageNode as NixTemplate)._render(this, null);\n this._handle = { unmount: renderCleanup };\n }\n }\n\n disconnectedCallback(): void {\n // ion-router-outlet SOLO desconecta el elemento cuando lo saca \n // definitivamente de la caché (ej. al hacer click en el back-button)\n this._handle?.unmount();\n this._handle = null;\n }\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Render exacto al HTML que te funcionaba\n // -------------------------------------------------------------------------\n\n override render(): NixTemplate {\n const self = this;\n return {\n __isNixTemplate: true as const,\n mount(container: Element | string) {\n const el = typeof container === \"string\" ? document.querySelector(container)! : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n \n // Creamos <ion-router>\n const routerEl = document.createElement(\"ion-router\");\n routerEl.setAttribute(\"use-hash\", \"false\");\n routerEl.innerHTML = self.routes\n .filter((r) => r.path !== \"*\")\n .map((r) => `<ion-route url=\"${r.path}\" component=\"${self._pathToTag(r.path)}\"></ion-route>`)\n .join(\"\");\n\n // Creamos <ion-router-outlet> real\n const outletEl = document.createElement(\"ion-router-outlet\");\n\n parent.insertBefore(routerEl, before);\n parent.insertBefore(outletEl, before);\n\n return () => {\n routerEl.remove();\n outletEl.remove();\n };\n },\n };\n }\n}"],"mappings":"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"}
@@ -1 +1 @@
1
- import{NixComponent as e,signal as t,watch as n}from"@deijose/nix-js";function r(){return{willEnter:t(0),didEnter:t(0),willLeave:t(0),didLeave:t(0)}}var i=class extends e{__lc;constructor(e){super(),this.__lc=e}onInit(){let e=this.__lc;this.ionViewWillEnter&&n(e.willEnter,this.ionViewWillEnter.bind(this)),this.ionViewDidEnter&&n(e.didEnter,this.ionViewDidEnter.bind(this)),this.ionViewWillLeave&&n(e.willLeave,this.ionViewWillLeave.bind(this)),this.ionViewDidLeave&&n(e.didLeave,this.ionViewDidLeave.bind(this))}};function a(e,t){n(e.willEnter,t)}function o(e,t){n(e.didEnter,t)}function s(e,t){n(e.willLeave,t)}function c(e,t){n(e.didLeave,t)}var l=null;function u(){if(!l)throw Error("[nix-ionic] useRouter() llamado antes de montar IonRouterOutlet");return l}function d(e){return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-back-button");return e&&i.setAttribute("default-href",e),t.insertBefore(i,n),()=>i.remove()}}}var f=new Map,p=class extends e{routes;ionRouterEl=null;_currentPath=t(location.pathname);_canGoBack=t(!1);_params=t({});constructor(e){super(),this.routes=e,l={navigate:e=>this.navigate(e),replace:e=>this.replace(e),back:()=>this.back(),canGoBack:this._canGoBack,params:this._params,path:this._currentPath},this._registerCustomElements()}onInit(){window.addEventListener("popstate",this._handlePopState)}onMount(){this.ionRouterEl=document.querySelector("ion-router");let e=e=>{let{to:t,from:n}=e.detail;this._handleWillChange(t,n??"")},t=e=>{let{to:t}=e.detail;this._handleDidChange(t)},n=e=>{e.detail.register(100,e=>{this._canGoBack.value?this.back():e()})};return window.addEventListener("ionRouteWillChange",e),window.addEventListener("ionRouteDidChange",t),document.addEventListener("ionBackButton",n),()=>{window.removeEventListener("popstate",this._handlePopState),window.removeEventListener("ionRouteWillChange",e),window.removeEventListener("ionRouteDidChange",t),document.removeEventListener("ionBackButton",n),l=null}}navigate(e){e!==location.pathname&&this.ionRouterEl?.push(e,"forward")}replace(e){this.ionRouterEl?.push(e,"root")}back(){this.ionRouterEl?.back()}_handlePopState=()=>{this._handleWillChange(location.pathname,this._currentPath.peek())};_handleWillChange(e,t){let n=this._matchRoute(e);if(!n)return;if(n.beforeEnter){let i=n.beforeEnter(e,t);if(!1===i)return void this.back();if("string"==typeof i)return void this.navigate(i)}this._currentPath.value=e,this._params.value=this._extractParams(n.path,e),f.get(this._pathToTag(t))?.willLeave.update(e=>e+1);let i=this._pathToTag(e),a=f.get(i);a&&(a.willEnter.update(e=>e+1),a._pendingEnter=!0)}_handleDidChange(e){let t=document.querySelector("ion-nav");t&&"function"==typeof t.canGoBack?t.canGoBack().then(e=>{this._canGoBack.value=e}):this._canGoBack.value=!1,f.forEach((t,n)=>{n!==this._pathToTag(e)&&t.didLeave.update(e=>e+1)});let n=this._pathToTag(e),i=f.get(n);i&&i._pendingEnter&&(i.didEnter.update(e=>e+1),i._pendingEnter=!1)}_matchRoute(e){let t=this.routes.find(t=>t.path===e);if(t)return t;for(let t of this.routes)if(RegExp(`^${t.path.replace(/:[^/]+/g,"([^/]+)")}$`).test(e))return t;return this.routes.find(e=>"*"===e.path)}_extractParams(e,t){let n={},i=e.split("/"),a=t.split("/");for(let e=0;e<i.length;e++)i[e].startsWith(":")&&(n[i[e].slice(1)]=a[e]??"");return n}_pathToTag(e){return e&&"/"!==e?`nix-page-${e.replace(/\/:?[^/]+/g,e=>"-"+e.replace(/\//g,"").replace(/:/g,"")).replace(/^\//,"").replace(/\//g,"-")}`:"nix-page-home"}_registerCustomElements(){for(let e of this.routes){if("*"===e.path)continue;let t=this._pathToTag(e.path),n=e,i=this;customElements.get(t)||customElements.define(t,class extends HTMLElement{_cleanup=null;_lc=null;_isInitialized=!1;connectedCallback(){if(this._isInitialized)return;this._isInitialized=!0;let e=i._extractParams(n.path,location.pathname),a=r();this._lc=a,f.set(t,a),this.classList.add("ion-page");let o=n.component({lc:a,params:e});if("render"in o&&"function"==typeof o.render){let e=o;e.onInit?.();let t=e.render()._render(this,null),n=e.onMount?.();this._cleanup=()=>{e.onUnmount?.(),"function"==typeof n&&n(),t()}}else this._cleanup=o._render(this,null);a.willEnter.update(e=>e+1);let l=e=>{i._pathToTag(e.detail.to)===t&&(a.didEnter.update(e=>e+1),window.removeEventListener("ionRouteDidChange",l))};window.addEventListener("ionRouteDidChange",l)}disconnectedCallback(){let e=this._lc;setTimeout(()=>{this.isConnected||(f.delete(t),e?.willLeave.update(e=>e+1),e?.didLeave.update(e=>e+1),this._cleanup?.(),this._cleanup=null,this._lc=null,this._isInitialized=!1)},100)}})}}render(){let e=this;return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-router");i.setAttribute("use-hash","false"),i.innerHTML=e.routes.filter(e=>"*"!==e.path).map(t=>`<ion-route url="${t.path}" component="${e._pathToTag(t.path)}"></ion-route>`).join("");let a=document.createElement("ion-nav");return t.insertBefore(i,n),t.insertBefore(a,n),e.ionRouterEl=i,()=>{i.remove(),a.remove()}}}}onUnmount(){f.clear(),l=null}};export{d as IonBackButton,i as IonPage,p as IonRouterOutlet,r as createPageLifecycle,o as useIonViewDidEnter,c as useIonViewDidLeave,a as useIonViewWillEnter,s as useIonViewWillLeave,u as useRouter};
1
+ import{NixComponent as e,signal as t,watch as n}from"@deijose/nix-js";function r(){return{willEnter:t(0),didEnter:t(0),willLeave:t(0),didLeave:t(0)}}var i=class extends e{__lc;constructor(e){super(),this.__lc=e}onInit(){let e=this.__lc;this.ionViewWillEnter&&n(e.willEnter,this.ionViewWillEnter.bind(this)),this.ionViewDidEnter&&n(e.didEnter,this.ionViewDidEnter.bind(this)),this.ionViewWillLeave&&n(e.willLeave,this.ionViewWillLeave.bind(this)),this.ionViewDidLeave&&n(e.didLeave,this.ionViewDidLeave.bind(this))}};function a(e,t){n(e.willEnter,t)}function o(e,t){n(e.didEnter,t)}function s(e,t){n(e.willLeave,t)}function c(e,t){n(e.didLeave,t)}function l(){let e=document.querySelector("ion-router");return{navigate:t=>{e?.push(t,"forward")},replace:t=>{e?.push(t,"root")},back:()=>{e?.back()}}}function u(e){return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-back-button");return e&&i.setAttribute("default-href",e),t.insertBefore(i,n),()=>i.remove()}}}var d=class extends e{routes;constructor(e){super(),this.routes=e,this._registerCustomElements()}_extractParams(e,t){let n={},i=e.split("/"),r=t.split("/");for(let e=0;e<i.length;e++)i[e]?.startsWith(":")&&(n[i[e].slice(1)]=r[e]??"");return n}_pathToTag(e){return e&&"/"!==e?`nix-page-${e.replace(/\/:?[^/]+/g,e=>"-"+e.replace(/\//g,"").replace(/:/g,"")).replace(/^\//,"").replace(/\//g,"-")}`:"nix-page-home"}_registerCustomElements(){let e=this;for(let t of this.routes){if("*"===t.path)continue;let n=this._pathToTag(t.path);customElements.get(n)||customElements.define(n,class extends HTMLElement{_handle=null;connectedCallback(){if(this._handle)return;this.classList.add("ion-page");let n=r();this.addEventListener("ionViewWillEnter",()=>n.willEnter.update(e=>e+1)),this.addEventListener("ionViewDidEnter",()=>n.didEnter.update(e=>e+1)),this.addEventListener("ionViewWillLeave",()=>n.willLeave.update(e=>e+1)),this.addEventListener("ionViewDidLeave",()=>n.didLeave.update(e=>e+1));let i=e._extractParams(t.path,location.pathname),o=t.component({lc:n,params:i});if("render"in o&&"function"==typeof o.render){let e=o;e.onInit?.();let t=e.render()._render(this,null),n=e.onMount?.();this._handle={unmount:()=>{e.onUnmount?.(),"function"==typeof n&&n(),t()}}}else this._handle={unmount:o._render(this,null)}}disconnectedCallback(){this._handle?.unmount(),this._handle=null}})}}render(){let e=this;return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-router");i.setAttribute("use-hash","false"),i.innerHTML=e.routes.filter(e=>"*"!==e.path).map(t=>`<ion-route url="${t.path}" component="${e._pathToTag(t.path)}"></ion-route>`).join("");let r=document.createElement("ion-router-outlet");return t.insertBefore(i,n),t.insertBefore(r,n),()=>{i.remove(),r.remove()}}}}};export{u as IonBackButton,i as IonPage,d as IonRouterOutlet,r as createPageLifecycle,o as useIonViewDidEnter,c as useIonViewDidLeave,a as useIonViewWillEnter,s as useIonViewWillLeave,l as useRouter};
@@ -1 +1 @@
1
- {"version":3,"file":"nix-ionic.js","names":[],"sources":["../../src/lifecycle.ts","../../src/IonRouterOutlet.ts"],"sourcesContent":["/**\n * ionic-nix/lifecycle.ts\n *\n * Sistema de ciclo de vida de navegación, análogo a los hooks de Ionic:\n * ionViewWillEnter / ionViewDidEnter / ionViewWillLeave / ionViewDidLeave\n *\n * Cómo funciona (sin provide/inject):\n * 1. IonRouterOutlet crea un `PageLifecycle` por cada ruta.\n * 2. Lo pasa directamente al factory de la ruta como argumento.\n * 3. El factory llama a `new MiPagina(lc)` o `MiPagina(lc)`.\n * 4. IonPage/composables registran watchers sobre las señales del lc.\n * 5. Cuando el router navega, incrementa las señales → watchers se disparan.\n */\n\nimport { signal, watch } from \"@deijose/nix-js\";\nimport type { Signal } from \"@deijose/nix-js\";\nimport { NixComponent } from \"@deijose/nix-js\";\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageLifecycle {\n willEnter: Signal<number>;\n didEnter: Signal<number>;\n willLeave: Signal<number>;\n didLeave: Signal<number>;\n}\n\n/** Crea un nuevo PageLifecycle con señales en 0. */\nexport function createPageLifecycle(): PageLifecycle {\n return {\n willEnter: signal(0),\n didEnter: signal(0),\n willLeave: signal(0),\n didLeave: signal(0),\n };\n}\n\n// --------------------------------------------------------------------------\n// IonPage — clase base para páginas con hooks de navegación\n// --------------------------------------------------------------------------\n//\n// Uso:\n// class HomePage extends IonPage {\n// constructor(lc: PageLifecycle) { super(lc); }\n//\n// ionViewWillEnter() { /* fetch de datos frescos */ }\n// render() { return html`...`; }\n// }\n\nexport abstract class IonPage extends NixComponent {\n private __lc: PageLifecycle;\n\n constructor(lc: PageLifecycle) {\n super();\n this.__lc = lc;\n }\n\n override onInit(): void {\n const lc = this.__lc;\n // watch no corre en init (immediate: false), así que ionViewWillEnter\n // solo se llama cuando el outlet incrementa la señal, no al construir.\n if (this.ionViewWillEnter) watch(lc.willEnter, this.ionViewWillEnter.bind(this));\n if (this.ionViewDidEnter) watch(lc.didEnter, this.ionViewDidEnter.bind(this));\n if (this.ionViewWillLeave) watch(lc.willLeave, this.ionViewWillLeave.bind(this));\n if (this.ionViewDidLeave) watch(lc.didLeave, this.ionViewDidLeave.bind(this));\n }\n\n ionViewWillEnter?(): void;\n ionViewDidEnter?(): void;\n ionViewWillLeave?(): void;\n ionViewDidLeave?(): void;\n}\n\n// --------------------------------------------------------------------------\n// Composables para function components\n// --------------------------------------------------------------------------\n//\n// Uso:\n// function ProfilePage(lc: PageLifecycle): NixTemplate {\n// useIonViewWillEnter(lc, () => { /* fetch */ });\n// return html`...`;\n// }\n\nexport function useIonViewWillEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willEnter, fn);\n}\n\nexport function useIonViewDidEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didEnter, fn);\n}\n\nexport function useIonViewWillLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willLeave, fn);\n}\n\nexport function useIonViewDidLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didLeave, fn);\n}","/**\n * ionic-nix/IonRouterOutlet.ts\n *\n * Bridge entre Nix.js e ion-router (API pública oficial para vanilla JS).\n *\n * Arquitectura:\n * 1. Por cada ruta registramos un Custom Element (nix-page-home, etc.)\n * 2. ion-router activa el custom element según la URL\n * 3. ion-router-outlet gestiona: caché, animaciones, back button, swipe back\n * 4. connectedCallback del custom element monta el componente Nix adentro\n * 5. El flag _mounted evita destruir el componente cuando ion-router-outlet\n * desconecta el elemento temporalmente (caché). Solo se destruye si\n * el elemento no vuelve a conectarse en los siguientes 100ms.\n */\n\nimport { NixComponent, signal } from \"@deijose/nix-js\";\nimport type { NixTemplate, Signal } from \"@deijose/nix-js\";\nimport { createPageLifecycle, type PageLifecycle } from \"./lifecycle\";\n\n// --------------------------------------------------------------------------\n// Router store singleton\n// --------------------------------------------------------------------------\n\ninterface RouterStore {\n navigate: (path: string) => void;\n replace: (path: string) => void;\n back: () => void;\n canGoBack: Signal<boolean>;\n params: Signal<Record<string, string>>;\n path: Signal<string>;\n}\n\nlet _router: RouterStore | null = null;\n\nexport function useRouter(): RouterStore {\n if (!_router) throw new Error(\"[nix-ionic] useRouter() llamado antes de montar IonRouterOutlet\");\n return _router;\n}\n\n// --------------------------------------------------------------------------\n// IonBackButton\n// --------------------------------------------------------------------------\n\nexport function IonBackButton(defaultHref?: string): NixTemplate {\n return {\n __isNixTemplate: true as const,\n mount(container) {\n const el = typeof container === \"string\"\n ? document.querySelector(container)! as Element\n : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n const btn = document.createElement(\"ion-back-button\") as HTMLElement;\n \n // Solo asigna si existe. Si no, Ionic lo oculta de forma inteligente.\n if (defaultHref) {\n btn.setAttribute(\"default-href\", defaultHref);\n }\n\n parent.insertBefore(btn, before);\n return () => btn.remove();\n },\n };\n}\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageContext {\n lc: PageLifecycle;\n params: Record<string, string>;\n}\n\nexport interface RouteDefinition {\n path: string;\n component: (ctx: PageContext) => NixComponent | NixTemplate;\n beforeEnter?: (to: string, from: string) => boolean | string | void;\n}\n\n// --------------------------------------------------------------------------\n// Mapa global de lifecycles activos por tag\n// --------------------------------------------------------------------------\n\nconst _lifecycleRegistry = new Map<string, PageLifecycle>();\n\n// --------------------------------------------------------------------------\n// IonRouterOutlet\n// --------------------------------------------------------------------------\n\nexport class IonRouterOutlet extends NixComponent {\n private routes: RouteDefinition[];\n private ionRouterEl: HTMLElement | null = null;\n\n private _currentPath = signal(location.pathname);\n private _canGoBack = signal(false);\n private _params = signal<Record<string, string>>({});\n\n constructor(routes: RouteDefinition[]) {\n super();\n this.routes = routes;\n\n _router = {\n navigate: (path) => this.navigate(path),\n replace: (path) => this.replace(path),\n back: () => this.back(),\n canGoBack: this._canGoBack,\n params: this._params,\n path: this._currentPath,\n };\n\n this._registerCustomElements();\n }\n\n override onInit(): void {\n window.addEventListener(\"popstate\", this._handlePopState);\n }\n\n override onMount(): () => void {\n this.ionRouterEl = document.querySelector(\"ion-router\");\n\n const onWillChange = (ev: Event) => {\n const { to, from } = (ev as CustomEvent).detail as { to: string; from: string };\n this._handleWillChange(to, from ?? \"\");\n };\n const onDidChange = (ev: Event) => {\n const { to } = (ev as CustomEvent).detail as { to: string };\n this._handleDidChange(to);\n };\n const onIonBack = (ev: Event) => {\n (ev as CustomEvent).detail.register(100, (next: () => void) => {\n if (this._canGoBack.value) this.back(); else next();\n });\n };\n\n window.addEventListener(\"ionRouteWillChange\", onWillChange);\n window.addEventListener(\"ionRouteDidChange\", onDidChange);\n document.addEventListener(\"ionBackButton\", onIonBack);\n\n return () => {\n window.removeEventListener(\"popstate\", this._handlePopState);\n window.removeEventListener(\"ionRouteWillChange\", onWillChange);\n window.removeEventListener(\"ionRouteDidChange\", onDidChange);\n document.removeEventListener(\"ionBackButton\", onIonBack);\n _router = null;\n };\n }\n\n // -------------------------------------------------------------------------\n // API pública\n // -------------------------------------------------------------------------\n\n navigate(path: string): void {\n if (path === location.pathname) return;\n (this.ionRouterEl as any)?.push(path, \"forward\");\n }\n\n replace(path: string): void {\n (this.ionRouterEl as any)?.push(path, \"root\");\n }\n\n back(): void {\n (this.ionRouterEl as any)?.back();\n }\n\n // -------------------------------------------------------------------------\n // Handlers privados\n // -------------------------------------------------------------------------\n\n private _handlePopState = (): void => {\n this._handleWillChange(location.pathname, this._currentPath.peek());\n };\n\n private _handleWillChange(to: string, from: string): void {\n const route = this._matchRoute(to);\n if (!route) return;\n\n if (route.beforeEnter) {\n const result = route.beforeEnter(to, from);\n if (result === false) { this.back(); return; }\n if (typeof result === \"string\") { this.navigate(result); return; }\n }\n\n this._currentPath.value = to;\n this._params.value = this._extractParams(route.path, to);\n\n // Despide a la vista que sale\n _lifecycleRegistry.get(this._pathToTag(from))?.willLeave.update((n) => n + 1);\n\n // Si la vista a la que vamos ya existe en el registro, significa que ESTÁ EN CACHÉ.\n const toTag = this._pathToTag(to);\n const toLc = _lifecycleRegistry.get(toTag);\n if (toLc) {\n toLc.willEnter.update((n) => n + 1);\n (toLc as any)._pendingEnter = true; // Flag para que el didChange dispare el didEnter\n }\n }\n\n private _handleDidChange(to: string): void {\n const nav = document.querySelector(\"ion-nav\") as any;\n \n // ion-nav.canGoBack() retorna una Promesa, la resolvemos para tu señal\n if (nav && typeof nav.canGoBack === \"function\") {\n nav.canGoBack().then((res: boolean) => {\n this._canGoBack.value = res;\n });\n } else {\n this._canGoBack.value = false;\n }\n\n _lifecycleRegistry.forEach((lc, tag) => {\n if (tag !== this._pathToTag(to)) lc.didLeave.update((n) => n + 1);\n });\n\n // Avisa a la vista cacheada que la transición ya terminó\n const toTag = this._pathToTag(to);\n const toLc = _lifecycleRegistry.get(toTag);\n if (toLc && (toLc as any)._pendingEnter) {\n toLc.didEnter.update((n) => n + 1);\n (toLc as any)._pendingEnter = false;\n }\n }\n\n // -------------------------------------------------------------------------\n // Helpers\n // -------------------------------------------------------------------------\n\n private _matchRoute(path: string): RouteDefinition | undefined {\n const exact = this.routes.find((r) => r.path === path);\n if (exact) return exact;\n for (const route of this.routes) {\n const re = new RegExp(`^${route.path.replace(/:[^/]+/g, \"([^/]+)\")}$`);\n if (re.test(path)) return route;\n }\n return this.routes.find((r) => r.path === \"*\");\n }\n\n private _extractParams(routePath: string, realPath: string): Record<string, string> {\n const params: Record<string, string> = {};\n const rP = routePath.split(\"/\");\n const uP = realPath.split(\"/\");\n for (let i = 0; i < rP.length; i++) {\n if (rP[i].startsWith(\":\")) params[rP[i].slice(1)] = uP[i] ?? \"\";\n }\n return params;\n }\n\n private _pathToTag(path: string): string {\n if (!path || path === \"/\") return \"nix-page-home\";\n const clean = path\n .replace(/\\/:?[^/]+/g, (m) => \"-\" + m.replace(/\\//g, \"\").replace(/:/g, \"\"))\n .replace(/^\\//, \"\")\n .replace(/\\//g, \"-\");\n return `nix-page-${clean}`;\n }\n\n // -------------------------------------------------------------------------\n // Registro de Custom Elements\n // -------------------------------------------------------------------------\n\n private _registerCustomElements(): void {\n for (const route of this.routes) {\n if (route.path === \"*\") continue;\n\n const tag = this._pathToTag(route.path);\n const routeDef = route;\n const self = this;\n\n if (customElements.get(tag)) continue;\n\n customElements.define(tag, class extends HTMLElement {\n private _cleanup: (() => void) | null = null;\n private _lc: PageLifecycle | null = null;\n private _isInitialized = false; // Solo necesitamos este flag\n\n connectedCallback(): void {\n if (this._isInitialized) return;\n this._isInitialized = true;\n\n const params = self._extractParams(routeDef.path, location.pathname);\n const lc = createPageLifecycle();\n this._lc = lc;\n\n _lifecycleRegistry.set(tag, lc);\n this.classList.add(\"ion-page\");\n\n const pageNode = routeDef.component({ lc, params });\n\n if (\"render\" in pageNode && typeof (pageNode as NixComponent).render === \"function\") {\n const comp = pageNode as NixComponent;\n comp.onInit?.();\n const renderCleanup = comp.render()._render(this, null);\n const mountRet = comp.onMount?.();\n this._cleanup = () => {\n comp.onUnmount?.();\n if (typeof mountRet === \"function\") mountRet();\n renderCleanup();\n };\n } else {\n this._cleanup = (pageNode as NixTemplate)._render(this, null);\n }\n\n // Disparar hooks de la primera vez\n lc.willEnter.update((n) => n + 1);\n \n // Esperar al cambio de ruta para disparar didEnter de forma segura\n const onReady = (e: Event) => {\n if (self._pathToTag((e as CustomEvent).detail.to) === tag) {\n lc.didEnter.update((n) => n + 1);\n window.removeEventListener(\"ionRouteDidChange\", onReady);\n }\n };\n window.addEventListener(\"ionRouteDidChange\", onReady);\n }\n\n disconnectedCallback(): void {\n const lc = this._lc;\n \n setTimeout(() => {\n // Si después de 100ms sigue conectado, fue solo una animación de Ionic\n if (this.isConnected) return; \n\n // Destrucción real (se sacó del stack)\n _lifecycleRegistry.delete(tag);\n lc?.willLeave.update((n) => n + 1);\n lc?.didLeave.update((n) => n + 1);\n this._cleanup?.();\n this._cleanup = null;\n this._lc = null;\n this._isInitialized = false; // Permite que se recree en el futuro\n }, 100);\n }\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Render\n // -------------------------------------------------------------------------\n\n override render(): NixTemplate {\n const self = this;\n return {\n __isNixTemplate: true as const,\n mount(container: Element | string): { unmount(): void } {\n const el = typeof container === \"string\"\n ? (document.querySelector(container) as Element)\n : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n const routerEl = document.createElement(\"ion-router\");\n routerEl.setAttribute(\"use-hash\", \"false\");\n routerEl.innerHTML = self.routes\n .filter((r) => r.path !== \"*\")\n .map((r) => `<ion-route url=\"${r.path}\" component=\"${self._pathToTag(r.path)}\"></ion-route>`)\n .join(\"\");\n\n // 🚀 EL FIX MÁGICO: ion-nav mantiene los nodos en el DOM nativamente\n const navEl = document.createElement(\"ion-nav\");\n\n parent.insertBefore(routerEl, before);\n parent.insertBefore(navEl, before);\n\n self.ionRouterEl = routerEl;\n\n return () => { routerEl.remove(); navEl.remove(); };\n },\n };\n }\n\n override onUnmount(): void {\n _lifecycleRegistry.clear();\n _router = null;\n }\n}"],"mappings":"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"}
1
+ {"version":3,"file":"nix-ionic.js","names":[],"sources":["../../src/lifecycle.ts","../../src/IonRouterOutlet.ts"],"sourcesContent":["/**\n * ionic-nix/lifecycle.ts\n *\n * Sistema de ciclo de vida de navegación, análogo a los hooks de Ionic:\n * ionViewWillEnter / ionViewDidEnter / ionViewWillLeave / ionViewDidLeave\n *\n * Cómo funciona (sin provide/inject):\n * 1. IonRouterOutlet crea un `PageLifecycle` por cada ruta.\n * 2. Lo pasa directamente al factory de la ruta como argumento.\n * 3. El factory llama a `new MiPagina(lc)` o `MiPagina(lc)`.\n * 4. IonPage/composables registran watchers sobre las señales del lc.\n * 5. Cuando el router navega, incrementa las señales → watchers se disparan.\n */\n\nimport { signal, watch } from \"@deijose/nix-js\";\nimport type { Signal } from \"@deijose/nix-js\";\nimport { NixComponent } from \"@deijose/nix-js\";\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageLifecycle {\n willEnter: Signal<number>;\n didEnter: Signal<number>;\n willLeave: Signal<number>;\n didLeave: Signal<number>;\n}\n\n/** Crea un nuevo PageLifecycle con señales en 0. */\nexport function createPageLifecycle(): PageLifecycle {\n return {\n willEnter: signal(0),\n didEnter: signal(0),\n willLeave: signal(0),\n didLeave: signal(0),\n };\n}\n\n// --------------------------------------------------------------------------\n// IonPage — clase base para páginas con hooks de navegación\n// --------------------------------------------------------------------------\n//\n// Uso:\n// class HomePage extends IonPage {\n// constructor(lc: PageLifecycle) { super(lc); }\n//\n// ionViewWillEnter() { /* fetch de datos frescos */ }\n// render() { return html`...`; }\n// }\n\nexport abstract class IonPage extends NixComponent {\n private __lc: PageLifecycle;\n\n constructor(lc: PageLifecycle) {\n super();\n this.__lc = lc;\n }\n\n override onInit(): void {\n const lc = this.__lc;\n // watch no corre en init (immediate: false), así que ionViewWillEnter\n // solo se llama cuando el outlet incrementa la señal, no al construir.\n if (this.ionViewWillEnter) watch(lc.willEnter, this.ionViewWillEnter.bind(this));\n if (this.ionViewDidEnter) watch(lc.didEnter, this.ionViewDidEnter.bind(this));\n if (this.ionViewWillLeave) watch(lc.willLeave, this.ionViewWillLeave.bind(this));\n if (this.ionViewDidLeave) watch(lc.didLeave, this.ionViewDidLeave.bind(this));\n }\n\n ionViewWillEnter?(): void;\n ionViewDidEnter?(): void;\n ionViewWillLeave?(): void;\n ionViewDidLeave?(): void;\n}\n\n// --------------------------------------------------------------------------\n// Composables para function components\n// --------------------------------------------------------------------------\n//\n// Uso:\n// function ProfilePage(lc: PageLifecycle): NixTemplate {\n// useIonViewWillEnter(lc, () => { /* fetch */ });\n// return html`...`;\n// }\n\nexport function useIonViewWillEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willEnter, fn);\n}\n\nexport function useIonViewDidEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didEnter, fn);\n}\n\nexport function useIonViewWillLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willLeave, fn);\n}\n\nexport function useIonViewDidLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didLeave, fn);\n}","/**\n * ionic-nix/IonRouterOutlet.ts\n *\n * Bridge 100% nativo. Genera exactamente el mismo DOM que Vanilla Ionic.\n * Delegamos el view-caching y los ciclos de vida a los Eventos del DOM de ion-router-outlet.\n */\n\nimport { NixComponent } from \"@deijose/nix-js\";\nimport type { NixTemplate } from \"@deijose/nix-js\";\nimport { createPageLifecycle, type PageLifecycle } from \"./lifecycle\";\nimport { Components } from \"@ionic/core\";\n\n// --------------------------------------------------------------------------\n// Router store singleton\n// --------------------------------------------------------------------------\n\nexport function useRouter() {\n const router = <Components.IonRouter>document.querySelector(\"ion-router\");\n return {\n navigate: (path: string) => {\n router?.push(path, \"forward\");\n },\n replace: (path: string) => {\n router?.push(path, \"root\");\n },\n back: () => {\n router?.back();\n }\n };\n}\n\n// --------------------------------------------------------------------------\n// IonBackButton (Sin listeners, 100% nativo)\n// --------------------------------------------------------------------------\n\nexport function IonBackButton(defaultHref?: string): NixTemplate {\n return {\n __isNixTemplate: true as const,\n mount(container) {\n const el = typeof container === \"string\" ? document.querySelector(container)! : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n const btn = document.createElement(\"ion-back-button\") as HTMLElement;\n if (defaultHref) btn.setAttribute(\"default-href\", defaultHref);\n parent.insertBefore(btn, before);\n return () => btn.remove();\n },\n };\n}\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageContext {\n lc: PageLifecycle;\n params: Record<string, string>;\n}\n\nexport interface RouteDefinition {\n path: string;\n component: (ctx: PageContext) => NixComponent | NixTemplate;\n}\n\n// --------------------------------------------------------------------------\n// IonRouterOutlet (El Bridge Maestro)\n// --------------------------------------------------------------------------\n\nexport class IonRouterOutlet extends NixComponent {\n private routes: RouteDefinition[];\n\n constructor(routes: RouteDefinition[]) {\n super();\n this.routes = routes;\n this._registerCustomElements();\n }\n\n // -------------------------------------------------------------------------\n // Helpers\n // -------------------------------------------------------------------------\n\n private _extractParams(routePath: string, realPath: string): Record<string, string> {\n const params: Record<string, string> = {};\n const rP = routePath.split(\"/\");\n const uP = realPath.split(\"/\");\n for (let i = 0; i < rP.length; i++) {\n if (rP[i]?.startsWith(\":\")) params[rP[i].slice(1)] = uP[i] ?? \"\";\n }\n return params;\n }\n\n private _pathToTag(path: string): string {\n if (!path || path === \"/\") return \"nix-page-home\";\n const clean = path\n .replace(/\\/:?[^/]+/g, (m) => \"-\" + m.replace(/\\//g, \"\").replace(/:/g, \"\"))\n .replace(/^\\//, \"\")\n .replace(/\\//g, \"-\");\n return `nix-page-${clean}`;\n }\n\n // -------------------------------------------------------------------------\n // Registro de Custom Elements estilo Vanilla JS\n // -------------------------------------------------------------------------\n\n private _registerCustomElements(): void {\n const self = this;\n for (const route of this.routes) {\n if (route.path === \"*\") continue;\n\n const tag = this._pathToTag(route.path);\n if (customElements.get(tag)) continue;\n\n customElements.define(tag, class extends HTMLElement {\n private _handle: { unmount(): void } | null = null;\n\n connectedCallback(): void {\n // Prevenir doble montaje si Ionic reconecta el nodo\n if (this._handle) return; \n\n this.classList.add(\"ion-page\");\n\n // 1. Escuchar los eventos NATIVOS del DOM que dispara ion-router-outlet\n const lc = createPageLifecycle();\n this.addEventListener(\"ionViewWillEnter\", () => lc.willEnter.update((n) => n + 1));\n this.addEventListener(\"ionViewDidEnter\", () => lc.didEnter.update((n) => n + 1));\n this.addEventListener(\"ionViewWillLeave\", () => lc.willLeave.update((n) => n + 1));\n this.addEventListener(\"ionViewDidLeave\", () => lc.didLeave.update((n) => n + 1));\n\n // 2. Extraer parámetros\n const params = self._extractParams(route.path, location.pathname);\n\n // 3. Montar componente Nix\n const pageNode = route.component({ lc, params });\n\n if (\"render\" in pageNode && typeof (pageNode as NixComponent).render === \"function\") {\n const comp = pageNode as NixComponent;\n comp.onInit?.();\n const renderCleanup = comp.render()._render(this, null);\n const mountRet = comp.onMount?.();\n\n this._handle = {\n unmount: () => {\n comp.onUnmount?.();\n if (typeof mountRet === \"function\") mountRet();\n renderCleanup();\n }\n };\n } else {\n const renderCleanup = (pageNode as NixTemplate)._render(this, null);\n this._handle = { unmount: renderCleanup };\n }\n }\n\n disconnectedCallback(): void {\n // ion-router-outlet SOLO desconecta el elemento cuando lo saca \n // definitivamente de la caché (ej. al hacer click en el back-button)\n this._handle?.unmount();\n this._handle = null;\n }\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Render exacto al HTML que te funcionaba\n // -------------------------------------------------------------------------\n\n override render(): NixTemplate {\n const self = this;\n return {\n __isNixTemplate: true as const,\n mount(container: Element | string) {\n const el = typeof container === \"string\" ? document.querySelector(container)! : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n \n // Creamos <ion-router>\n const routerEl = document.createElement(\"ion-router\");\n routerEl.setAttribute(\"use-hash\", \"false\");\n routerEl.innerHTML = self.routes\n .filter((r) => r.path !== \"*\")\n .map((r) => `<ion-route url=\"${r.path}\" component=\"${self._pathToTag(r.path)}\"></ion-route>`)\n .join(\"\");\n\n // Creamos <ion-router-outlet> real\n const outletEl = document.createElement(\"ion-router-outlet\");\n\n parent.insertBefore(routerEl, before);\n parent.insertBefore(outletEl, before);\n\n return () => {\n routerEl.remove();\n outletEl.remove();\n };\n },\n };\n }\n}"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deijose/nix-ionic",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Ionic lifecycle & router bridge for Nix.js",
5
5
  "license": "MIT",
6
6
  "author": "Deiver Vasquez",
@@ -65,11 +65,14 @@
65
65
  "@deijose/nix-js": ">=1.7.7"
66
66
  },
67
67
  "devDependencies": {
68
- "@deijose/nix-js": "^1.7.7",
69
68
  "typescript": "~5.9.3",
70
69
  "vite": "^8.0.0",
71
70
  "vite-plugin-dts": "^4.5.4",
72
71
  "happy-dom": "^20.8.3",
73
72
  "terser": "^5.46.0"
73
+ },
74
+ "dependencies": {
75
+ "@deijose/nix-js": "^1.7.7",
76
+ "@ionic/core": "^8.8.1"
74
77
  }
75
78
  }
@@ -1,59 +0,0 @@
1
- /**
2
- * ionic-nix/IonRouterOutlet.ts
3
- *
4
- * Bridge entre Nix.js e ion-router (API pública oficial para vanilla JS).
5
- *
6
- * Arquitectura:
7
- * 1. Por cada ruta registramos un Custom Element (nix-page-home, etc.)
8
- * 2. ion-router activa el custom element según la URL
9
- * 3. ion-router-outlet gestiona: caché, animaciones, back button, swipe back
10
- * 4. connectedCallback del custom element monta el componente Nix adentro
11
- * 5. El flag _mounted evita destruir el componente cuando ion-router-outlet
12
- * desconecta el elemento temporalmente (caché). Solo se destruye si
13
- * el elemento no vuelve a conectarse en los siguientes 100ms.
14
- */
15
- import { NixComponent } from "@deijose/nix-js";
16
- import type { NixTemplate, Signal } from "@deijose/nix-js";
17
- import { type PageLifecycle } from "./lifecycle";
18
- interface RouterStore {
19
- navigate: (path: string) => void;
20
- replace: (path: string) => void;
21
- back: () => void;
22
- canGoBack: Signal<boolean>;
23
- params: Signal<Record<string, string>>;
24
- path: Signal<string>;
25
- }
26
- export declare function useRouter(): RouterStore;
27
- export declare function IonBackButton(defaultHref?: string): NixTemplate;
28
- export interface PageContext {
29
- lc: PageLifecycle;
30
- params: Record<string, string>;
31
- }
32
- export interface RouteDefinition {
33
- path: string;
34
- component: (ctx: PageContext) => NixComponent | NixTemplate;
35
- beforeEnter?: (to: string, from: string) => boolean | string | void;
36
- }
37
- export declare class IonRouterOutlet extends NixComponent {
38
- private routes;
39
- private ionRouterEl;
40
- private _currentPath;
41
- private _canGoBack;
42
- private _params;
43
- constructor(routes: RouteDefinition[]);
44
- onInit(): void;
45
- onMount(): () => void;
46
- navigate(path: string): void;
47
- replace(path: string): void;
48
- back(): void;
49
- private _handlePopState;
50
- private _handleWillChange;
51
- private _handleDidChange;
52
- private _matchRoute;
53
- private _extractParams;
54
- private _pathToTag;
55
- private _registerCustomElements;
56
- render(): NixTemplate;
57
- onUnmount(): void;
58
- }
59
- export {};
File without changes
File without changes