Code Monkey home page Code Monkey logo

ampersand-router's Introduction

ampersand-router

Clientside router with fallbacks for browsers that don't support pushState. Mostly lifted from Backbone.js.

Ampersand-router also adds a redirectTo method which is handy for doing "internal" redirects without breaking backbutton functionality in the browser.

Part of the Ampersand.js toolkit for building clientside applications.

install

npm install ampersand-router

example

In this example the router is trigger a newPage event along with the instance of a page. It is up to your application to listen for this event on the router and then do something with the page instance.

This is helpful when paired with the ampersand-view-switcher#set method to ensure that the page instance is rendered into the correct container and it gets cleaned up properly via its remove method when a new page gets triggered by the router. Check out how the ampersand cli accomplishes this within its router and view-switcher.

var Router = require('ampersand-router');


module.exports = Router.extend({
    routes: {
        '': 'home',
        'users/:id': 'userDetail',
        'info': 'info'
    },

    // ------- ROUTE HANDLERS ---------
    home: function () {
        this.trigger('newPage', new HomePage());
    },

    // redirect example
    userDetail: function (id) {
        var user = app.users.get(id);
        if (user) {
            this.trigger('newPage', new HomePage());
        } else {
            this.redirectTo('users');
        }
    }

    ...
});

API Reference

extend AmpersandRouter.extend(properties)

Get started by creating a custom router class. Define actions that are triggered when certain URL fragments are matched, and provide a routes hash that pairs routes to actions. Note that you'll want to avoid using a leading slash in your route definitions:

var AppRouter = AmpersandRouter.extend({

  routes: {
    "help":                 "help",    // #help
    "search/:query":        "search",  // #search/kiwis
    "search/:query/p:page": "search"   // #search/kiwis/p7
  },

  help: function() {
    //...
  },

  search: function(query, page) {
    //...
  }

});

routes router.routes

The routes hash maps URLs with parameters to functions on your router (or just direct function definitions, if you prefer), similar to the View's events hash. Routes can contain parameter parts, :param, which match a single URL component between slashes; and splat parts *splat, which can match any number of URL components. Part of a route can be made optional by surrounding it in parentheses (/:optional).

For example, a route of "search/:query/p:page" will match a fragment of #search/obama/p2, passing "obama" and "2" to the action.

A route of "file/*path" will match #file/nested/folder/file.txt, passing "nested/folder/file.txt" to the action.

A route of "docs/:section(/:subsection)" will match #docs/faq and #docs/faq/installing, passing "faq" to the action in the first case, and passing "faq" and "installing" to the action in the second.

Trailing slashes are treated as part of the URL, and (correctly) treated as a unique route when accessed. docs and docs/ will fire different callbacks. If you can't avoid generating both types of URLs, you can define a "docs(/)" matcher to capture both cases.

When the visitor presses the back button, or enters a URL, and a particular route is matched, the name of the action will be fired as an event, so that other objects can listen to the router, and be notified. In the following example, visiting #help/uploading will fire a route:help event from the router.

routes: {
  "help/:page":         "help",
  "download/*path":     "download",
  "folder/:name":       "openFolder",
  "folder/:name-:mode": "openFolder"
}

router.on("route:help", function(page) {
  ...
});

constructor / initialize new Router([options])

When creating a new router, you may pass its routes hash directly as the routes option, if you choose. All options will also be passed to your initialize function, if defined.

route router.route(route, name, [callback])

Manually create a route for the router, The route argument may be a routing string or regular expression. Each matching capture from the route or regular expression will be passed as an argument to the callback. The name argument will be triggered as a "route:name" event whenever the route is matched. If the callback argument is omitted router[name] will be used instead. Routes added later may override previously declared routes.

initialize: function(options) {

  // Matches #page/10, passing "10"
  this.route("page/:number", "page", function(number){ ... });

  // Matches /117-a/b/c/open, passing "117-a/b/c" to this.open
  this.route(/^(.*?)\/open$/, "open");

},

open: function(id) { ... }

navigate router.navigate(fragment, [options])

Whenever you reach a point in your application that you'd like to save as a URL, call navigate in order to update the URL. Route function will be called by default, but if you want to prevent it, you can set the trigger option to false. To update the URL without creating an entry in the browser's history, set the replace option to true.

openPage: function(pageNumber) {
  this.document.pages.at(pageNumber).open();
  this.navigate("page/" + pageNumber);
}

// Or ...

app.navigate("help/troubleshooting", {trigger: false});

// Or ...

app.navigate("help/troubleshooting", {replace: true});

reload router.reload()

Allows you to re-navigate to the same page. Re-runs the route handler for the current url.

redirectTo router.redirectTo(fragment)

Sometimes you want to be able to redirect to a different route in your application without adding an entry in the browser's history. RedirectTo is just a shorthand for calling navigate with both trigger and replace set to true.

var AppRouter = AmpersandRouter.extend({
    routes: {
        'login': 'login',
        'dashboard': 'dashboard'
    },

    dashboard: function () {
        if (!app.me.loggedIn) return this.redirectTo('login');

        // show dashboard page...
    }
});

execute router.execute(callback, args)

This method is called internally within the router, whenever a route matches and its corresponding callback is about to be executed. Override it to perform custom parsing or wrapping of your routes, for example, to parse query strings before handing them to your route callback, like so:

var Router = AmpersandRouter.extend({
  execute: function(callback, args) {
    args.push(parseQueryString(args.pop()));
    if (callback) callback.apply(this, args);
  }
});

history.start router.history.start([options])

AmpersandRouter automatically requires and instantiates a single ampersand-history object. AmpersandHistory serves as a global router (per frame) to handle hashchange events or pushState, match the appropriate route, and trigger callbacks. You shouldn't ever have to create one of these yourself since ampersand-router already contains one.

When all of your Routers have been created, and all of the routes are set up properly, call router.history.start() on one of your routers to begin monitoring hashchange events, and dispatching routes. Subsequent calls to history.start() will throw an error, and router.history.started() is a boolean value indicating whether it has already been called.

Supported options:

  • pushState {Boolean} - HTML5 pushState is turned on by default. However if you want to indicate that you don't want to use it in your application, you can add {pushState: false} to the options. Defaults to true
  • hashChange {Boolean} - If you'd like to use pushState, but have browsers that don't support it natively use full page refreshes instead, you can add {hashChange: false} to the options. Defaults to true
  • root {String} - If your application is not being served from the root url / of your domain, be sure to tell History where the root really is, as an option: router.history.start({root: "/public/search/"}). Defaults to /
  • silent {Boolean} - If the server has already rendered the entire page, and you don't want the initial route to trigger when starting History, pass silent: true. Defaults to false

When called, if a route succeeds with a match for the current URL, router.history.start() returns true. If no defined route matches the current URL, it returns false.

credits

All credit goes to Jeremy Ashkenas and the rest of the Backbone.js authors.

If you like this follow @HenrikJoreteg on twitter.

license

MIT

ampersand-router's People

Contributors

bear avatar beck avatar dhritzkiv avatar fyockm avatar gingermusketeer avatar henrikjoreteg avatar joeybaker avatar kamilogorek avatar latentflip avatar lukekarrys avatar mcous avatar pgilad avatar samhashemi avatar vovacodes avatar wraithgar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ampersand-router's Issues

What about the order of the routes-property when JavaScript Object properties have no inherent order?

The order of the routes-property-object seems to be of importance in the Ampersand Router.

We stumbled upon this, because we needed to dynamically define our routes. When we used lodash.assign(routes1, routes2) to merge a bunch of routes, the "catch-all-wildcart" of the first "routes1"-object overruled all routes in our second "routes2"-object. If we reverse the assign-order, things behave differently.

Anyone having same issues here? No-one having crossbrowser issues here?
From ECMAScript Third Edition (pdf):

4.3.3 Object
An object is a member of the type Object. It is an unordered collection of properties each of which contains a primitive value, object, or function. A function stored in a property of an object is called a method.

We noticed in other parts of our app that we can not rely on the order of the properties in a Javascript Object. Yes, we were testing on an old Android 4.x.x's Stock Browser, but this post does confirm our observations: http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop

ampersand-router pulls in underscore

ampersand-router depends on underscore, but looking through the source the only non-trivial method used is extend, which can be more minimally done using a module such as object-assign. Alternatively, the individual lodash sub-modules available on npm are an option. For example isRegExp.

Some background: I'm working on something that I want to use ampersand-router with, but not the rest of ampersand (for now at least). It would be a shame to add underscore as an indirect dependency.

If this proposal seems good, I'd be happy to make a pull request.

is a "catchAll" route impossible by design?

The catchAll route "(*path)": "catchAll" used in the ampersand demo webapp must be the last route in the hash. Otherwise every route defined after is impossible to reach.

{
  "index.html": "home",
  "collections": "collectionDemo",
  "info": "info",
  "person/add": "personAdd",
  "person/:id": "personView",
  "person/:id/edit": "personEdit",
  "(*path)": "catchAll"
}

The Problem is that the object key sorting is not static. I have handled the routes as JSON before using it to extend the router and they got sorted alphabetically. In result the catchAll-Route enters the first place.

{
  "(*path)": "catchAll",
  "collections": "collectionDemo",
  "index.html": "home",
  "info": "info",
  "person/:id": "personView",
  "person/:id/edit": "personEdit",
  "person/add": "personAdd"
}

Should we switch to an array of routes or oversee i something that solves the problem an other way?

search part of location is misinterpreted as fragment when root is not "/"

When root isn't set to "/", I've noticed some unexpected behaviour with regard to the root url when it has a query string.

An example:

const Router = AmpersandRouter.extend({
    routes: {
        "": "index",
        "(*path)": "catchAll"
    },
    index() {
        //never fires
    },
    catchAll(path, query) {
        console.log(pathname);//path results in the query string instead
    }
});

const router = new Router();

router.history.start({
    pushState: true,
    root: "/app"
});

When starting the page at, say /app?foo=bar, path incorrectly results in "foo=bar", which isn't right. router.history.fragment also reports "foo=bar".

However, staring the page at /app/second?foo=bar, path correctly results in "/second"

encoded "%" gets stripped fragment in navigate call

I noticed a bug when dealing with encoded percent signs in urls. Take the following simple example:

store/search/<query>

with a query value of % and proper encoding this gets

store/search/%25

When i pass this as a route to the navigate function the fragment gets changed to store/search/%by the decodeURI call in the following line. Changing the URL fragment on navigation appears like a bug to me. Based on navigating around the code a bit and the comment for that function saying that the caller is responsible for proper encoding I wonder if the decodeURI call is actually necessary.

Any thoughts on that? Thanks.

Non-root routes pass requests to server

So I'm pretty sure this has to be something I'm doing wrong, but I haven't quite found it. I'm currently using just ampersand-router in an app - no other ampersand dependencies. I have something like this happening (vastly simplified):

import ampRouter from 'ampersand-router'

let Router = ampRouter.extend({
  routes: {
    '': 'home',
    'reset': 'reset'
  },
  home: function () {
    console.log('Home!')
  },
  reset: function () {
    console.log('Reset!')
  }
})


class Application {
  constructor() {
    // A bunch of other stuff is instantiated
    this.router = new Router()
    this.router.history.start({
      pushState: true
    })
  }
}

// Global for example purposes
window.app = new Application()

Navigating to the root route ('') calls the home handler as expected, but navigating to /reset sends the request to the server (which in this case returns a 404). Navigating to /#reset calls the reset handler as expected (and ampersand-router rewrites the URL as /reset, as expected). Why are non-hash, non-root routes routing to the server instead of routing to the ampersand-router?

(Also of note is that in both of the above cases, each handler is called twice - not sure what's going on there either)

Extract History and Router into independent modules.

Is there a reason that I cannot use History without Router and visa versa? I'm curious is this was a design decision or just the result of the Backbone port. The current implementation prevents the router from being used outside of the browser.

Honestly, I'd love to see 3 modules:

  1. History/pushState - A client-side module that manages browser history for me and provides events for state change.
  2. Route matching and URL interpolation - Basically I want to map from a URL to a route and params, and from a route and params back to a URL. Could be used client-side or in node.
  3. The whole enchilada - A fancy facade that wraps these two together for me on the client. Basically what this module is today (plus url interpolation FTW).

Question: no route change detection by default?

Dumbest code:

var AmpersandRouter = require('ampersand-router');

var Router = AmpersandRouter.extend({
  routes: {
    "": "main", // #help
    "search/:query": "search", // #search/kiwis
    "search/:query/p:page": "search", // #search/kiwis/p7
    "*notFound": "notFound"
  },
  main: function() {
    console.log('main', arguments);
  },
  search: function(query, page) {
    console.log('search', arguments);
  },
  notFound: function notFound() {
    console.log('notFound', arguments);
  }
});

var router = new Router();

router.history.start();
console.log('blastoff');

If I enter '/#search/foo' in the url nothing happens. Do I have to manually do router.navigate()? I couldn't find a working example for the router, sorry for posting here.

Document browser support

Hey - great to finally see a standalone JS router with a hash fallback! Thank you.

Documentation regarding what browsers are supported would be great . . . specifically what versions of IE are supported?

Even better, consider automating test runs using Sauce Labs to verify that tests are passing in all supported browsers.

Cheers
-Alden

Document history API

Some currently undocumented methods of the history API (like getFragment) are very useful for users of this package - any change you'd consider documenting them publicly? ๐Ÿ˜„

Incorrect routing of anchors when root is set

When a non-empty root is set during router start, routing of clicked anchors fails.

This happens because the anchor pathname is used, which includes the root, which is passed to navigate, which passes that to getFragment, which fails to remove the root from the fragment.

event binding fails while initialize new Router(options)

When creating a new router, you may pass its routes hash directly as an option, if you choose.

The route init want to read particular callbacks from the surrogate object.
if (!callback) callback = this[name];

Problem is that the surrogate object is created in the Router.extend() function only. When passing the routes hash directly this[name] is always undefined and the event binding is broken.

Can the extend step be included in the constructor?

Hash changes are impossible to follow

If I have hashes that I want to track changes in, in addition to normal routing, this is impossible because getFragment() strips out the hash. I know the url changed, but I don't know why without hacking in something tracking hash state on its own.

Trigger current route callback again

Say I am on the page /contact. Is there anyway to force the router to trigger the path to load again if I run router.navigate('/contact')? Currently, the router doesn't fire the route callback if it's already on that route (which is useful 95% of the time)

add event after navigate

Hello,

I have a bug with Firefox only... if I go to : /some-page, and scroll to its bottom, and then change page to /some-other-page , I land on the new page but not scrolled to the top.

The way I see to solve this would be to add an event after router.navigate and scroll manually to the top, but it does not seem to have an event existing for this.

Any thought ?
Vincent

Think about and document how to handle multiple routers. Specifically starting them.

AmpersandRouter automatically requires and instantiates a single ampersand-history object. AmpersandHistory serves as a global router (per frame) to handle hashchange events or pushState, match the appropriate route, and trigger callbacks. You shouldn't ever have to create one of these yourself since ampersand-router already contains one.

When all of your Routers have been created, and all of the routes are set up properly, call router.history.start() on one of your routers to begin monitoring hashchange events, and dispatching routes. Subsequent calls to history.start() will throw an error, and router.history.started is a boolean value indicating whether it has already been called.

Last index in execute args is always null

I'm overriding the execute method to filter the query, just like in the documentation, but the last argument in args is always null. The documentation says that we can do args.pop() to get the query argument (assuming a route like help/:query), which makes sense, but since the last argument is always null, the pop just returns null.

I traced the issue to the route.exec call in _extractParameters so it seems that the regex used to extract the params always returns a null at the end. I can write a workaround for this in my overriding execute method but it seems incorrect for a null to be there.

On a side note, if you add an extra parameter to your route argument list, you get the null but any other extra parameters are undefined. So that null is passed on.

routes: {
    'help/:query': 'help'
},
help: function (query, nully, undef) {
    console.log(nully === null); // true
    console.log(undef === null); // false
    console.log(undef === undefined); // true
}

Underscore dependency

This issue is not really specific to ampersand-router, but I it seems there is no general place to discuss things that cover multiple ampersand modules.

The thing is, underscore is awesome, I use and love it, but it is also a pretty big dependency that doesn't seem appropriate for a collection-of-tiny-modules type framework. Also, unlike Backbone, you can't easily replace underscore with alternatives like lodash.

There was already a mention in #1 that it is planned to remove the underscore dependency in the router, but it makes sense only if it is removed in other modules as well in my opinion.

As a replacement, I would suggest taking underscore and splitting its functions in separate files, probably removing a few that are provided by the most browsers already (like bind, map etc). So instead of doing this:

var _ = require('underscore');
_.extend(...);

you would have to do this:

var extend = require('ampersand-something/extend');
extend(...);

Thoughts?

Basic readme example not cleaning up stuff?

    home: function () {
        this.trigger('newPage', new HomePage());
    },

This means that whenever you go to the homepage it will create a new instance of HomePage and probably bind some events. That code suggests that the events don't get cleaned up.

I think it's better to update the example to either store the last view and then cleanup or to use the ampersand-view-switcher.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.