UNPKG

@shaun/alpinejs-router

Version:

A lightweight client-side router for Alpine.js that adds declarative routes, link navigation, dynamic params, external templates, and hash or HTML5 history modes without turning your app into a larger framework.

1 lines • 7.93 kB
(function(){var e=class{#e;#t;constructor(e,t={}){this.#e=new URL(e),this.mode=t.mode??`web`,this.base=t.base??``}set url(e){this.#e=new URL(e),this.#t=void 0}get url(){return this.#e.href}get path(){return this.#t??=this.#r(),this.#t}#n(){return this.#e.hash.slice(1).split(`#`).shift()}#r(){if(this.mode===`hash`&&this.#e.hash)return this.#n().split(`?`).shift()||`/`;let e=this.#e.pathname,t=this.base&&this.base!==`/`?this.base.replace(/\/$/,``):``;return t?e===t?`/`:e.startsWith(t+`/`)?e.slice(t.length)||`/`:e:e}get query(){let e=this.mode===`hash`&&this.#e.hash?this.#n().split(`?`).slice(1).join(`?`):this.#e.search;return Object.fromEntries(new URLSearchParams(e))}resolve(e,t,n=!1){let r=this.#e.origin+this.base+(this.mode===`hash`?`#`:``)+e,i=n?new URLSearchParams(t).toString():new URLSearchParams({...this.query,...t}).toString();return this.url=r+(i?`?`+i:``),this}static resolveTemplatePath(e,t){if(!t||t.startsWith(`/`))return t;let n=e.slice(0,e.lastIndexOf(`/`));if(t.startsWith(`./`))return n+t.slice(1);if(t.startsWith(`../`)){let e=/\.\.\//g,r=t.match(e).length,i=n.split(`/`);for(;r--;)i.pop();return i.length===0&&(i=[``]),[...i,t.replace(e,``)].join(`/`)}return t}},t=class{static build(e){if(e.indexOf(`:`)===-1)return e;let t=new Set,n=e.split(`/`).map(n=>{if(!n.startsWith(`:`))return n;let r=n.substr(1),i=`[^/]+`,a=r.indexOf(`(`);if(a>-1){if(!r.endsWith(`)`))throw Error(`Malformed route parameter '${n}' in route '${e}'`);i=r.slice(a+1,-1),r=r.slice(0,a)}if(!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(r))throw Error(`Invalid route parameter name ':${r}' in route '${e}'`);if(t.has(r))throw Error(`Duplicate route parameter name ':${r}' in route '${e}'`);return t.add(r),`(?<${r}>${i})`}).join(`/`);try{return RegExp(`^${n}$`)}catch(t){throw Error(`Invalid route pattern '${e}': ${t.message}`)}}static match(e,t){if(t instanceof RegExp){let n=e.match(t);return n?{...n.groups}:!1}return e===t}static is(e,t){return t instanceof RegExp?t.test(e):e===t}},n=class n{#e={};#t=new Set;#n={};#r={};add(e){let n=this.#r[e]??t.build(e);this.#r[e]=n,this.#e[e]=n,this.#i(e,n)}match(n){console.assert(n instanceof e);let r=n.path;if(this.#t.has(r))return{};for(let e of this.#a(r))for(let[,n]of e){let e=t.match(r,n);if(e)return e===!0?{}:e}return!1}is(n,...r){console.assert(n instanceof e);let i=n.path;for(let e of r){let n=this.#e[e]??this.#r[e]??t.build(e);if(this.#r[e]=n,t.is(i,n))return!0}return!1}not(n,...r){console.assert(n instanceof e);let i=n.path;for(let e of r){let n=this.#e[e]??this.#r[e]??t.build(e);if(this.#r[e]=n,t.is(i,n))return!1}return!0}notfound(n){console.assert(n instanceof e);let r=n.path;if(this.#t.has(r))return!1;for(let e of this.#a(r))for(let[,n]of e)if(t.is(r,n))return!1;return!0}#i(e,t){if(!(t instanceof RegExp)){this.#t.add(e);return}let r=n.#c(e),i=r.length,a=this.#n[i]??{static:new Map,wildcard:[]},o=r[0],s=o.startsWith(`:`)?a.wildcard:a.static.get(o)??[],c=[e,t,n.#s(e)],l=s.findIndex(([t])=>t===e);l>-1&&s.splice(l,1);let u=s.findIndex(e=>n.#o(c,e)<0);u===-1&&(u=s.length),s.splice(u,0,c),o.startsWith(`:`)||a.static.set(o,s),this.#n[i]=a}#a(e){let t=n.#c(e),r=this.#n[t.length];if(!r)return[];let i=[],a=t[0],o=r.static.get(a);return o&&i.push(o),r.wildcard.length&&i.push(r.wildcard),i}static#o([e,,t],[n,,r]){let i=Math.max(t.length,r.length);for(let e=0;e<i;e+=1){let n=(r[e]??0)-(t[e]??0);if(n!==0)return n}return n.length-e.length}static#s(e){return e.split(`/`).filter(Boolean).map(e=>e.startsWith(`:`)?e.includes(`(`)?2:1:3)}static#c(e){return e.split(`/`).filter(Boolean)}};function r(e,t){return e===`/`?t===`/`:t===e||t.startsWith(e+`/`)}function i(e,t,n=location.origin){let r;try{r=new URL(t.href,n)}catch{return!1}return!(e.defaultPrevented||e.button!==0||e.metaKey||e.altKey||e.ctrlKey||e.shiftKey||t.target&&t.target!==`_self`||t.hasAttribute(`download`)||r.origin!==n)}function a({hasInlineTemplate:e,templateUrl:t,hasCachedTemplate:n,preloadFailed:r}){return t?n?`external`:r&&e?`inline`:`load`:e?`inline`:`missing`}function o(e,t,n=console.error){let r=e.content.firstElementChild;return r?r.nextElementSibling?(n(`Route '${t}' must render exactly one root element`),null):r:null}function s(t){let s=new n,c=0,l=t.reactive({mode:`web`,base:``,href:location.href,path:``,query:{},params:{},loading:!1}),u={get path(){return l.path},get query(){return l.query},get params(){return l.params||{}},get loading(){return l.loading},config(e={}){let t=e.mode!==`hash`&&e.base&&e.base.endsWith(`/`)?e.base.slice(0,-1):e.base;l.mode=e.mode??`web`,l.base=t??``},push(...e){return g(...e)},replace(...e){return _(...e)},resolve(e={}){return m(l.href).resolve(l.path,e).url},is(...e){return s.is(p(l.href),...e)},not(...e){return s.not(p(l.href),...e)},get notfound(){return s.notfound(p(l.href))}};t.$router=u,t.magic(`router`,()=>u);let d,f;function p(e){let t=`${l.mode}|${l.base}|${e}`;return t!==d&&(d=t,f=m(e)),f}function m(t){return new e(t,{mode:l.mode,base:l.base})}function h(){let e=p(l.href);l.path=e.path,l.query=e.query,l.params=s.match(e)}t.effect(()=>h()),t.nextTick(()=>{l.mode===`web`&&!l.base&&h()}),window.addEventListener(`popstate`,()=>l.href=location.href),window.addEventListener(`hashchange`,()=>l.href=location.href);function g(e,t={}){let n=null;try{n=new URL(e)}catch{n=null}if(n){if(n.origin!==location.origin){console.error(`Cannot push cross-origin URL '${e}'`);return}}else e=l.mode===`hash`?location.origin+(l.base||`/`)+`#`+e:location.origin+l.base+e;location.href!==e&&(history[t.replace?`replaceState`:`pushState`]({},``,e),l.href=e)}function _(e){g(e,{replace:!0})}function v(e){return c+=1,l.loading=!0,e.finally(()=>{c=Math.max(c-1,0),l.loading=c>0})}function y(e,t,n){console.error(`Failed to load template '${t}' for route '${e}'`,n)}let b=Object.create(null),x=Object.create(null),S=new WeakSet;t.directive(`route`,(n,{modifiers:r,expression:i},{effect:c,cleanup:u})=>{let d=!1;if(!r.includes(`notfound`))try{s.add(i)}catch(e){console.error(e.message),d=!0}let f=e=>(x[e]||(x[e]=fetch(e).then(e=>{if(!e.ok)throw Error(`Failed to fetch template: ${e.status} ${e.statusText}`);return e.text()}).then(t=>(b[e]=t,t)).finally(()=>delete x[e])),x[e].then(e=>(n.innerHTML=e,e))),m=e.resolveTemplatePath(location.pathname,n.getAttribute(`template`)??n.getAttribute(`template.preload`)),h,g=!1;n.hasAttribute(`template.preload`)&&(h=v(f(m)).then(()=>!0).catch(e=>(g=!0,y(i,m,e),!1)).finally(()=>{h=!1}));function _(){if(n._x_currentIfEl)return n._x_currentIfEl;let e=()=>{if(S.has(n))return;let e=o(n,i);if(!e)return;S.add(n);let r=e.cloneNode(!0);t.addScopeToNode(r,{},n),t.mutateDom(()=>{n.after(r),t.initTree(r)}),n._x_currentIfEl=r,n._x_undoIf=()=>{r.remove(),delete n._x_currentIfEl},t.nextTick(()=>S.delete(n))},r=()=>n.content.firstElementChild?(e(),!0):!1,s=a({hasInlineTemplate:!!n.content.firstElementChild,templateUrl:m,hasCachedTemplate:m in b,preloadFailed:g});s===`inline`?r():s===`external`?m in b&&(n.innerHTML=b[m],e()):s===`load`?h?h.then(t=>t?e():r()):v(f(m)).then(()=>e()).catch(e=>{y(i,m,e),r()||console.error(`Template for '${i}' is missing`)}):console.error(`Template for '${i}' is missing`)}function C(){n._x_undoIf&&(n._x_undoIf(),delete n._x_undoIf)}t.nextTick(()=>{c(()=>{if(d){C();return}let e=p(l.href);(r.includes(`notfound`)?s.notfound(e):s.is(e,i))?_():C()})}),u(()=>n._x_undoIf&&n._x_undoIf())}),t.directive(`link`,(e,{modifiers:t,expression:n},{evaluate:a,effect:o,cleanup:s})=>{let c=m(e.href);e.href=c.resolve(c.path,c.query,!0).url;function u(n){i(n,e,location.origin)&&(n.preventDefault(),g(e.href,{replace:t.includes(`replace`)}))}if(e.addEventListener(`click`,u),t.includes(`activity`)){let t=n?a(n):{};t.active??=`active`,t.exactActive??=`exact-active`;let i=c.path;o(()=>{let n=p(l.href).path;e.classList.toggle(t.active,r(i,n)),e.classList.toggle(t.exactActive,i===n)})}s(()=>{e.removeEventListener(`click`,u)})})}document.addEventListener(`alpine:init`,()=>{window.Alpine.plugin(s)})})();