Light Router

Light router is contained in the mvcct-controls package file mvcct.routing.js. When using usual synchronous loading all utilities are contained in the jJavaScript namespace mvcct.routing, with AMD or Node.js loading the object containing them may be obtained by requiring mvcct.routing.

Main features

  • It is very light (7kb minimized) and has no dependencies.
  • You may have several independent routers in the same page. Only the main router may be connected to the browser history, but all other routers have their private history that may be easily exploited by calling its methods.
  • The format of all routes may be changed without affecting the remainder of the application. In fact, the action method generates all urls from a set of parameters, thus adapting them to routing rules changes. The router, transforms urls into parameters values that are passed to user provided action functions, while the action method performs the inverse transformation by creating the url that would yield a given set of parameters.
  • Routing rules may be nested. For instance, the 0 level routing rule might select the view, while level 1 might select further options of the view, or a detail view to be shown in a different area. If the user clicks a link that doesn't change the url section that selects the view, the view isn't re-loaded and mantains its state, while receiving the new options passed by the level 1 routing rules.
    In general the action function of a level n rule is invoked only if there was a change in the ulr part that is processed by the routing rule up to level n.
  • Routers understand relative links. More specifically, if the purpose of a link is just to change the selection made by the level n routing rules, all parameters extracted by the n-1 level before are not required in the action method, since their values are inferred automatically. Thus, for instance, if we have a 2 level routing rule where the 0 level section extracts the view while the level 1  section extracts the id of a record to be shown

Basic usage

As a first step we specify a default action function to be invoked each time the current rule match a route:

        
                mvcct.routing.defaultAction(function(obj, level, hasChildren){
                    //function code
                });
         
        

Where:

  • obj contains the parameters extracted by the routing rules. The action function invoked at kevel n of a routing rule is passed all parameters extracted up to level n.
  • level is the level of the routing rule that invoked the action function. Levels are 0 based.
  • hasChildren is true, if and only if the routing rule has children nested rules.

Then we may specify all routing rules:

var router = new mvcct.routing.router()
    .route("#/home/index", { module: "home", view: "index" })
    .prefix("#/nested/:view", { module: "nested", nested: true })
        .route("/:par?", { par: 1 })
        .notFound({ module: "error", view: "nested" })
    .end()
    .route("#/:module?/pref-:view?", { module: "default module", view: "default view", extension: "dotted" })
    .route("#/:module?/:view?.com", { module: "default module", view: "default view", extension: "com" })
    .route("#/:module?/:view?", { module: "default module", view: "default view" })
    .notFound({ module: "error", view: "default" });

important: routing may be started on the initial page url, as soon as the Html page is loaded by calling router.history.refresh();

As a default the first router defined, is assumed to be the main router that is associated with the browser history.  Leaf routes are specified with the route function, while routes with children rules are specified with the prefix function that is closed with the end function. route and prefix, accepts exactly the same parameters:

  1. The string to match. The match string may contain parameters that are alfanumeric strings preceeded by :. Optional parameters are followed by ?. When an option parameter is immediately preceeded by /, the / must be omitted whenever the parameter value is omitted. Parameter names ends as soon as a character that is neither a letter or a number is encountered. Some parameter values may be passed also as query string. Thus for instance, the url #/home/index?id=1 matches the first routing rule, thus defining module="home", and view="index" that are taken by the rule default values, and id=1 that is taken from the query string. Anagously, #/people/customer?id=1 matches the last routing rules(the one immediately before the 0 level notFound), thus defining module="people" and view="customer" that are extracted by matching the rule parameters with the url, and id=1 that is extracted from the query string.
    Match string may contain alfanumeric strings plus the following characters: ?, /, ., -, #, !.
  2. The parameter default values. These values are taken whenever the parameter value isn't extracted by matching the current url with the match string.
  3. An optional action function. If this parameter is not provided the default action function is taken.

The notFound method only accepts the parameter defaults and the optional action function, since it matches all urls. It is used to catch all request with no match.

All rules are evaluated in the same order they were defined till a match is found. In the case of nested rules, the leading part of the url that matched the rule is removed, and the remainder of the string is passed to the nested routing rules. Thus, for instance after that #/nested/main/5 matched the 0 level rule "#/nested/:view", the parameter view=5 is extracted, the action function is invoked, and the string /5 is passed to the "/:par?" children rule. If  all n level rules fail the n level notFound rule is invoked if it is defined, otherwise no further action is performed.

Urls may be inserted either as values of href attributes of anchors, or as values of data-action attributes of any Html node.

The main router, that is connected with the browser history, accepts only urls coming from href attributes that starts with #, since all urls it processes must be visible to the search engines. Actually, for a correct interaction with search engines  these links should start with #!. GoogleBot is now able to run javascript, and to perform ajax calls, but all other search engines require a static html snapshot(see the previous link for more details).

In order to keep the code independent from the match string formats, urls are usually created starting from the parameter values the router should yield:

        
                var url=localRouter.action({module: 'home', view: 'index', id= "1"});
         
        

Or, for the main router, by calling:

        
                var url=mvcct.routing.action({module: 'home', view: 'index', id= "1"});
         
        

The way the above url are inserted into links, depends on actual client framework being used.

Changing the main router

In order to change the main router one need first to reset the routing module by calling:

ko.routing.reset()

Then it is enough to create another router by calling new ko.routing.router(). Any attempt to define a new router without resetting the routing system will define a local router, also if the router is not explicitely declared to be local.

Relative reference

Suppose we have these nested rules:

.prefix("#/nested/:view", { module: "nested", nested: true })
    .route("/:par?", { par: 1 })
    .notFound({ module: "error", view: "nested" })
.end()

Where the level 0 rule selects the View that shows a list of records in a grid, while the par parameter selects which record to show in a detail area. Suppose also that each grid row has a detail link that once clicked shows the record bound to the row in the detail area. How to implement the row link? 

Since each router remembers the context of all parameters of the current route, it is enough to specify the value of the par parameter that selects the record to be shown in the detail area:

        
                var url=mvcct.routing.action({par: id});
         
        

Where id is the property of the model bound to the grid row that contains the record principal key.

The action method is smart enough to build the complete url.

When the user clicks a detail link, since the section of the url that matches "#/nested/:view" doesn't change, the action function of the 0 level view doesn't fire, so the view isn't reloaded. Only the action function associated to "/:par?" fires again and loads the new record in the detail area.

In general, in case of nested rules:

  • The action method needs to specify just the parameters associated to the levels that will change. In the previous example just the par parameter of the level 1 rule, since all level 0 parameters will not change.
  • The action functions are invoked starting from the one of the first level whose matching url section changes. In the previous example, since the section of the url that matches the level 0 rule doesn't change, the first action function invoked is the one of the level 1 matching rule.

Local routers

As a defaul the main router processes all links defined in the page whose urls start with "#", and  all other urls cause a physical page change.

However, we may associate a local router to a specific area of the page by calling the local router attach method:

        
                localRouter.attach(htmlNode);
         
        

Each local router has an associate url prefix: only urls starting with that prefix are processed by the router, all others link clicks are bubbled up till they reach another html element with a local router attached to it whose url prefix is compatible with the link url. if no other local router is intercepted, link clicks are processed by the main router if the url starts with '#', otherwise they cause a physical page change.

If a local router doesn't specify the fakeUrls option all its routing rules must start with '#'. Each url is split in its hash and server url parts. The hash part is processed by the routing rules, while the server url part must match the router prefix, otherwise the request is bubbled up. This limitation exists since local routers without the fakeUrls option must work only with urls that represent actual web resources to be shown into an area of the page. The same url inserted directly in the browser address bar is supposed to render the same resource.

If the fakeUrl option is specified the routing rules may have any format. In this case urls should be inserted in the data-action attribute instead of the href attribute. In fact in this case urls are used just as a way to trigger generic actions in the page, and doesn't have the meaning of actual web resources. Accordingly they are hidden to search engines by placing them in the data-action attribute instead of the href link attribute.

a local router is created with:

new mvcct.routing.router(true, baseUrl, fakeUrls);

The first argument set to true specifies that the router is local, baseUrl is the url prefix associated with the local router, and fakeUrls is the fakeUrl option discussed above.

History & manual routing

All routers have an history property containing an history object that may be used to set manually the router current url and to navigate among past urls. The history object of the main router is connected with the browser history, while the history object of all local routers simulates the back-forward url navigation, and provides some observables that may be connected to some UI to implement a local navigation bar.

The following methods are common to all history objects:

  • set(url, replace, title). Sets url as the current url. If replace is true, the back and forward urls are unaffected, otherwise, as usual, the previous url is pushed in the back stack and the forward stack is cleared. After that the router is invoked on the hash part of the url. If the router has the fakeUrls option set the router is invoked on the whole url. If we pass just the hash part of the url, a complete url is formed by prepending the server part of the current url.
    The title argument is processed only by local router history objects, that handle a virtual document title.
  • back([x]). Moves, x places back in the url history. If x is omitted its default is  1.
  • forward([x]). Moves, x places forward in the url history. If x is omitted its default is 1.
  • go(x). if x<0 it moves x places back in the url history, otherwise if x>0 it moves x places forward in the url history.

The following methods are specific for the main router history object:

  • refresh(). Repeat routing on the current url. Useful to start routing on the initial page url as soon as the Html page is loaded. There is no equivalent method for local routers histories, since for local routers histories the set method triggers routing also when the url doesn't change.

The following methods are specific for the local router history objects.

  • clear(url, title). Clears the whole urls history and sets new url and title.

The following properties are specific  for the local router history objects:

  • url. Contains the current url.
  • title. Contains the current title.
  • canBack. True if the back stack is not empty.
  • canForward. True if the forward stack is not empty.

The above properties may be used to implement an UI for the history navigation.


Fork me on GitHub