Routing

TIBET Routing

wins

  • Flexible URL route parsing with clean semantic path formatting.
  • History/Back/Forward button integration with Route signaling.
  • Parameter-driven route mapping and tokenization without coding.
  • Integration w/TIBET StateMachine for complex application states.
  • Configuration/Feature flag support built in to URL parsing.

concepts

Server-side routing maps an incoming URL request to an appropriate route handler and is a core feature of how many modern server-side web frameworks function.

Client-side routing similarly maps changes to the URL to a handler, however, using routing in your client-side code is not something to undertake lightly.

TIBET provides the URL mapping and client-side persistence APIs needed to implement routes in your applications but we encourage you to be very deliberate in your design.

When you design client-side routes you're really designing bookmarks. Each route presents the user with a URL they can store and trigger directly. This means routes can introduce a lot of complexity in applications that aren't relying on storing all their state on the server.

To fully reproduce a client-side route's state your handler code may have to authenticate the user, restore state, navigate among "screens", or perform other tasks, all while taking into consideration whether the user is running offline. Keep that in mind as you design your routes.

Routing, as we've said, is the process of reacting to changes in the URL and responding appropriately. Let's take a look at the format of a URL to see how this plays out in practice.

URL Components

A standard HTTP(S) scheme URL has the following basic structure:

scheme://host[:port]/path[?params][#fragment]

Browsers never send the fragment portion to the server, it's specifically reserved for the client. As a result the URL fragment has traditionally been used to drive client-side functionality via onhashchange notifications etc.

While it is possible to change the path portion of a URL via the pushState call in HTML5 browsers, we don't recommend overloading the server portion of the URL with client-focused bookmarks since it blurs server and client responsibilities.

Bookmarks which leverage a "synthetic" path may trigger 404s when the user accesses them since the server may not know the path. Avoiding this problem requires the server to handle every possible synthetic route, increasing maintenance overhead and server dependencies.

Instead of synthetic path manipulation we recommend you use the URL fragment which keeps reponsibilities clearly partitioned in accordance with the URL standard.

That said, there's no formal specification for the format of a URL fragment (outside of XPointer). That opens the possibility of treating the fragment as a path[?params] block itself:

scheme://host[:port]/path[?params][#[fragment_path][?boot_params]]

In many cases you can simplify that URL dramatically, reducing it to:

scheme://host/path#fragment_path

Regardless of whether you're using fragment-based client URLs or the more recent HTML5 history.pushState approach, TIBET looks at the URL components in a consistent fashion as described below.

Base Components

Scheme

Scheme is a fixed aspect of the URL, even with history.pushState so we do not use scheme values in the computation of client-side routes.

Host

Host (domain) is a fixed aspect of the URL, even with history.pushState so we do not use host/domain values in the computation of client-side routes.

Port

Port is a fixed aspect of the URL, even with history.pushState so we do not use port values in the computation of client-side routes.

Path

If you're using history.pushState you can manipulate the path portion of your application launch URL to be any value you like. We don't recommend this practice for TIBET.

For your TIBET code we recommend you rely solely on aspects of the URL fragment since that portion of the URL is never sent to the server and keeps the semantic meaning clearer.

Parameters

Explicit parameters for a server request are provided by adding ? and the desired key/value pairs to the end of the path component:

http://example.com/help?t=foo       => Help params: {t: 'foo'}

NOTE that these "legacy URL" forms are not routed by TIBET. Parameters in the server portion of the URL are never used by the TIBET client.

Fragment Components

The URL fragment, often referred to as the "hash" (officially an 'octothorpe'), is the recommended control point for driving client-side routing activity in TIBET.

In the context of routing TIBET treats the fragment as a path[?params] section. In other words, TIBET splits the fragment at the first ? as if it were a standard URL path[?params] segment.

Any portion in front of the first ? in the fragment is referred to as the "fragment path". Any portion after ? is referred to as the "fragment parameters".

URLs containing "path-only" fragments follow a form similar to:

http://127.0.0.1:1407#path
http://127.0.0.1:1407#/path
http://127.0.0.1:1407#path/path
http://127.0.0.1:1407#/path/path

URLs containing "parameter-only" fragments look like this:

http://127.0.0.1:1407#?param
http://127.0.0.1:1407#?param=value
http://127.0.0.1:1407#?param=value&param2=value

Combining both fragment path and parameter values gives you URLs of the form:

http://127.0.0.1:1407#path?param=value
http://127.0.0.1:1407#/path?param=value
http://127.0.0.1:1407#path/path?param=value&param2=value
http://127.0.0.1:1407#/path/path?param=value&param2=value

Fragment Paths

Like other routing solutions, TIBET allows you to map different fragment path patterns to a handler. The syntax of these mapping patterns is similar to that found in Rails, which pioneered much of modern web URL routing.

Explicit portions of a URL path can be named explicitly. Parameterized portions of the path typically use "tokens", values which start with a colon.

For example, a mapping for http://foo.example.com/authors/11/lname might be:

/authors/:authorId/lname

A route matching the tokenized pattern above will parse the route and create a parameter named authorId whose value is 11 for use by any handler.

We'll cover TIBET's URL mapping syntax and handler invocation in detail in a moment but first let's take a look at fragment parameters.

Fragment Parameters

Fragment parameters are a concept based on treating the fragment as a kind of URL in itself, one that can have its own path and parameter segments split by ?.

Fragment parameters are never used for routing by TIBET. We only route based on changes to the "clean" or "semantic" URL fragment path segment.

Fragment parameters are reserved for use during TIBET startup. Parameters on the fragment configure the application's configuration variables during initial startup. This lets you easily support feature flags and other configuration-driven behavior during development.

For example, you can turn on trace-level logging during startup using a fragment parameter of boot.level as shown below:

http://127.0.0.1:1407#?boot.level=trace

In the URL above the # starts the fragment and the ? starts the fragment parameter section. Additional parameters are separated with & just as with standard URL parameters.

http://127.0.0.1:1407#?boot.level=trace&fluffy

Note that fluffy above has no ={value} portion. This shorthand is used to define a true value for any flag. The URL above sets the boot log level to trace and fluffy to true.

Production configurations never parse boot parameters on the URL so you can normally rely on just a clean semantic path being presented to users. Setting flags for production is done entirely through configuration files or settings within the code of your production package.

Routing Triggers

URL changes which can impact a TIBET route are limited to the path and the fragment path. These two segments of the URL can be changed either by the user or by your code via HTML5's pushState call.

Since both hashchange and popstate events can be used to trigger routing, TIBET provides a configuration variable, uri.route, which takes either hashchange or popstate to define which event to rely on.

By default TIBET routes hashchange notifications and ignores popstate for routing. This default is based on our recommendation that you keep client routes and bookmarks restricted to client-only portions of the URL.

TIBET's low-level handling of routing events is managed by the TP.core.History type. This type is the default handler for all history-related activity and is available via TP.sys.getHistory() or from the application via getHistory().

When hashchange or popstate events occur TIBET will automatically invoke the current application's router to handle the routing process. You can also trigger routing by signaling LocationChange or by invoking the router's route() call.

The Router

All TIBET applications provide a default router via TP.sys.getRouter() or from the application via getRouter().

The default router is TP.core.URIRouter, a type which handles route definition and resolution processing using an approach that helps simplify route definition and keep route handler code organized efficiently.

While you can define your own router it's not recommended for most projects. The discussions which follow regarding route definition and route resolution describe the TP.core.URIRouter version of routing.

Route Signals

In typical routing solutions you map a pattern to a function, usually in an order-dependent fashion. The first pattern matched determines the function to invoke and any tokenized portions of the pattern are provided as parameters.

TIBET manages routes a little differently. Instead of invoking handler functions directly TIBET converts route matches into signal names and fires those signals.

For example, by default, the route / is mapped to the concept of 'Home' in TIBET by default through the route.root configuration flag value.

Any time the router detects the route has changed to / it will signal 'HomeRoute'. This signal can be handled by any observer in TIBET but it will normally be handled by the Application instance for the application:

APP.hello.Application.Inst.defineHandler('HomeRoute', function(aSignal) {
    APP.trace('HomeRoute signaled');
    //  do the right thing to reset app to the home page ui etc.
});

To be more explicit, when route() is invoked it actually runs the list of explicitly-defined route patterns, takes all of the routes which matched, sorts them using TP.uri.URIRouter's BEST_ROUTE_SORT by default, and then selects the one the sort function prioritizes.

If no explicitly-defined route pattern match is found TIBET takes the path and generates a signal name from it automatically. The path /foo/bar will result is a signal name of FooBarRoute and so on, meaning you don't have to define route/signal mappings for common paths.

Once a route is chosen, TIBET fires the matching signal with a payload that includes any token values from the route pattern. This approach blends routing with TIBET signal handling.

The advantage of tying routing to signal handling is that you can leverage the responder chain, inheritance and state-sensitive handler lookups, signal-driven route testing, and all the other incredible features of TIBET Signaling in support of your application routes.

Route Definition (via Config)

You can express interest in a route using TIBET configuration parameters, in particular the route.paths configuration parameter.

Here's a simple example:

"route": {
    "paths": {
        "/foo/:id/bar": "SignalMe"
    }
}

You can define as many paths as you like. Each will be processed through the definePath method as described in the next cookbook item.

Note that the second value is used as the route name and the explicit signal name unless otherwise defined. You can remap a signal name using the following syntax:

"route": {
    "paths": {
        "/foo/:id/bar": "SignalMe"
    },
    "map": {
        "SignalMe": {
            "signal": "NoNoSignalMe"
        }
    }
}

There are a number of other parameters you can set for a specific route. See the section on Route Maps for more information.

Route Definition (via Code)

Interest in a URL path is expressed using the definePath router method.

Assume we want to process routes of the form:

http://127.0.0.1:1407#all

TIBET can handle that route by default, but let's assume it couldn't. We define our interest in that route as follows:

TP.sys.getRouter().definePath('/all');

// or, if we want to specify a custom signal name...

TP.sys.getRouter().definePath('/all', 'SignalThis');

In both examples you might be asking "Where's the handler function?"

The answer is "Where you'd expect it to be for a signal named 'AllRoute'" (or 'SignalThis').

Since routing in TIBET is effectively signaling you can manage the handlers in the same place you handle all other signal responders, somewhere in the responder chain.

For simple applications that normally means the application controller itself:

APP.hello.Application.Inst.defineHandler('AllRoute',
function(aSignal) {
    APP.info('handling ' + aSignal.getSignalName());
});

Leveraging the responder chain and signaling means your route handler functions stay organized along with all your other event handlers. It also means you can trigger them simply by signaling the appropriate RouteChange signal.

Default Routes

TIBET's default route definitions can often handle simple routing patterns without the need for you to do anything but define a signal handler.

Let's look at a few examples of default route usage.

Root Routes

The default route definitions are designed to handle "root" routes, eg. routes without any path value, by mapping them to a signal name based on the value of the router's 'root' property, which defaults to route.root or 'Home'.

Any time the URL path changes to an empty value a signal for {root}Route will be fired. Since this is built in you can handle root route requests in TIBET simply by defining a handler for that signal.

APP.hello.Application.Inst.defineHandler('HomeRoute',
function(aSignal) {
    APP.info('handling ' + aSignal.getSignalName());
});

If we want to tie our home route to 'All' instead of 'Home' we can do that by changing the root value for the router via configuration values:

APP.hello.Application.defineMethod('initialize', function() {
    TP.sys.setcfg('route.root', 'All');
});

You can also use a simple configuration parameter setting in tibet.json:

"route": {
    "root": "All"
}

Once you remap route.root you can set up a handler to match the new signal:

APP.hello.Application.Inst.defineHandler('AllRoute',
function(aSignal) {
    APP.info('handling ' + aSignal.getName());
});

Note however that definePath and setting the route.root have slightly different results. When you use definePath any signal name you provide is used exactly as is. So definePath('/', 'All') signals 'All' while using a route.root of 'All' signals 'AllRoute'.

Path Routes

For URLs with a valid path value TIBET will automatically convert the path into a camel-cased signal name, extracting any segments which are not valid JS identifier values and treating them like parameters.

For example:

http://127.0.0.1:1407#all/the/things

will signal AllTheThingsRoute, while:

http://127.0.0.1:1407#all/23/things

will signal 'AllThingsRoute' and give it a payload containing a parameter named 'arg0' with a value of 23.

Keep in mind this process happens without any need to actually define the path in question. TIBET simply takes the path apart and assumes any valid JS identifier values are part of the signal name and anything else is a parameter.

Custom Routes

When a default route isn't enough TIBET gives you several options for defining specific route patterns. The simplest of these leverages built-in support for routing "tokens", path segments prefixed with a colon much like Rails.

Tokenized Routes

TIBET supports simple tokenized paths via definePath. Any token names in the path are treated as parameter names for the values which match those segments.

TP.sys.getRouter().definePath('/all/:count/things');

With this route definition in place http://127.0.0.1:1407#all/23/things will still signal AllThingsRoute, but now the parameter name will be count rather than arg0. Note however that the value of count will be a string, not a number due to the nature of regular expression matching.

Token Patterns

If you have a token you want to use that requires special pattern processing you can define that using the defineToken method.

For example, if we want to force our :count token from the previous example to be numeric we might define the following token pattern:

TP.sys.getRouter().defineToken('count', /\d+/);

With the token definition above our route of /all/:count/things will only match if the value for count matches the regular expression we've defined for that token but the result will be the parsed value, a number, when it does match.

As with other routing-related definitions you can define tokens in tibet.json:

"route": {
    "tokens": {
        "formid": "/[a-zA-Z0-9]+/",
        "submissionid": "/[a-zA-Z0-9]+/"
    }
}

Note that tokens are a top-level construct you can reuse across all of your path definitions.

Path Expressions

You can use defineToken to create regular expression "segments" but you can also define an entire path as a regular expression:

TP.sys.getRouter().definePath(/\/(.+)\/(\d+)/);

In cases where you're using regular expressions each captured portion will be assigned to arg{N} where N is the index of that capturing block.

For the path above that means our first match will be arg0 and the one-or-more-digits portion of the match will be named arg1.

If none of the approaches presented so far is sufficient you can provide your own pattern-to-signal conversion function.

Pattern Processing

You can change how URLs are converted into signals by provided your own pattern processing function to definePath.

For example, you might decide any path that's just a number is a "global navigation ID" which should trigger navigation to a specific screen in an application of dozens or more screens. (We had a customer use this approach for an application where their users knew the 3-digit codes for instantly jumping to a desired target screen out of hundreds of possible screens).

Let's define a path and match processing function for global navigation:

TP.sys.getRouter().definePath(/^\/(\d+)$/,
function(path, match, names) {
    return TP.ac('GoToScreen', TP.hc('screen', match[1]));
});

In the example above we've defined a pattern consisting of a leading / for the path followed by one or more digits. When the path matches this pattern our function will be invoked to return signal and parameter data.

The function we provide for pattern processing is passed the original path, the results of running the route's regular expression match() call, and a list of any token names found while processing the original path.

In our example's case we don't have tokens so we can't name the parameter and there's no text to help us define a signal name to fire. Our best choice is to custom-build the signal/payload pair.

Our single-line implementation returns a signal name of GoToScreen and names the first match value screen since we want handlers to be provided with the screen number to navigate to.

The handler function itself might be defined as:

APP.hello.Application.Inst.defineHandler('GoToScreen',
function(aSignal) {
    TP.info('handling: ' + aSignal.getSignalName() + ' screen: ' +
        aSignal.at('screen'));
});

Signal Processing

There's nothing "special" about how TIBET processes routing signals but knowing how they are constructed can help you do some interesting things as you define your routes and handlers.

The match-handing function for a path is intended to return an ordered pair containing a signal type name and a hash containing signal payload parameters.

TIBET takes the signal type name and attempts to find that type and construct an instance of it. If the name doesn't represent a unique type then TIBET creates an instance of RouteFinalize and sets the signal name instead.

RouteFinalize signals, like all other Change signals in TIBET, are fired using what we call inheritance firing, a dispatch policy which traverses the inheritance chain of the signal looking for the best handler match.

An interesting implication is that you can create your own RouteFinalize signal hierarchy and implement handlers at whatever levels make sense for your application giving you either chaining or fallback processing as needed.

Why RouteFinalize though? What's the 'finalize' part for? Well, it turns out TIBET actually signals 3 times for routes.

When a route is about to change TIBET signals a RouteExit signal for the route about to be left. Next TIBET will signal a RouteEnter, and eventually a RouteFinalize letting the system know all exit/enter processing is in place.

The exit and enter variations are useful for ensuring things setup and teardown cleanly when changing routes. Simple routes can rely on RouteFinalize subtypes.

State Machines

As you may know, TIBET includes a full implementation of a signal-driven State Machine. This allows you to manage functionality within your application based on application states…and one of the more prominent "states" in a web application is "the current route".

The default implementation of your Application object's RouteFinalize handler will check for any application state machine. If you've assigned a state machine to your application that handler will automatically check to see if, given the current state of your state machine, it can transition to a new state which matches the route name. If so that state transition will be triggered.

By letting you implement your route-change logic as part of your application's state machine you can be sure that logic stays centralized and modularized.

Route Maps

As some of the previous sections have shown, you can do most of your route definition work directly in the tibet.json file for your project without having to write explicit code.

Here's the full description of what you can put in a route entry:

"route": {
    "controller": "a single controller type name",
    "paths": {
        "pattern": "route/signal name",
        ...
    },
    "tokens": {
        "name": "properly escaped regular expression string",
        ...
    },
    "map" : {
        ": {
            "signal": "theSignalNameToFire",
            "content": "string/tagname/url to set in 'target'",
            "target": "urlOrElementIdOfTargetElement",
            "reroute": "someRouteName",
            "redirect": "someURL"
            "deeproot": "trueIfRouteShouldNotRedirectHome",
            "controller": "aRouteControllerTypeName"
        },
        ...
    }
}

Each of the settings in a route map entry can be used to help configure how TIBET responds to your route change.

You can map a specific signal name to the route. You can map a specific controller type to handle that route. You can tell the system to redirect or "reroute" when this route is triggered. You can tell the system not to redirect to / but treat the route as a valid deep link. Finally, you can tell TIBET you want to put a particular bit of content into a particular element.

This latter feature can be particularly nice in reducing route-handling code.

For example, if you set up an xctrls:panelbox with an ID you might use content and target to change what you see based on route changes. You can tell each route to render a particular tag when the route changes:

"route": {
    "map": {
        "Page1": {
            "target": "panelbox",
            "content": "<app:page1/>"
        },
        "Page2": {
            "target": "panelbox",
            "content": "<app:page2/>"
        },
        ...
    }
}

Using the approach above, coupled with custom route controllers per page, you can easily have TIBET configure the proper content and controller stack in response to changes to the URL.

cookbook

Define A Route

Defining a route is something you typically only need to do if a) your route requires specialized parsing of the URL (non-string values or special patterns) or b) you want to define a custom signal name to an otherwise-default route.

To define a route use the definePath method of the TP.core.Router type. You'll want to get a handle to the current router instance first by using TP.sys.getRouter() and then you can message it to define your route path and optional signal:

TP.sys.getRouter().definePath('/all');

// or, if we want to specify a custom signal name...

TP.sys.getRouter().definePath('/all', 'SignalThis');

That's all there is to it.

Define A Token

When you're going to require custom parsing of a part of the URL you can use defineToken to essentially create a named regular expression:

router = TP.sys.getRouter();

router.defineToken('fluffy', /\d{3}/);

Once you've defined a token you can reference it in paths by prefixing it with a colon (:):

Define A Tokenized Route

To define a tokenized route first be sure you've defined your token using defineToken, then use definePath to create a path and reference your token(s) within that path by prefixing each one with a colon (:):

router = TP.sys.getRouter();
router.defineToken('fluffy', /\d{3}/);
router.definePath('/foo/:fluffy/bar', 'SignalMe');

When the 'SignalMe' signal arrives it will contain a 'fluffy' parameter whose value is set to the numerical result parsed by the token's regular expression.

Handle Home Route

The 'Home' route is a default route triggered whenever your application lands on /.

The actual name of the route in this case is defined by the TIBET configuration parameter route.root whose default value is Home.

To handle the 'Home' route you typically define a signal handler on your application instance for the 'HomeRoute' signal. If you changed route.root to some over value you'd set your handler up for {value}Route instead:

APP.hello.Application.Inst.defineHandler('HomeRoute',
function(aSignal) {
    APP.info('handling ' + aSignal.getSignalName());
});

Handle Custom Route

Handling custom route changes is as easy as handling any other signal. Just define a signal handler that will be on the responder chain when the route is triggered.

APP.hello.Application.Inst.defineHandler('MyFavoriteRoute',
function(aSignal) {
    APP.info('handling ' + aSignal.getSignalName());
});

code

The "router" TP.uri.URIRouter is defined in ~lib/src/tibet/kernel/TIBETURITypes.js

The RouteChange-related signals are in ~lib/src/tibet/kernel/TIBETNotification.js

TP.core.RouteController is defined in ~/lib/src/tibet/kernel/TIBETWorkflowTypes.js. This type defines the common handlers for certain route signals and is the default type for all route controllers managed by the TIBET responder chain logic.

Routing tests are found in ~lib/test/src/tibet/routing.