Convert Figma logo to code with AI

visionmedia logopage.js

Micro client-side router inspired by the Express router

7,689
689
7,689
129

Top Related Projects

Declarative routing for React

6,874

8,333

Manage session history with JavaScript

7,404

🥢 A minimalist-friendly ~2.1KB routing for React and Preact

2,791

A simple vanilla JavaScript router.

Quick Overview

Page.js is a lightweight client-side routing library for single-page applications. It provides a simple API for defining routes and handling navigation events, allowing developers to create smooth, history-aware web applications without page reloads.

Pros

  • Lightweight and minimalistic, with a small footprint
  • Easy to learn and implement, with a straightforward API
  • Supports both hash-based and pushState routing
  • Compatible with various front-end frameworks and vanilla JavaScript

Cons

  • Limited built-in features compared to more comprehensive routing libraries
  • May require additional plugins or custom code for advanced routing scenarios
  • Documentation could be more extensive and up-to-date
  • Not as actively maintained as some other routing libraries

Code Examples

  1. Basic route definition:
page('/', function(ctx) {
  document.body.innerHTML = 'Home page';
});

page('/about', function(ctx) {
  document.body.innerHTML = 'About page';
});

page();
  1. Handling route parameters:
page('/user/:id', function(ctx) {
  const userId = ctx.params.id;
  document.body.innerHTML = `User profile for ID: ${userId}`;
});
  1. Using middleware:
function loadUser(ctx, next) {
  // Simulating an API call
  setTimeout(() => {
    ctx.user = { name: 'John Doe' };
    next();
  }, 100);
}

page('/dashboard', loadUser, function(ctx) {
  document.body.innerHTML = `Welcome, ${ctx.user.name}!`;
});

Getting Started

  1. Install Page.js using npm:
npm install page
  1. Import and use Page.js in your JavaScript file:
import page from 'page';

page('/', () => {
  console.log('Home page');
});

page('/about', () => {
  console.log('About page');
});

page();
  1. Include the necessary HTML5 history API polyfill for older browsers if needed.

  2. Start building your single-page application by defining routes and handling navigation events.

Competitor Comparisons

Declarative routing for React

Pros of React Router

  • Designed specifically for React applications, offering seamless integration
  • Provides nested routing capabilities, allowing for complex UI structures
  • Offers a declarative approach to routing, making it easier to understand and maintain

Cons of React Router

  • Larger bundle size compared to page.js, which may impact initial load times
  • Steeper learning curve for developers new to React or single-page applications
  • More complex setup and configuration, especially for advanced features

Code Comparison

React Router:

import { BrowserRouter, Route, Switch } from 'react-router-dom';

<BrowserRouter>
  <Switch>
    <Route exact path="/" component={Home} />
    <Route path="/about" component={About} />
  </Switch>
</BrowserRouter>

page.js:

import page from 'page';

page('/', home);
page('/about', about);
page();

React Router offers a more declarative approach within JSX, while page.js provides a simpler, function-based routing setup. React Router's syntax is more verbose but allows for easier nesting and configuration of routes directly in the component structure. page.js, on the other hand, offers a more lightweight and straightforward approach to routing, which can be beneficial for smaller projects or those not using React.

6,874

Pros of Reach Router

  • Built specifically for React applications, offering seamless integration
  • Provides accessible navigation out of the box
  • Supports nested routes and relative navigation

Cons of Reach Router

  • Less flexible for non-React applications
  • Smaller community and ecosystem compared to Page.js

Code Comparison

Page.js:

page('/', index)
page('/user/:user', show)
page('/user/:user/edit', edit)
page('/user/:user/album', album)
page('/user/:user/album/sort', sort)
page()

Reach Router:

<Router>
  <Home path="/" />
  <User path="user/:userId" />
  <UserEdit path="user/:userId/edit" />
  <UserAlbum path="user/:userId/album" />
  <UserAlbumSort path="user/:userId/album/sort" />
</Router>

Key Differences

  • Page.js uses a more traditional, imperative approach to routing
  • Reach Router leverages React's component-based architecture
  • Page.js is more lightweight and can be used in various JavaScript environments
  • Reach Router provides built-in accessibility features and focus management

Use Cases

  • Choose Page.js for lightweight, flexible routing in various JavaScript projects
  • Opt for Reach Router in React applications, especially when accessibility is a priority
8,333

Manage session history with JavaScript

Pros of history

  • More comprehensive API with additional features like createBrowserHistory and createHashHistory
  • Better TypeScript support and type definitions
  • Actively maintained with regular updates and bug fixes

Cons of history

  • Larger bundle size compared to page.js
  • Steeper learning curve due to more complex API
  • Less focused on single-page applications compared to page.js

Code Comparison

page.js:

page('/', index)
page('/user/:user', show)
page('/user/:user/edit', edit)
page('*', notfound)
page()

history:

const history = createBrowserHistory()
history.push('/home')
history.replace('/dashboard')
history.listen(({ location, action }) => {
  // Handle location changes
})

Summary

history offers a more comprehensive solution for managing browser history and navigation in JavaScript applications, with better TypeScript support and active maintenance. However, it comes with a larger bundle size and a steeper learning curve. page.js, on the other hand, provides a simpler and more focused approach for single-page applications but lacks some advanced features and type definitions. The choice between the two depends on the specific requirements of your project and your preferred level of abstraction.

7,404

🥢 A minimalist-friendly ~2.1KB routing for React and Preact

Pros of wouter

  • Lightweight and minimalistic, with a smaller bundle size
  • Built-in support for React Hooks, making it more modern and easier to use with functional components
  • No external dependencies, reducing potential conflicts and simplifying integration

Cons of wouter

  • Less mature and less widely adopted compared to page.js
  • Fewer advanced features and customization options
  • Limited to React applications, while page.js is framework-agnostic

Code Comparison

page.js:

page('/', index)
page('/user/:user', show)
page('/user/:user/edit', edit)
page('/user/:user/album', album)
page('/user/:user/album/sort', sort)
page()

wouter:

import { Route, Switch } from "wouter";

<Switch>
  <Route path="/" component={Home} />
  <Route path="/users/:name" component={UserProfile} />
  <Route path="/about" component={About} />
</Switch>

Both libraries provide a simple and intuitive way to define routes, but wouter's approach is more React-centric and leverages JSX syntax. page.js uses a more traditional, imperative style of route definition, which can be used in various JavaScript environments.

2,791

A simple vanilla JavaScript router.

Pros of Navigo

  • Supports hash-based routing, which is useful for single-page applications
  • Offers a more flexible API for defining routes and handling parameters
  • Provides built-in support for nested routes and route hooks

Cons of Navigo

  • Less mature and less widely adopted compared to Page.js
  • May have a steeper learning curve for developers new to routing libraries
  • Limited community support and fewer third-party integrations

Code Comparison

Page.js:

page('/user/:id', function(ctx) {
  console.log('User:', ctx.params.id);
});
page();

Navigo:

const router = new Navigo('/');
router.on('/user/:id', function(params) {
  console.log('User:', params.id);
});
router.resolve();

Both libraries offer similar functionality for basic routing, but Navigo provides more advanced features like nested routes and hooks. Page.js has a simpler API and is more lightweight, while Navigo offers more flexibility and control over routing behavior. The choice between the two depends on the specific requirements of your project and your preferred coding style.

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

page router logo

Tiny Express-inspired client-side router.

Build Status Coverage Status Gitter

page('/', index)
page('/user/:user', show)
page('/user/:user/edit', edit)
page('/user/:user/album', album)
page('/user/:user/album/sort', sort)
page('*', notfound)
page()

Installation

There are multiple ways to install page.js. With package managers:

$ npm install page # for browserify
$ component install visionmedia/page.js
$ bower install visionmedia/page.js

Or use with a CDN. We support:

Using with global script tags:

<script src="https://unpkg.com/page/page.js"></script>
<script>
  page('/about', function(){
    // Do stuff
  });
</script>

Or with modules, in modern browsers:

<script type="module">
  import page from "//unpkg.com/page/page.mjs";

  page('/home', () => { ... });
</script>

Running examples

To run examples do the following to install dev dependencies and run the example server:

$ git clone git://github.com/visionmedia/page.js
$ cd page.js
$ npm install
$ node examples
$ open http://localhost:4000

Currently we have examples for:

  • basic minimal application showing basic routing
  • notfound similar to basic with single-page 404 support
  • album showing pagination and external links
  • profile simple user profiles
  • query-string shows how you can integrate plugins using the router
  • state illustrates how the history state may be used to cache data
  • server illustrates how to use the dispatch option to server initial content
  • chrome Google Chrome style administration interface
  • transitions Shows off a simple technique for adding transitions between "pages"
  • partials using hogan.js to render mustache partials client side

NOTE: keep in mind these examples do not use jQuery or similar, so portions of the examples may be relatively verbose, though they're not directly related to page.js in any way.

API

page(path, callback[, callback ...])

Defines a route mapping path to the given callback(s). Each callback is invoked with two arguments, context and next. Much like Express invoking next will call the next registered callback with the given path.

page('/', user.list)
page('/user/:id', user.load, user.show)
page('/user/:id/edit', user.load, user.edit)
page('*', notfound)

Under certain conditions, links will be disregarded and will not be dispatched, such as:

  • Links that are not of the same origin
  • Links with the download attribute
  • Links with the target attribute
  • Links with the rel="external" attribute

page(callback)

This is equivalent to page('*', callback) for generic "middleware".

page(path)

Navigate to the given path.

$('.view').click(function(e){
  page('/user/12')
  e.preventDefault()
})

page(fromPath, toPath)

Setup redirect from one path to another.

page.redirect(fromPath, toPath)

Identical to page(fromPath, toPath)

page.redirect(path)

Calling page.redirect with only a string as the first parameter redirects to another route. Waits for the current route to push state and after replaces it with the new one leaving the browser history clean.

page('/default', function(){
  // some logic to decide which route to redirect to
  if(admin) {
    page.redirect('/admin');
  } else {
    page.redirect('/guest');
  }
});

page('/default');

page.show(path)

Identical to page(path) above.

page([options])

Register page's popstate / click bindings. If you're doing selective binding you'll like want to pass { click: false } to specify this yourself. The following options are available:

  • click bind to click events [true]
  • popstate bind to popstate [true]
  • dispatch perform initial dispatch [true]
  • hashbang add #! before urls [false]
  • decodeURLComponents remove URL encoding from path components (query string, pathname, hash) [true]
  • window provide a window to control (by default it will control the main window)

If you wish to load serve initial content from the server you likely will want to set dispatch to false.

page.start([options])

Identical to page([options]) above.

page.stop()

Unbind both the popstate and click handlers.

page.base([path])

Get or set the base path. For example if page.js is operating within /blog/* set the base path to "/blog".

page.strict([enable])

Get or set the strict path matching mode to enable. If enabled /blog will not match "/blog/" and /blog/ will not match "/blog".

page.exit(path, callback[, callback ...])

Defines an exit route mapping path to the given callback(s).

Exit routes are called when a page changes, using the context from the previous change. For example:

page('/sidebar', function(ctx, next) {
  sidebar.open = true
  next()
})

page.exit('/sidebar', function(ctx, next) {
  sidebar.open = false
  next()
})

page.exit(callback)

Equivalent to page.exit('*', callback).

page.create([options])

Create a new page instance with the given options. Options provided are the same as provided in page([options]) above. Use this if you need to control multiple windows (like iframes or popups) in addition to the main window.

var otherPage = page.create({ window: iframe.contentWindow });
otherPage('/', main);

page.clickHandler

This is the click handler used by page to handle routing when a user clicks an anchor like <a href="/user/profile">. This is exported for those who want to disable the click handling behavior with page.start({ click: false }), but still might want to dispatch based on the click handler's logic in some scenarios.

Context

Routes are passed Context objects, these may be used to share state, for example ctx.user =, as well as the history "state" ctx.state that the pushState API provides.

Context#save()

Saves the context using replaceState(). For example this is useful for caching HTML or other resources that were loaded for when a user presses "back".

Context#handled

If true, marks the context as handled to prevent default 404 behaviour. For example this is useful for the routes with interminate quantity of the callbacks.

Context#canonicalPath

Pathname including the "base" (if any) and query string "/admin/login?foo=bar".

Context#path

Pathname and query string "/login?foo=bar".

Context#querystring

Query string void of leading ? such as "foo=bar", defaults to "".

Context#pathname

The pathname void of query string "/login".

Context#state

The pushState state object.

Context#title

The pushState title.

Routing

The router uses the same string-to-regexp conversion that Express does, so things like ":id", ":id?", and "*" work as you might expect.

Another aspect that is much like Express is the ability to pass multiple callbacks. You can use this to your advantage to flatten nested callbacks, or simply to abstract components.

Separating concerns

For example suppose you have a route to edit users, and a route to view users. In both cases you need to load the user. One way to achieve this is with several callbacks as shown here:

page('/user/:user', load, show)
page('/user/:user/edit', load, edit)

Using the * character we can alter this to match all routes prefixed with "/user" to achieve the same result:

page('/user/*', load)
page('/user/:user', show)
page('/user/:user/edit', edit)

Likewise * can be used as catch-alls after all routes acting as a 404 handler, before all routes, in-between and so on. For example:

page('/user/:user', load, show)
page('*', function(){
  $('body').text('Not found!')
})

Default 404 behaviour

By default when a route is not matched, page.js invokes page.stop() to unbind itself, and proceed with redirecting to the location requested. This means you may use page.js with a multi-page application without explicitly binding to certain links.

Working with parameters and contexts

Much like request and response objects are passed around in Express, page.js has a single "Context" object. Using the previous examples of load and show for a user, we can assign arbitrary properties to ctx to maintain state between callbacks.

To build a load function that will load the user for subsequent routes you'll need to access the ":id" passed. You can do this with ctx.params.NAME much like Express:

function load(ctx, next){
  var id = ctx.params.id
}

Then perform some kind of action against the server, assigning the user to ctx.user for other routes to utilize. next() is then invoked to pass control to the following matching route in sequence, if any.

function load(ctx, next){
  var id = ctx.params.id
  $.getJSON('/user/' + id + '.json', function(user){
    ctx.user = user
    next()
  })
}

The "show" function might look something like this, however you may render templates or do anything you want. Note that here next() is not invoked, because this is considered the "end point", and no routes will be matched until another link is clicked or page(path) is called.

function show(ctx){
  $('body')
    .empty()
    .append('<h1>' + ctx.user.name + '<h1>');
}

Finally using them like so:

page('/user/:id', load, show)

NOTE: The value of ctx.params.NAME is decoded via decodeURIComponent(sliceOfUrl). One exception though is the use of the plus sign (+) in the url, e.g. /user/john+doe, which is decoded to a space: ctx.params.id == 'john doe'. Also an encoded plus sign (%2B) is decoded to a space.

Working with state

When working with the pushState API, and page.js you may optionally provide state objects available when the user navigates the history.

For example if you had a photo application and you performed a relatively extensive search to populate a list of images, normally when a user clicks "back" in the browser the route would be invoked and the query would be made yet-again.

An example implementation might look as follows:

function show(ctx){
  $.getJSON('/photos', function(images){
    displayImages(images)
  })
}

You may utilize the history's state object to cache this result, or any other values you wish. This makes it possible to completely omit the query when a user presses back, providing a much nicer experience.

function show(ctx){
  if (ctx.state.images) {
    displayImages(ctx.state.images)
  } else {
    $.getJSON('/photos', function(images){
      ctx.state.images = images
      ctx.save()
      displayImages(images)
    })
  }
}

NOTE: ctx.save() must be used if the state changes after the first tick (xhr, setTimeout, etc), otherwise it is optional and the state will be saved after dispatching.

Matching paths

Here are some examples of what's possible with the string to RegExp conversion.

Match an explicit path:

page('/about', callback)

Match with required parameter accessed via ctx.params.name:

page('/user/:name', callback)

Match with several params, for example /user/tj/edit or /user/tj/view.

page('/user/:name/:operation', callback)

Match with one optional and one required, now /user/tj will match the same route as /user/tj/show etc:

page('/user/:name/:operation?', callback)

Use the wildcard char * to match across segments, available via ctx.params[N] where N is the index of * since you may use several. For example the following will match /user/12/edit, /user/12/albums/2/admin and so on.

page('/user/*', loadUser)

Named wildcard accessed, for example /file/javascripts/jquery.js would provide "/javascripts/jquery.js" as ctx.params.file:

page('/file/:file(.*)', loadUser)

And of course RegExp literals, where the capture groups are available via ctx.params[N] where N is the index of the capture group.

page(/^\/commits\/(\d+)\.\.(\d+)/, loadUser)

Plugins

An example plugin examples/query-string/query.js demonstrates how to make plugins. It will provide a parsed ctx.query object derived from node-querystring.

Usage by using "*" to match any path in order to parse the query-string:

page('*', parse)
page('/', show)
page()

function parse(ctx, next) {
  ctx.query = qs.parse(location.search.slice(1));
  next();
}

function show(ctx) {
  if (Object.keys(ctx.query).length) {
    document
      .querySelector('pre')
      .textContent = JSON.stringify(ctx.query, null, 2);
  }
}

Available plugins

Please submit pull requests to add more to this list.

Running tests

In the console:

$ npm install
$ npm test

In the browser:

$ npm install
$ npm run serve
$ open http://localhost:3000/

Support in IE8+

If you want the router to work in older version of Internet Explorer that don't support pushState, you can use the HTML5-History-API polyfill:

  npm install html5-history-api
How to use a Polyfill together with router (OPTIONAL):

If your web app is located within a nested basepath, you will need to specify the basepath for the HTML5-History-API polyfill. Before calling page.base() use: history.redirect([prefixType], [basepath]) - Translation link if required.

  • prefixType: [string|null] - Substitute the string after the anchor (#) by default "/".
  • basepath: [string|null] - Set the base path. See page.base() by default "/". (Note: Slash after pathname required)

Pull Requests

  • Break commits into a single objective.
  • An objective should be a chunk of code that is related but requires explanation.
  • Commits should be in the form of what-it-is: how-it-does-it and or why-it's-needed or what-it-is for trivial changes
  • Pull requests and commits should be a guide to the code.

Server configuration

In order to load and update any URL managed by page.js, you need to configure your environment to point to your project's main file (index.html, for example) for each non-existent URL. Below you will find examples for most common server scenarios.

Nginx

If using Nginx, add this to the .conf file related to your project (inside the "server" part), and reload your Nginx server:

location / {
    try_files $uri $uri/ /index.html?$args;
}

Apache

If using Apache, create (or add to) the .htaccess file in the root of your public folder, with the code:

Options +FollowSymLinks
RewriteEngine On

RewriteCond %{SCRIPT_FILENAME} !-d
RewriteCond %{SCRIPT_FILENAME} !-f

RewriteRule ^.*$ ./index.html

Node.js - Express

For development and/or production, using Express, you need to use express-history-api-fallback package. An example:

import { join } from 'path';
import express from 'express';
import history from 'express-history-api-fallback';

const app = express();
const root = join(__dirname, '../public');

app.use(express.static(root));
app.use(history('index.html', { root }));

const server = app.listen(process.env.PORT || 3000);

export default server;

Node.js - Browsersync

For development using Browsersync, you need to use history-api-fallback package. An example:

var browserSync = require("browser-sync").create();
var historyApiFallback = require('connect-history-api-fallback');

browserSync.init({
	files: ["*.html", "css/*.css", "js/*.js"],
	server: {
		baseDir: ".",
		middleware: [ historyApiFallback() ]
	},
	port: 3030
});

Integrations

License

(The MIT License)

Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

NPM DownloadsLast 30 Days