Skip to content

Latest commit

 

History

History
799 lines (621 loc) · 25.9 KB

DOCUMENTATION.md

File metadata and controls

799 lines (621 loc) · 25.9 KB

API

Method What it does
Frequently used
constructor
on Registers a route
navigate Navigates to a route with a change of the browser URL. You usually are calling this as a result of user interaction. You want to change the URL.
resolve Navigates to a route but it doesn't change the browser URL. You should fire this at least one in the beginning.
notFound Defining a not-found handler
Other methods
navigateByName Navigate to a route by having its name and supplying URL data args
off Removes a registered route
match Checks if the passed path matches some of the routes. It doesn't trigger handlers or hooks.
matchLocation The bare matching logic of Navigo
destroy Removing the currently registered routes
updatePageLinks Call this if you re-render (change the DOM) and want Navigo to recognize the links with data-navigo attribute
link Constructs a path
generate Constructs a path based on a registered route
lastResolved Returns the last resolved route/s
hooks Define all-routes hooks
addBeforeHook Adding before hook on an already created route
addAfterHook Adding after hook on an already created route
addAlreadyHook Adding already hook on an already created route
addLeaveHook Adding leave hook on an already created route
getCurrentLocation Returns a Match object for the current browser location
getRoute Get a Route by name/path

Topics


Types

Object
Navigo
Match
Route
RouteHooks
RouterOptions
NavigateOptions
ResolveOptions

Initializing

Navigo constructor has one mandatory argument - the root path of your application. For example, if you are hosting the application at https://site.com/my/project you have to specify the following:

const router = new Navigo('/my/project');

The second argument is the default router options. The strategy field there defines how many matches the router finds - one or many.

const router = new Navigo('/my/project', { strategy: 'ALL' });

By default the strategy is equal to "ONE". Meaning that when a match is found the router stops resolving other routes.

Another option available is noMatchWarning. Which if you set to true will prevent the router of warning a message if there is no matching route.

Adding a route

interface Navigo {
  on(path: string, f: Function, hooks?: RouteHooks): Navigo;
  on(f: Function, hooks?: RouteHooks): Navigo;
  on(map: Object, hooks?: RouteHooks): Navigo;
}

To add a route use the on method. It can be used in four different ways. The first and the most straightforward one is when you have a path and want to map it to a function.

const router = new Navigo('/');
router.on('/foo/bar', () => {
  // Fired if the page URL matches '/foo/bar'.
});

The path in this case could be also a regular expression. For example:

const router = new Navigo('/');
router.on(/foo\/(.*)/, () => {
  // Fired if the page URL matches '/foo/bar'.
});

If you skip the path you are basically defining a handler for your root.

const router = new Navigo('/my/app');
router.on(() => {
  // Fired if the page URL matches '/my/app' route.
  // Or in other words the home of your app.
})

The on method is chainable so you can call on('...', () => {}).on('...', () => {}) if you want.

The next option is to define a key-value pairs of your routes:

router.on({
  '/foo/bar': () => {
    // Fired if the route matches '/foo/bar'.
  },
  '/foo/zar': () => {
    // Fired if the route matches '/foo/zar'.
  }
});

And the most complex one is by giving your route a name (via the as field). This could be used in a combination of the generate method to construct a page path or simply to identify what is currently matching.

router.on({
  '/foo/bar': {
    as: 'routeA',
    uses: () => {
       // Fired if the route matches '/foo/bar'.
    }
  }
  '/foo/bar': {
    as: 'routeB',
    uses: () => {
       // Fired if the route matches '/foo/bar'.
    }
  }
})

Notice the typing of the on method in the beginning of this section and you'll see that you can pass hooks to each route. More about that in the hooks section.

Parameterized routes

The parameterized routes have paths that contain dynamic parts. For example:

const router = new Navigo('/');

router.on('/user/:id/:action', ({ data }) => {
  console.log(data); // { id: 'xxx', action: 'save' }
});

router.resolve('/user/xxx/save');

"/user/xxx/save" matches the defined route. "xxx" maps to id and "save" to action. The data from the URL comes into the data field of the Match object passed to your handler.

Parameterized routes happen also when we use a regular expression as a path. It's just our data property comes as an array containing the matched groups.

const router = new Navigo('/');
router.on(/rock\/(.*)\/(.*)/, ({ data }) => {
  console.log(data); // ["paper", "scissors"]
});
router.resolve("/rock/paper/scissors");

Reading GET params

Navigo captures the GET params of the matched routes. For example:

const router = new Navigo('/');

router.on('/user/:id/:action', ({ data, params, queryString }) => {
  console.log(data); // { id: 'xxx', action: 'save' }
  console.log(params); // { m: "n", k: "z" }
  console.log(queryString); // "m=n&k=z"
});

router.resolve('/user/xxx/save?m=n&k=z');

Reading hash params

Navigo captures the hash bit of the URL for the matched routes. For example:

const router = new Navigo('/');

router.on('/foobar', ({ hashString }) => {
  console.log(hashString); // something-else
});

router.resolve('/foobar#something-else');

Matching logic

Navigo relies on regular expressions to match strings against location paths. This logic is abstracted and for the final user we have a simple DSL. Here are couple of examples:

router.on('/foo', () => {});
// matches specifically "/foo"

router.on('/foo/:name', () => {});
// matches "/foo/my-name-here"

router.on(':page', () => {});
// matches "/about-page"

router.on('/foo/*', () => {});
// matches "/foo/a/b/c"

router.on('*', () => {});
// matches "/foo/bar/moo"

router.on(/rock\/(.*)\/(.*)/, () => {});
// matches "/rock/paper/scissors"

router.on('/foo/:id/?', () => {});
// matches "/foo/20/save" and also "/foo/20"

Removing a route

interface Navigo {
  off(path: string | RegExp): Navigo;
  off(handler: Function): Navigo;
}

To remove a route call the off method by passing the path (or the used regular expression) or the handler of the route.

Navigating between routes

interface Navigo {
  navigate(to: string, options?: NavigateOptions): void;
}

type NavigateOptions = {
  title?: string;
  stateObj?: Object;
  historyAPIMethod?: string;
  updateBrowserURL?: boolean;
  callHandler?: boolean;
  callHooks?: boolean;
  updateState?: boolean;
  force?: boolean;
  resolveOptions?: ResolveOptions;
};

The navigate method by default:

  • Checks if there is a match. And if the answer is "yes" then ...
  • It calls hooks (if any) and your route handler.
  • Updates the internal state of the router.
  • Updates the browser URL.

Consider the following example:

const router = new Navigo("/");

router
  .on("/foo/bar", () => {
    console.log('Nope');
  })
  .on("/about", () => {
    console.log('This is About page');
  });

router.navigate("about");

After the last line the browser will have in its address bar /about as a path and in the console we'll see "This is About page". router.lastResolved() and router.current will point to an array of a single object of type Match.

navigate accepts a few options:

  • title is a string that gets passed to pushState (or replaceState).
  • stateObj is a state object that gets passed to pushState (or replaceState).
  • If you don't want to have a new entry in the history you should pass { historyAPIMethod: 'replaceState' }. By default is pushState.
  • If updateBrowserURL is set to false the library will not use the history API at all. Meaning, the browser URL will not change.
  • If callHandler is set to false your route handler will not be fired.
  • If callHooks is set to false your route hooks will be skipped.
  • If updateState is set to false the router will not update its internal state. This means that the lastResolved()/current route will not be updated.
  • If force is set to true the router will update its internal state only. This makes the router like it already resolved specific URL.
  • resolveOptions are the same options used in the resolve method.

Navigating by route name

interface Navigo {
  navigateByName(name: string, data?:Object, options?: NavigateOptions): boolean;
}

Very often we have complex URLs and we want to have a quick way to reach them. navigateByName is a method that helps in such cases:

route.on({
  "/users/:name": { 
    as: "user",
    uses: (match) => {
      console.log(match.data.name); // Krasimir
    }
  },
});

router.navigateByName("user", { name: "Krasimir" });

/users/Krasimir is set in the address bar of the browser and the handler is executed.

The method returns true if there is such route with that name (i.e. the navigating happened) and false if it doesn't.

Augment your <a> tags

Let's say that we have a page with links (<a> tags). The links have href attributes pointing to locations inside your app. By default Navigo doesn't know about those links and if you click them you'll probably get a full page load. To augment those links and integrate them with Navigo just add data-navigo attribute. For example:

<a href="/company/about" data-navigo>About</a>

When Navigo is initialized checks the page for such tags and attaches click handler which fires the router's navigate method.

Navigo has a method called updatePageLinks which you have to call every time when you change the DOM and you expect to see new links on the page. Because Navigo is not wired to a rendering engine doesn't really know about the DOM manipulations. It does though makes an assumption - after each of your route handlers there is a updatePageLinks call. The router expects that after the successful route resolving the DOM is updated and calls that function again. Feel free to fire updatePageLinks multiple times on the same DOM tree. There will be just one click handler attached to your links.

Links with target="_blank" are ignored even if they have data-navigo attribute.

data-navigo with value

If you use data-navigo="false" the link will be ignored by Navigo and if there was a click handler for it the handler will be removed.

Custom selector

If you are not happy with data-navigo attribute you may provide your own CSS selector. You may even say "just augment all the links on the page".

const router = new Navigo("/", { linksSelector: "a" });

Example here.

Passing options to the navigate method

As we learned above, when a link with data-navigo attribute is clicked the navigate method of the router gets executed. That same method accepts options and if you want to pass some of them use the following syntax:

<a href="/foo/bar" data-navigo data-navigo-options="updateBrowserURL:false, callHandler: false, updateState: false, force: false, historyAPIMethod: replaceState, resolveOptionsStrategy: ALL, resolveOptionsHash: true">my link</a>

Which will result in the following options:

{
  updateBrowserURL: false,
  callHandler: false,
  updateState: false,
  force: false,
  historyAPIMethod: "replaceState",
  resolveOptions: { strategy: "ALL", hash: true },
}

Resolving routes

interface Navigo {
  resolve(path?: string, resolveOptions?: ResolveOptions): false | Match[];
}

export type ResolveOptions = {
  strategy?: ONE | ALL;
  noMatchWarning?: true | false;
};
type Match = {
  url: string;
  queryString: string;
  route: Route;
  data: Object | null;
  params: Object | null;
};
type Route = {
  name: string;
  path: string;
  handler: Function;
  hooks: RouteHooks;
};

By default Navigo is not resolving your routes. You have to at least once call resolve method. The path argument is not mandatory. If you skip it the library will use the current URL of the page. The method is fired automatically in the following cases:

  • If there is a popstate event dispatched (this happens when the user manually changes the browser location by hitting for example the back button)
  • If the navigate method is called and shouldResolve is not set to false

By default the resolve function catches the first match and stops searching. You can amend this behavior by passing "ALL" as a value of the strategy field. However, if there is a matching route you'll get an array of object (or objects) of type Match. If there is no match then resolve returns false. When your route gets resolved its handler is called. It receives a Match object. From that object you can pull the data passed through the URL (if you used a parameterized path) or the GET params set in the URL.

If you need to see the latest match (or matches) you can access it via the lastResolved() method.

resolve does the following:

  • Checks if there is a match. And if the answer is "yes" then ...
  • It calls hooks (if any) and your route handler.
  • Updates the internal state of the router.

Another option available is noMatchWarning. Which if you set to true will prevent the router of warning a message if there is no matching route.

Resolve options

export type ResolveOptions = {
  strategy?: ONE | ALL;
  hash?: boolean;
  noMatchWarning?: true | false;
};
  • strategy - either "ONE" (by default) or "ALL".
  • hash - whether to use hash based routing or not (by default is false).
  • noMatchWarning - false (by default) or true

Hash based routing

Navigo supports hash based routing. Which means that it uses the hash string as path for routing. For example /my/app/#/about/team is treated as /about/team. To enable this mode you have to pass hash: true when creating the router.

const router = new Navigo('/', { hash: true });

Direct matching of registered routes

If you want to check if some path is matching any of the routes without triggering hooks, handlers or changing the browser URL you may use the match method. For example:

const r: Navigo = new Navigo("/");

r.on("/user/:id", () => {});

console.log(r.match("/nope"));
// result: false

console.log(r.match("/user/xxx/?a=b"));
// result:
// [
//   {
//     data: {
//       id: "xxx",
//     },
//     params: { a: "b" },
//     queryString: "a=b",
//     route: {
//       handler: [Function],
//       hooks: undefined,
//       name: "user/:id",
//       path: "user/:id",
//     },
//     url: "user/xxx",
//   }
// ]

The function returns an array of Match objects or false.

Direct matching of paths

There is a matchLocation method that offers the bare matching logic of the router. You pass a path and it checks if the string matches the current location. If you don't want to use the current location of the browser you may send a second argument. For example:

// let's say that the path of the browser is "/foo/bar?a=b"
router.matchLocation('/foo/:id');
/*
{
  data: {
    id: "bar",
  },
  params: { a: "b" },
  queryString: "a=b",
  route: {
    handler: expect.any(Function),
    hooks: {},
    name: "foo/:id",
    path: "foo/:id",
  },
  url: "foo/bar",
}
*/

// passing the current location manually
r.matchLocation("/foo/:id", "/foo/bar?a=b");

The method returns false if there is no match.

Hooks

The hooks are functions that are fired as part of the resolving process. Think about them as lifecycle methods.

Type of hooks

The hooks object has the following signature:

type RouteHooks = {
  before?: (done: Function, match: Match) => void;
  after?: (match: Match) => void;
  leave?: (done: Function, match: Match) => void;
  already?: (match: Match) => void;
};
  • The before hook receives two arguments. The first one is a function that needs to be called with either no arguments or false. The no-argument version means "move forward". false stops the resolving and your handler will not be called.
  • after is called after your handler
  • leave is called when you are about to leave out of the route. Similarly to the before hook accepts a function as first argument and a Match object (or an array of Match objects) as second. If the function is called with false Navigo will stop resolving the new matched route meaning "we cant' go out of the current route".
  • already is called when this is the current route and it matches again

Defining hooks for specific route

The on method accepts a hooks object as a last argument. For example:

// for specific path
router.on('/foo/bar/', () => {...}, {
  before(done, match) {
    // do something
    done();
  }
});

// for the root
router.on(() => {...}, {
  before(done, match) {
    // do something
    done();
  }
});

// when using a route map
r.on({
  "/foo/:id": {
    as: "some.name",
    uses: handler,
    hooks: {
      before: (done) => {
        // do something
        done();
      },
    },
  },
});

// with the notFound method
router.notFound(() => {...}, {
  before(done, match) {
    // do something
    done();
  }
});

Defining hooks for all the routes

You can define hooks that will be used for all the registered routes. It is important to set the hooks before the other route definition.

const router = new Navigo("/");
router.hooks({
  before(done, match) {
    // do something
    done();
  }
});
router.on("/foo/bar", () => {});
router.on("/", () => {});

Adding a hook to already defined route

Sometimes you may need to add a hook to a route later. In such cases use the following syntax:

const router = new Navigo("/");

router.on("/foo/bar", () => {});
const cleanup = router.addBeforeHook('foo/bar', (done) => {
  // my before hook logic
})

// ... some other logic
cleanup();

With addBeforeHook we have addAfterHook, addAlreadyHook and addLeaveHook methods. All of them return a function which if called will remove the hook from the specific route.

Destroying the router

destroy(): void;

If you no longer need Navigo working just call router.destroy(). This will flush all the registered routes so nothing will match.

Generating paths

There are two methods link and generate that you can use to create string paths. For example:

const router = new Navigo('/my/app');

router.link('blah'); // "/my/app/blah

router.on({
  "/user/:id/:action": { as: "RouteNameHere", uses: () => {} },
});

r.generate("RouteNameHere", { id: "xxx", action: "save" }); // "/my/app/user/xxx/save"

Notice that the produced strings by default start with the root that you passed to the Navigo's constructor. If you don't want this behavior pass a third argument to the function { includeRoot: false }. In this case the second argument should be always present. Feel free to pass null if you don't have any data.

Handling a not-found page

interface Navigo {
  notFound(handler: Function, hooks?: RouteHooks): Navigo;
}

Navigo offers a special handler for the cases where a no match is found.

const router = new Navigo('/');

router.notFound(() => {
  // this runs if there is no match found
});

Getting current location of the browser

router.getCurrentLocation() returns the current location of the browser in the format of a Match object.

Getting access to a route

There is getRoute method that accepts a route name/path. The object that you receive is of type Route.

Types

Navigo

class Navigo {
  constructor(root: string, options?: RouterOptions);
  destroyed: boolean;
  current: Match[];
  routes: Route[];
  on(f: Function, hooks?: RouteHooks): Navigo;
  on(map: Object, hooks?: RouteHooks): Navigo;
  on(path: string | RegExp, f: Function, hooks?: RouteHooks): Navigo;
  off(path: string | RegExp): Navigo;
  off(handler: Function): Navigo;
  navigate(to: string, options?: NavigateOptions): void;
  navigateByName(name: string, data?: Object, options?: NavigateOptions): void;
  resolve(path?: string, resolveOptions?: ResolveOptions): false | Match;
  destroy(): void;
  notFound(handler: Function, hooks?: RouteHooks): Navigo;
  updatePageLinks(): Navigo;
  link(path: string): string;
  lastResolved(): null | Match[];
  generate(name: string, data?: Object, options:? GenerateOptions): string;
  hooks(hooks: RouteHooks): Navigo;
  getLinkPath(link: Object): string;
  match(path: string): false | Match[];
  matchLocation(path: string, currentLocation?: string, annotatePathWithRoot?: boolean): false | Match;
  getCurrentLocation(): Match;
  addBeforeHook(route: Route | string, hookFunction: Function): Function;
  addAfterHook(route: Route | string, hookFunction: Function): Function;
  addAlreadyHook(route: Route | string, hookFunction: Function): Function;
  addLeaveHook(route: Route | string, hookFunction: Function): Function;
  getRoute(name: string): Router | undefined;
}

Match

type Match = {
  url: string;
  queryString: string;
  hashString: string;
  route: Route;
  data: Object | null;
  params: Object | null;
};

Route

type Route = {
  name: string;
  path: string | RegExp;
  handler: Function;
  hooks: RouteHooks;
};

RouteHooks

type RouteHooks = {
  before?: (done: Function, match: Match) => void;
  after?: (match: Match) => void;
  leave?: (done: Function, match: Match | Match[]) => void;
  already?: (match: Match) => void;
};

RouterOptions

export type RouterOptions = ResolveOptions & { linksSelector?: string };

NavigateOptions

type NavigateOptions = {
  title?: string;
  stateObj?: Object;
  historyAPIMethod?: string;
  updateBrowserURL?: boolean;
  callHandler?: boolean;
  callHooks?: boolean;
  updateState?: boolean;
  force?: boolean;
  resolveOptions?: ResolveOptions;
};

ResolveOptions

export type ResolveOptions = {
  strategy?: ONE | ALL;
  hash?: boolean;
  noMatchWarning?: true | false;
};

GenerateOptions

export type GenerateOptions = {
  includeRoot: boolean;
};