node package manager

sugarskull


![/sugarskull/](https://github.com/flatiron/SugarSkull/raw/master/img/sugarskull.png)

Website/Demo: http://flatiron.github.com/sugarskull

SugarSkull is a router. Sugarskull can handle location.hash-based routing on the server, url.path-based routing for HTTP requests, and optimist-based routing for cli applications. As a client side router, it's the smallest amount of glue needed for building dynamic single page applications.

On the client, SugarSkull has no dependencies---not even jquery.

Examples

Client-Side Hash Routing

<!html>
<html>
  <head>
    <script src="/sugarskull.js"></script> 
    <script>
 
      var meow = function (req, res) { /* ... */ },
          scratch = function (req, res) { /* ... */ };
 
      var routes = {
 
        '/dog': bark,
        '/cat': [meow, scratch]
      };
 
      var router = Router(routes);
    </script> 
  </head>
  <body>
    <!-- body goes here -->
  </body>
</html>

Server-Side HTTP Routing

var http = require('http'),
    sugarskull = require('../lib/sugarskull');
 
var router = new sugarskull.http.Router();
 
var server = http.createServer(function (req, res) {
  router.dispatch(req, res, function (err) {
    if (err) {
      res.writeHead(404);
      res.end();
    }
  });
});
 
router.get(/foo/, function () {
  this.res.writeHead(200, { 'Content-Type': 'text/plain' })
  this.res.end('hello world\n');
});
 
server.listen(8080);
console.log('vanilla http server with sugarskull running on 8080');

Motivation

Sometimes you only need a wrench, not the toolbox with a hammer, screwdriver, etc. sugarskull is small. it does one job and does it well. It's not a framework, its a simple tool easy to add or remove. It promotes centralizing router logic so it's not intertwined throughout your code. Sugarskull was intended to replace backbone.js routes and provide a lighter weight, less sinatra-like alternative to sammy.js.

Storing some information about the state of an application within the URL allows the URL of the application to be emailed, bookmarked or copied and pasted. When the URL is visited it restores the state of the application. A client side router will also notify the browser about changes to the page, so even if the page does not reload, the back/forward buttons will give the illusion of navigation.

The HTML5 history API isn't a replacement for using the location hash. The HTML5 history API requires that a URL resolves to real assets on the server. It is also designed around the requirement that all pages should load without Javascript. SugarSkull targets script-rich applications whose audience is well-known.

Anatomy of a Route

In general, SugarSkull associates functions with routes. When routing occurs, if the route matches one defined in SugarSkull's routing table then the function(s) associated with that route are executed.

The url is divided into two parts, divided by a "#":

HTTP Routing

Server-Side

The part of the url before the "#" is considered a server-side path, and is routed server-side.

Client-Side

The part of the url after the "#" is not considered part of the document's path, and is routed client-side for single-page apps.

HashRoute

CLI

SugarSkull routes for cli options are based on command line input instead of url, based on the output of optimist.

Usage

Constructor

 
  var router = Router(routes);
 

routes (required)

An object literal that contains nested route definitions. A potentially nested set of key/value pairs. The keys in the object literal represent each potential part of the URL. The values in the object literal contain references to the functions that should be associated with them. bark and meow are two functions that you have defined in your code.

 
  var routes = { // an object literal. 
 
    '/dog': bark, // a route which assigns the function `bark`. 
    '/cat': [meow, scratch] // a route which assigns the functions `meow` and `scratch`. 
  };
 
  var router = Router(routes); // Instantiate the router. 
 

URL Matching

 
  var router = Router({ // given the route '/dog/yella'. 
 
    '/dog': {
      '/:color': {
        on: function(color) { console.log(color) } // this function will return the value 'yella'. 
      }
    }
 
  }).init();
 

Routes can sometimes become very complex, simple/:tokens don't always suffice. SugarSkull supports regular expressions inside the route names. The values captured from the regular expressions are passed to your listener function.

 
  var router = Router({ // given the route '/dog/yella'. 
 
    '/dog': {
      '/(\\w+)': {
        on: function(color) { console.log(color) } // this function will return the value 'yella'. 
      }
    }
 
  }).init();
 
 
  var router = Router({ // given the route '/ferret/smelly/true/damn'. 
 
    '/ferret': {
      '/smelly/?([^\/]*)\/([^\/]*)/?': function(a, b) {
        console.log(a, b);
      }
    }
 
  }).init();
 

Special Events

In some cases a listener should only fire once or only after the user leaves the route. See the API section for more events and details about what events are available.

 
  var router = Router({
 
    '/dog': {
      on: bark
    },
 
    '/cat': {
      on: meow
      after: function() {}
    }
 
  }).use({ 
    
    // In some cases you may want to have these events always fire 
    
    on: function(value) { console.log('the previous route captured the value ' + value); }, 
    after: function(value) { console.log('the previous route captured the value ' + value); },
    
    // if you use multiple routers and define a notfound route, be cautious about multiple notfound listeners firing. 
    
    notfound: function(value) { console.log('the route named ' + value + ' could not be found'); }
 
  }).init();
 

More Options

recurse

Can be assigned the value of forward or backward. The recurse option will determine the order in which to fire the listeners that are associated with your routes. If this option is NOT specified or set to null, then only the listeners associated with an exact match will be fired.

No recursion, with the URL /dog/angry

 
  var routes = {
 
    '/dog': {
      '/angry': {
        on: growl // this method will be fired. 
      },
      on: bark
    }
  };
 
  var router = Router(routes);
 

Recursion set to true, with the URL /dog/angry

 
  var routes = {
 
    '/dog': {
      '/angry': {
        on: growl // this method will be fired second. 
      },
      on: bark // this method will be fired first. 
    }
  };
 
  var router = Router(routes).use({ recurse: 'forward' }).init();
 

Recursion set to false, with the URL /dog/angry

 
  var routes = {
 
    '/dog': {
      '/angry': {
        on: growl // this method will be fired first. 
      },
      on: bark // this method will be fired second. 
    }
  };
 
  var router = Router(routes).use({ recurse: 'backward' }).init();
 

Breaking out of recursion, with the URL /dog/angry

 
  var routes = {
 
    '/dog': {
      '/angry': {
        on: function() { return false; } // this method will be fired first. 
      },
      on: bark // this method will not be fired. 
    }
  };
  
  // this feature works in reverse with recursion set to true. 
 
  var router = Router(routes).use({ recurse: 'backward' }).init();
 

resource

An object literal containing functions. If a host object is specified, your route definitions can provide string literals that represent the function names inside the host object. A host object can provide the means for better encapsulation and design.

 
  var router = Router({
 
    '/moola': {
      '/benny': 'hundred',
      '/sawbuck': 'five'
    }
 
  }).use({ resource: container }).init();
 
  var container = {
    hundred: function() { return 100; },
    five: function() { return 5; }
  };
 

Maintaining State

It is possible to attach state to any segment of the router, so in our case above if /dog is reached, the current state will be set to { needy: true, fetch: 'possibly' }. Each nested section will merge into and overwrite the current state. So in the case where the router matches /cat/hungry, the state will become { needy: true, fetch: 'unlikely', frantic: true }.

 
  var router = Router({
 
    '/dog': {
      on: bark,
      state: { needy: true, fetch: 'possibly' }
    },
 
    '/cat': {
      '/hungry': {
        state: { needy: true, frantic: true }
      },
      on: meow,
      state: { needy: false, fetch: 'unlikely' }
    }
 
  }).init();
 

API

Constructor

Router(config)

config {Object} - An object literal representing the router configuration.

Returns a new instance of the router.

Instance methods

use([on, after, recurse, resource])

on {Function} or {Array} - A callback or list of callbacks that will fire on every route.
after {Function} or {Array} - A callback or list of callbacks that will fire after every route.
recurse {String} - Determines the order in which to fire the listeners that are associated with your routes. can be set to 'backward' or 'forward'.
resource {Object} - An object literal of function declarations.
notfound {Function} or {Array} - A callback or a list of callbacks to be called when there is no matching route.

Initialize the router, start listening for changes to the URL.

init()

Initialize the router, start listening for changes to the URL.

getState()

Returns the state object that is relative to the current route.

getRoute([index])

index {Numner} - The hash value is divided by forward slashes, each section then has an index, if this is provided, only that section of the route will be returned.

Returns the entire route or just a section of it.

setRoute(route)

route {String} - Supply a route value, such as home/stats.

Set the current route.

setRoute(start, length)

start {Number} - The position at which to start removing items.
length {Number} - The number of items to remove from the route.

Remove a segment from the current route.

setRoute(index, value)

index {Number} - The hash value is divided by forward slashes, each section then has an index.
value {String} - The new value to assign the the position indicated by the first parameter.

Set a segment of the current route.

Events

Events on each route

on - A function or array of functions to execute when the route is matched.
after - A function or array of functions to execute when leaving a particular route.
once - A function or array of functions to execute only once for a particular route.

Events on all routes

on - A function or array of functions to execute when any route is matched.
after - A function or array of functions to execute when leaving any route.

Article 1. the constructor should allow the centralization of the routing table.

var router = new Router({
  '/foo': fooLogic
});

Article 2. the constructor's routing table should allow definition nesting for terseness.

var router = new Router({
  '/foo': {
    '/bar': barLogic
    '/bazz': bazzLogic 
  }
});

Article 3. the constructor's routing table should allow named events to be associated with routing segments

var router = new Router({
  '/foo': {
    '/bar': barLogic,
    '/bazz': {
      on: bazzOn // any kind of VERB. 
      after: bazzAfter
    }
  }
});

Article 4. the constructor's routing table should allow special attributes to be associated with routing segments

var router = new Router({
  '/foo': {
    '/bar': barLogic,
    '/bazz': {
      accept: ['GET', 'POST'], // limit the verbs that on will accept. 
      on: bazzLogic,
      after: bazzAfter
    }
  }
});

Article 5. the constructor's routing table should allow event types to be associated with routing segments

var router = new Router({
  '/foo': {
    '/bar': barlogic // no event type specified, synonymous with GET 
    '/bazz': {
      GET: fooGetLogic, // explicitly accept the GET verb 
      POST: fooPostLogic // explicitly accept the POST verb 
    }
  }
});

Article 6. the instance should have a global method.

var router = new Router({
  '/foo': {
    '/bar': barlogic // no event type specified, defaults to GET 
    GET: fooLogic // explicitly accept the verb GET for `/foo` 
  }
});
 
router.global({
  on: onGlobal // fired for anything. 
});

Article 7. the constructor's routing table should allow filter definitions to be associated with routing segments, these are simple and resolve to true or false values which permit the execution of the logic associated with the route.

var router = new Router({
  '/foo': {
    '/bar': barlogic // no event type specified, synonymous with GET 
    '/bazz': {
      GET: bazzGetLogic, // explicit activity 
      filter: ['POST', 'DELETE'],
      on: bazzLogic // any other activity 
    }
  }
});
 
router.global({
  filterMethod: YAHOO.Auth.Secure(), // define the logic for filtering. 
  filter: ['DELETE'] // all DELETEs will be filtered 
});

Article 8. the instance should allow for ad-hoc routing.

  var router = new Router();
 
  router.global({
    filterMethod = YAHOO.Auth.Secure();
    router.filter = ['POST', 'DELETE']; // global filters. 
  });
 
  router.path('/regions', function () {
 
    this.accept = ['POST', 'GET', 'DELETE'];
    this.filter = ['POST', 'DELETE']; // scoped filters (catches `verb-as-method` and `on`). 
 
    this.on('/:state', function(country) {
      // this.request 
      // this.response 
    });
 
    this.on('/:provence', function(country) {
      // this.request 
      // this.response 
    });
 
  });

Article 9. the instance should allow for ad-hoc routing with special events.

var router = new Router();
router.routes('/foo', function() {
 
  this.route('/bar', barLogic);
  this.route('/bazz', function() {
    this.route('bla', { POST: blaPOSTLogic, after: blaAfterLogic });
  });
 
});

Frequently Asked Questions

What About SEO?

Is using a client side router a problem for SEO? Yes. If advertising is a requirement, you are probably building a "Web Page" and not a "Web Application". SugarSkull on the client is meant for script-heavy Web Applications.

Is SugarSkull compatible with X?

SugarSkull is known to be Ender.js compatible. However, the project still needs solid cross-browser testing.

Licence

(The MIT License)

Copyright (c) 2010 Nodejitsu Inc. http://www.twitter.com/nodejitsu

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.