npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

rove

v0.0.1

Published

Name-based routing library

Downloads

5

Readme

Rove

rove /rōv/: verb travel constantly without a fixed destination; wander.

npm install rove

Build Status

Standalone scripts

There are prebuilt bundles located in docs/dist:

  • rove.js: 15KB Browser production, strips all assertions and minified
  • rove.dev.js: 525KB Browser development, contains wicked assertions to help

Example

import Router from 'rove'

const router = new Router('index', r => [
  r('users', 'users-index', r => [
    r.param('userId', 'user-index', r => [
      r('edit', 'user-edit')
    ])
  ]),
  r('files', 'files-index', r => [
    r.splat('file-index')
  ]),
  r('a/b/c', 'deep-page')
])

function clientSideUsageExample () {
  router.onNavigation(route => {
    // update app state with the route
  })

  router.initialize()
  // ^ fires `onNavigation` with the current location's route

  router.interceptLinkClicks(true)
  // ^ binds to the document, waiting for links within the domain

  router.navigateTo({
    route: 'user-edit',
    routeParams: {userId: 'karl'},
    queryParams: {editing: 'yes'}
  })
  // updates URL to `/users/karl/edit?editing=yes` and
  // fires `onNavigation` with {route, routeParams, queryParams}
}

function serverSideUsageExample () {
  let UserEditController, NotImplementedYetController, NotFoundController

  const url = '/users/randy/edit'
  const route = router.getUrlRoute(url)
  if (route) {
    // no matching route must be a 404, fall through
    switch (route.route) {
      case 'user-edit':
        return UserEditController(route.routeParams.userId, route.queryParams)
      default:
        return NotImplementedYetController()
    }
  }

  return NotFoundController()
}

if (typeof window === 'object') {
  clientSideUsageExample()
} else {
  serverSideUsageExample()
}

About

Rove is a routing library for client and server applications. It does not dictate application structure around it. Rove is a small component of the application which has a small API and speaks in JavaScript objects. Rove also does not expose URLs or their construction, using route names to convey a given route, which is better and more resilient for real applications.

More specifically, Rove has the following features:

  • Push-state with hash history fallback
  • Route parameters, like :id
  • Query parameters, like ?yes=no
  • Splat segments, like master/src/index.js
  • Redirects
  • Some nice DOM listeners for navigating on <a> link clicks
  • Good debugging and errors for bad routing
  • Nested routes and routers
  • De/serialization for query parameters
  • Defaults for query parameters
  • Server side functions

Design benefits

  1. Names not paths. No one is working in URLs, every path has a name to use instead. This means a change from /users to /people won't break anything. A change from /users to /v1/users also does not break anything.

  2. No string manipulation. Route and query parameters inject behind the curtain. No need to manually build URLs.

  3. Less state. Having names instead of paths, we can do switch(route)'ing in our views. The current route also belongs in application state, not tied to the router.

  4. Better writing/debugging. Since the routing table builds from nested arrays, the way we check for a matching route and priorities are much more intuitive. Based on the routing, the developer receives errors for:

  • Registering a route name more than once (conflict)
  • Navigating to a route name that is not registered
  • Not providing all necessary route parameters for a given route
  • And more!
  1. Routing is more compose-able. Routes can assemble agnostic to where they mount. These can be anywhere in the app URL wise but we can always use them by name.

What's missing intentionally?

  • Regular Expression support. I do not think it necessary for now.

  • Document title management. This is another piece of state that can take into consideration the entire application state and different loading/errors states, which Rove stays away from knowing about.

  • UI components. Someone else can build a sweet <Link> component.

  • Function handlers for dispatching. Again, this is about state management. Rove does not care about application views or controllers and structuring an app around the router causes a lot of problems.

Documentation

  • Router
    • Universal methods
      • getRouteUrl(route: NavigationEntry): string
      • getUrlRoute(url: string): NavigationEntry
      • isRouteEqual(routeX: NavigationEntry, routeY: NavigationEntry): boolean
      • isRouteWithin(routeX: NavigationEntry, routeY: NavigationEntry): boolean
    • Client-side methods (i.e. history state aware)
      • onNavigation(listener: function)
      • offNavigation(listener: function)
      • navigateTo(route: NavigationOptions)
      • warnOnNavigation(message: string)
      • initialize()
      • interceptLinkClicks(attach: boolean)
      • onClick(event: MouseEvent)
      • getCurrentRoute(): NavigationEntry
      • isRouteActive(route: NavigationEntry): boolean
      • isCurrentRouteWithin(route: NavigationEntry): boolean

Router

The Route class is the Rove export. The overloaded constructor requires indexRouteName.

  • router = new Router(basePath: string, indexRouteName: string, options: RouteOptions, routeBuilderFn)
  • router = new Router(basePath: string, indexRouteName: string, routeBuilderFn)
  • router = new Router(basePath: string, indexRouteName: string, options: RouteOptions)
  • router = new Router(basePath: string, indexRouteName: string)
  • router = new Router(indexRouteName: string, options: RouteOptions, routeBuilderFn)
  • router = new Router(indexRouteName: string, routeBuilderFn)
  • router = new Router(indexRouteName: string, options: RouteOptions)
  • router = new Router(indexRouteName: string)

This builds the routing table of the router. The basePath (defaults to "") determines where to start caring about route matching. The indexRouteName names the top-level route (i.e. basePath). The routeBuilderFn must be a function which accepts the r(path: string, routeName: string [, options: RouteOptions][, routeBuilderFn]) function and returns an array containing the results of calls to r() or child Router instances.

RouteOptions is a map of the following options:

| Option | Type | Default | Description | | -------- | ---- | ------- | ----------- | | serializeQuery | function | identity | Change queryParams before they encode into the URL. | deserializeQuery | function | identity | Change queryParams after they decode from the URL. | queryDefaults | object | {} | Defaults which merge into queryParams for the route. Note: Default values in the final queryParam object are not reflected in the URL. This reduces URL clutter for routes with lots of options.

Params

r.param(paramName: string, routeName: string [, options: RouteOptions][, routeBuilderFn])

The paramName is the route parameter key that must have a value to navigate to routeName or its child routes.

Splats

r.splat(routeName: string, [options: RouteOptions])

Splats will match anything and that variable segment is the NavigationEntry splat: string property. Splats cannot have children. Note: Splats are catch-all so order them last in the routing table.

Redirects

r.redirect(path: string, route: NavigationEntry)

When sent to that path, the router will follow the given route to a route that cannot redirect. Redirect loops are not possible because redirects are not named. An error throws if a redirect points to a route that was not defined in the routing table.

Universal methods

Both client-side and server-side support these methods.

router.getRouteUrl(route: NavigationEntry): string

This method returns the route URL, or null if there is no matching route.

router.getUrlRoute(url: string): NavigationEntry

This method returns the URL's route, or null if there is no matching route.

router.isRouteEqual(routeX: NavigationEntry, routeY: NavigationEntry): boolean

This method returns whether to routes are equal. A route X is equal to route Y when their route is equal, splat is equal, and their serialized routeParams and queryParams are equal.

router.isRouteWithin(routeX: NavigationEntry, routeY: NavigationEntry): boolean

Returns whether routeX is within routeY. A route X is within route Y when:

  1. Y has no query parameters and Y's path is a base path of X. For example, /a/b is within /a, but not within /a?yes=no.
  2. Y and X have the same path and Y has query parameters which are a proper subset of the query parameters X has. For example, /a?i=1 is within /a but not within /a?i=1 or /a?i=1&j=2.

This is useful for top-level links, such as example.com/docs. You would like to know that the current route exists within /docs to highlight the top link a certain way. Using route.isRouteWithin() allows us not to keep track of every possible child route of the /docs route or have to manually slice on the current URL.

Client-side methods

The following methods are for client-side applications and will throw an error if used on the server-side.

router.onNavigation(listener: function)

This method subscribes the listener to all new navigation route changes. The listener will receive the route containing a NavigationEntry.

NavigationEntry is a map of the following properties:

| Property | Type | Default | Description | | -------- | ---- | ------- | ----------- | | route | string | required | Name for the route | splat | string | "" | Splat string for the route | routeParams | object | {} | Route parameters for the route | queryParams | object | {} | Query parameters for the route | redirect | boolean | false | Whether the route was the result of a redirect

Note: When the listener receives null this indicates navigation to a URL without a matching route.

Note: To remove the subscription, call router.offNavigation() with the same listener function.

router.warnOnNavigation(message: string)

This method will set a flag with the message on the current route. When navigating away from current route, the user must confirm they want to leave the current "page" before the navigation occurs. The next route will not warn, unless you call warnOnNavigation for that route. You can call this method repeated without side-effects if the message needs changed over time for the same route. An empty string "" will show the default browser message, which is best if you are not localizing your message. To cancel a set warning, call warnOnNavigation(false).

Aside: This is good for when someone is editing a form and tries to leave before they submit it. From a UI/UX standpoint use it sparingly. You should be saving draft versions of critical form data because the user will not get a prompt if the program or computer dies or breaks. Users hate popup dialogues so if you use them too much or incorrectly they will block your pages from using popups at all. Any resulting poor experiences are on you.

router.initialize()

This method reads the current location and fires onNavigation events to all listeners with the initial route. This should fire on client-side startup. This throws an error when called on the server.

router.interceptLinkClicks(attach: boolean)

This method attaches an event listener to the top-level window.document, which will trigger onNavigation events when the destination URL with within the router's basePath. This throws an error when called on the server.

Note: To remove the listener, call router.interceptLinkClicks(false).

router.onClick(event: MouseEvent)

This is a convenience function that attaches directly to links via onclick= or addEventListener(). It does the same as router.interceptLinkClicks() for single elements.

Aside: Without using the above click handlers, you could run into trouble with event.target. Rove event handlers recurse up from the original target looking for the immediate <a> parent. This consideration is important for <a> nodes which containing other potential click targets. Rove event handlers also respect target="_blank", which opens a new window/tab. You can build your own handlers, but be aware of these edge cases.

route.navigateTo(route: NavigationOptions)

This method will trigger an onNavigation event with the new route. This method will throw an error if route.route is not a route in the routing table, which is for developer sanity but also enforcing static route names.

NavigationOptions is a map of the following options:

| Property | Type | Default | Description | | -------- | ---- | ------- | ----------- | | route | string | required | Name for the route | splat | string | none | Splat string for the route | routeParams | object | {} | Route parameters for the route | queryParams | object | {} | Query parameters for the route | replace | boolean | false | Whether to replace the previous history state in the browser's history stack

router.getCurrentRoute(): NavigationEntry

This method returns the current route, which is the last onNavigation entry.

router.isRouteActive(route: NavigationEntry): boolean

This method returns whether the route is equal to the current route.

Note: This is x => router.isRouteEqual(router.getCurrentRoute(), x).

router.isCurrentRouteWithin(route: NavigationEntry): boolean

This method returns whether the current route is within the given route.

Note: This is x => router.isRouteWithin(router.getCurrentRoute(), x).

Contributing

Contributions are incredibly welcome as long as they are standardly applicable and pass the tests (or break bad ones). Tests are in AVA.

# running tests
npm run test

Follow me on Twitter for updates or for the lolz and please check out my other repositories if I have earned it. I thank you for reading.