Easy to use and flexible router for Alpine.js

Overview

alpinejs-router

NPM GitHub package.json version

Easy to use and flexible router for Alpine.js

Installation

npm

npm install @shaun/alpinejs-router

yarn

yarn add @shaun/alpinejs-router

cdn

<script src="https://unpkg.com/@shaun/[email protected]/dist/cdn.min.js" defer></script>

Getting Started

<a x-link href="/hello/world">Hello World</a>

<a x-link href="/somewhere">Load template</a>

<template x-route="/hello/:name">
  <!-- Inner template -->
  <div>Say hello to <span x-text="$router.params.name"></span></div>
</template>

<!-- Separate template file -->
<template x-route="/somewhere" template="/somewhere.html"></template>

somewhere.html

<div x-data="{ open: false }">
  <button @click="open = ! open">Toggle Content</button>

  <div x-show="open">Content...</div>
</div>

Dynamic Route Matching with Params

Very often we will need to map routes with the given pattern to the same template. For example we may have a user template which should be rendered for all users but with different user IDs. In @shaun/alpinejs-router we can use a dynamic segment in the path to achieve that, we call that a param:

<!-- dynamic segments start with a colon -->
<template x-route="/users/:id" template="/user.html"></template>

Now URLs like /users/johnny and /users/jolyne will both map to the same route.

A param is denoted by a colon :. When a route is matched, the value of its params will be exposed as $router.params. Therefore, we can render the current user ID by updating user's template to this:

<div>User ID: {$router.params.id}</div>

You can have multiple params in the same route, and they will map to corresponding fields on $router.params. Examples:

pattern matched path $router.params
/users/:username /users/eduardo { username: 'eduardo' }
/users/:username/posts/:postId /users/eduardo/posts/123 { username: 'eduardo', postId: '123' }

In addition to $router.params, the $router magic also exposes other useful information such as $router.query (if there is a query in the URL), $router.path, etc.

Routes' Matching Syntax

Most applications will use static routes like /about and dynamic routes like /users/:userId like we just saw in Dynamic Route Matching, but @shaun/alpinejs-router has much more to offer!

Custom regex in params

When defining a param like :userId, we internally use the following regex ([^/]+) (at least one character that isn't a slash /) to extract params from URLs. This works well unless you need to differentiate two routes based on the param content. Imagine two routes /:orderId and /:productName, both would match the exact same URLs, so we need a way to differentiate them. The easiest way would be to add a static section to the path that differentiates them:

<!-- matches /o/3549 -->
<template x-route="/o/:orderId"></template>
<!-- matches /p/books -->
<template x-route="/p/:productName"></template>

But in some scenarios we don't want to add that static section /o/p. However, orderId is always a number while productName can be anything, so we can specify a custom regex for a param in parentheses:

<!-- /:orderId -> matches only numbers -->
<template x-route="/:orderId(\d+)"></template>
<!-- /:productName -> matches anything else -->
<template x-route="/:productName"></template>

Now, going to /25 will match /:orderId while going to anything else will match /:productName.

Programmatic Navigation

Aside from using <a x-link href="..."> to create anchor tags for declarative navigation, we can do this programmatically using the router's instance methods.

Navigate to a different location

To navigate to a different URL, use $router.push. This method pushes a new entry into the history stack, so when the user clicks the browser back button they will be taken to the previous URL.

This is the method called internally when you click a x-link, so clicking <a x-link href="..."> is the equivalent of calling $router.push(...).

Declarative Programmatic
<a x-link href="..."> $router.push(...)

Replace current location

It acts like $router.push, the only difference is that it navigates without pushing a new history entry, as its name suggests - it replaces the current entry.

Declarative Programmatic
<a x-link.replace href="..."> $router.replace('...')

History Manipulation

You may have noticed that $router.push and $router.replace are counterparts of window.history.pushState and window.history.replaceState, and they do imitate the window.history APIs.

Therefore, if you are already familiar with Browser History APIs, manipulating history will feel familiar when using @shaun/alpinejs-router.

It is worth mentioning that @shaun/alpinejs-router navigation methods (push, replace) work consistently no matter the kind of mode option is passed when configuring the router instance.

Different History modes

The mode option when configuring the router instance allows us to choose among different history modes.

Hash Mode

The hash history mode is configured with 'hash':

<body x-data x-init="$router.config({ mode: 'hash' })"></body>

It uses a hash character (#) before the actual URL that is internally passed. Because this section of the URL is never sent to the server, it doesn't require any special treatment on the server level. It does however have a bad impact in SEO. If that's a concern for you, use the HTML5 history mode.

HTML5 Mode

The HTML5 mode is configured with 'web' and is the recommended mode:

<body x-data x-init="$router.config({ mode: 'web' })"></body>
<!-- Or do nothing by default -->
<body x-data></body>

When using 'web', the URL will look "normal," e.g. https://example.com/user/id. Beautiful!

Here comes a problem, though: Since our app is a single page client side app, without a proper server configuration, the users will get a 404 error if they access https://example.com/user/id directly in their browser. Now that's ugly.

Not to worry: To fix the issue, all you need to do is add a simple catch-all fallback route to your server. If the URL doesn't match any static assets, it should serve the same index.html page that your app lives in. Beautiful, again!

Route directive

Declare routes by creating a template tag with x-route attribute.

<template x-route="/path/to/route">
  <div x-data>
    ...
  </div>
</template>

<template x-route="/path/to/route" template="/path/to/template.html"></template>
<!-- Preload the separate template file -->
<template x-route="/path/to/route" template.preload="/path/to/template.html"></template>

<!-- When declaring a template that is not found, the path parameter does not need to be specified -->
<template x-route.notfound>
  <div>
    Error 404 not found
  </div>
</template>

Link directive

<!-- The same as $router.push -->
<a x-link href="/path/to/route">...</a>

<!-- The same as $router.replace -->
<a x-link.replace href="/path/to/route">...</a>

<!-- Activate the `active` and `exact-active` classes to router links -->
<a x-link.activity">...</a>
<!-- Custom active class and exact active class can be added by setting `active` and `exactActive` props to the `x-link.activity` directive -->
<a x-link.activity="{ active: 'text-blue-500', exactActive: 'text-green-500' }">...</a>

Magic $router

Properties

<!-- String $router.path -->
<span x-text="$router.path"></span>

<!-- Object $router.query -->
<span x-text="$router.query.page"></span>

<!-- Object $router.params -->
<span x-text="$router.params.userId"></span>

<!-- Boolean $router.loading -->
<span x-show="$router.loading">Separate template file is loading</span>

Methods

<!-- Navigate to -->
<button @click="$router.push('/path/to/route')">...</button>
<!-- Replace to -->
<button @click="$router.push('/path/to/route', { replace: true })">...</button>

<!-- Replace to -->
<button @click="$router.replace('/path/to/route')">...</button>

<!-- Add queries to the current URL -->
<a x-link x-bind:href="$router.resolve({ page: 2 })">Page 2/10</a>

<!-- Mode 'web' with prefix -->
<body x-data x-init="$router.config({ base: '/prefix/' })">...</body>
<!-- Mode 'web' with prefix -->
<body x-data x-init="$router.config({ mode: 'web', base: '/prefix/' })">...</body>
<!-- Mode 'hash' with no prefix -->
<body x-data x-init="$router.config({ mode: 'hash' })">...</body>
<!-- Mode 'hash' with prefix -->
<body x-data x-init="$router.config({ mode: 'hash', base: '/prefix/' })">...</body>
<!-- Do nothing by default to mode 'web' with no prefix -->
<body x-data>...</body>

<!-- Check if the route matches the current location -->
<div x-show="$router.is('/path/to/route')">You can see me</div>
<template x-if="$router.is('/path/to/route1', '/path/to/route2', ...)">You can see me</template>

License

Licensed under MIT

Comments
  • `$router.params` set to false when accessing route directly

    `$router.params` set to false when accessing route directly

    Copying the example from README.md, the value of $router.params is set to false when accessing the route /hello/world directly without using the link provided by x-link.

    opened by docweirdo 11
  • Provide API of `$router` in JS

    Provide API of `$router` in JS

    Hey, thanks for your work.

    I am trying to do something in the EventListener of alpine:init depending on the current route. I do not seem to be able to do that, since the magic property $router is not available in JS context.

    Is there any way to expose/export an instance of the route object of index.js so the same functions which are accessible in HTML are accessible in JS?

    opened by docweirdo 6
  • Path attribute of router not properly initialised in v1.2.12

    Path attribute of router not properly initialised in v1.2.12

    Unfortunately, there seems to be an issue with the new version. At least the path property of $router is not properly initialised when visiting a path, and only changes to the right value some time after. This worked in 1.2.9.

    Minimal reproducible example:

    index.html

    <body x-data x-init="fetchData($router)">
        <p>Data is: <span x-text="$store.someContent"></span></p>
        <script src="/app.js"></script>
    </body>
    

    app.js

    document.addEventListener('alpine:init', () => {
      Alpine.store('someContent', "")
    
    })
    
    function fetchData(r) {
      router = Alpine.$router ? Alpine.$router : r;
    
      Alpine.effect(() => console.log("router.path: " + router.path))
    
      if (router.path == '/fetch'){
        Alpine.store('someContent', "Hello World")
      }
    }
    

    Result for v1.2.9 image

    Result for v1.2.12 image

    Thank you for your continuous effort.

    opened by docweirdo 2
  • Catch-all fallback route for Vite [[question]]

    Catch-all fallback route for Vite [[question]]

    Hi Shaunlee, first thanks for this amazing code!

    For the web mode you say:

    Not to worry: To fix the issue, all you need to do is add a simple catch-all fallback route to your server. If the URL doesn't match any static assets, it should serve the same index.html page that your app lives in. Beautiful, again!

    But I don't fully understand this. I'm using Vite (no-vue, just Alpine) and I don't know how to add a catch-all fallback route.

    This is the piece I'm using with no luck. Could you help me with this?

      server: {
        proxy: {
          '/hello/world': {
            target: 'http://localhost:3000/',
            changeOrigin: true,
            secure: false,
            configure: (proxy, _options) => {
              proxy.on('error', (err, _req, _res) => {
                console.log('proxy error', err);
              });
              proxy.on('proxyReq', (proxyReq, req, _res) => {
                console.log('Sending Request to the Target:', req.method, req.url);
              });
              proxy.on('proxyRes', (proxyRes, req, _res) => {
                console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
              });
            }
          }
        }
      }
    
    opened by jgauna 2
  • Example does not work ?

    Example does not work ?

    From your example (using cdn)

     <div  x-data>
            <a x-link href="/hello/world">Hello World</a>
    
            <template x-route="/hello/:name">
                <div>Say hello to <span x-text="$router.params.name"></span></div>
            </template>
    
        </div>
    

    When clicking, browser will just try to go to /hello/world, should there be a prevent.default or something ?

    opened by bakman2 2
Owner
Shaun
Shaun
Hemsida för personer i Sverige som kan och vill erbjuda boende till människor på flykt

Getting Started with Create React App This project was bootstrapped with Create React App. Available Scripts In the project directory, you can run: np

null 4 May 3, 2022
Kurs-repo för kursen Webbserver och Databaser

Webbserver och databaser This repository is meant for CME students to access exercises and codealongs that happen throughout the course. I hope you wi

null 14 Jan 3, 2023
Complete, flexible, extensible and easy to use page transition library for your static web.

We're looking for maintainers! Complete, flexible, extensible and easy to use page transition library for your static web. Here's what's new in v2. Ch

null 3.7k Jan 2, 2023
🛣️ A tiny and fast http request router designed for use with deno and deno deploy

Rutt Rutt is a tiny http router designed for use with deno and deno deploy. It is written in about 200 lines of code and is pretty fast, using an exte

Denosaurs 26 Dec 10, 2022
Automating Beef to use over wan without configuring your router

BeefAuto Follow on Social Media Platforms python script Automate Beef And Configure it to use overwan by using ngrok to open ports ScreenShots INSTALL

youhacker55 50 Dec 6, 2022
Easy and flexible jQuery tabbed functionality without all the styling.

JQuery EasyTabs Plugin Tabs with(out) style. EasyTabs creates tabs with all the functionality, no unwanted changes to your markup, and no hidden styli

Steve Schwartz 553 Nov 23, 2022
jQuery easy ticker is a news ticker like plugin, which scrolls the list infinitely. It is highly customizable, flexible with lot of features and works in all browsers.

jQuery Easy Ticker plugin jQuery easy ticker is a news ticker like plugin which scrolls a list infinitely. It is highly customizable, flexible with lo

Aakash Chakravarthy 208 Dec 20, 2022
⏱ Simple Alpine.js plugin to display the human-readable distance between a date and now.

⏱ Alpine TimeAgo ⏱ An Alpine.js plugin to return the distance between a given date and now in words (like "3 months ago", "about 2 hours ago" or "in a

Marc Reichel 47 Dec 22, 2022
A clean integration between Cleave.js and Alpine. ✨

✨ Help support the maintenance of this package by sponsoring me. Alpine Mask This packages provide a custom x-mask directive and $mask magic variable,

Ryan Chandler 48 Dec 26, 2022
E-commerce website using Laravel, Tailwindcss and Alpine.js

About Laravel Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experie

TheCodeholic 15 Dec 12, 2022
Add focal point alignment of images to your Alpine 3.x components with a custom directive.

Alpine Focal Add focal point alignment of images to your Alpine 3.x components with a custom directive. This package only supports Alpine v3.x. About

Michael Lefrancois 2 Oct 12, 2021
Animate your Alpine components.

Animate your Alpine components ?? This package provides you with a simple helper to animate your ALpine.js components. Installation The recommended wa

Ralph J. Smit 18 Nov 16, 2022
Adds a handy $parent magic property to your Alpine components.

✨ Help support the maintenance of this package by sponsoring me. Alpine $parent Access parent components using a handy $parent magic variable. This pa

Ryan Chandler 10 Aug 29, 2022
↕️ A little Alpine.js plugin to automatically resize a textarea to fit its content.

↕️ Alpine Autosize ↕️ A little Alpine.js plugin to automatically resize a textarea to fit its content. ?? Installation CDN Include the following <scri

Marc Reichel 42 Nov 5, 2022
A simple library for handling keyboard shortcuts with Alpine.js.

✨ Help support the maintenance of this package by sponsoring me. Alpine.js Mousetrap A simple library for handling keyboard shortcuts with Alpine.js.

Dan Harrin 102 Nov 14, 2022
IntelliSense for Alpine.js

IntelliSense for Alpine.js Features Provides syntax highlighting for Alpine.js directives along with autocomplete for all base directives and modifier

P Christopher Bowers 8 Nov 17, 2022
Multi-step wizard helpers for Alpine.js

Alpine Wizard This package provides an Alpine directive (x-wizard) and a magic helper ($wizard) that allow you to quickly build multi-step wizards usi

Galahad 74 Dec 23, 2022
📦 Alpine JS plugin to extend the functionality of the official $persist plugin

Alpine JS Persist Extended Alpine JS magic method $storage extends the official $persist plugin to help you work with local storage ?? Example ?? <div

Mark Mead 11 Dec 28, 2022
Alpine.js Language Features (Volar) extension for coc.nvim

[Experimental] coc-volar-alpinejs fork from vscode-alpine-language-features Alpine Language Features extension for coc.nvim Note @volar/alpine-languag

yaegassy 6 Oct 12, 2022