Routing

The routing service provides a flexible, hierarchical routing mechanism for managing a tree-like application organisation. It follows the same principles as React, but benefits from some customisation. The routing is based on the URL e.g. set the URL for the required application state and the service will update the state accordingly. This means that changing the route by clicking on a link has the same effect as changing the URL manually, which makes bookmarking a state in the application trivial.

The routing service can be used from pure JavaScript and it also has React bindings.

Initialising

Routing needs to be initialised, typically in the index.js of your application. Both the way the routing is tracked and the setting up of the root scope (the index of the application) needs to be initialised.

There are 2 ways of tracking that are provided:

  • hash tracking (using the # part of the URL)

  • history tracking (using the browser’s history API)

The following example shows how to initialise ‘hash tracking’ using ES6, JSX and typescript:

'use strict';

import * as React from 'react';
import * as ReactDom from 'react-dom';
import { RouteService, Router } from 'ix-tools-ux-core';
import { Index } from './index/Index.react';

RouteService.initHashTracking();
// initialise route tracking using the URL hash

const div = document.createElement("div");
document.body.appendChild(div);
// create a div to host the react app and append to body

ReactDom.render(<Router.Scope route={ RouteService.root }><Index /></Router.Scope>, div);
// render the route scope as the root component, with the app index as its child

Defining Routes

Routes are defined hierarchically using <Router.Routes>. The defined routes are added as children to the container route which means that processes are self-contained and changing the entry point URL to a process does not require changes to the process (assuming only relative URLs are used).

To avoid ambiguity and prevent bugs (caused by incorrect route ordering), routes are sorted. Longer paths are ordered before shorter paths and static paths are ordered after parameterised paths. An empty path is always the last to be matched. This ensures that longer paths are never overshadowed by short paths. For example, /a/b/c cannot be overshadowed by /a/b.

Parameters will never take the value of a static path e.g. /a/:id to /a/b/ (so the parameter id can never have a value of ‘b’).

Example

This scenario shows a list that can be filtered and allows drilling down to a specific item in the list or to add another item. The routes are as follows:

  • []: empty route (the index route and the entry point to the list of items)
  • [/add]: this the route to add an item
  • [/:id]: this is the route to show a specific item with the given id. The details view has three nested tabs, each with their own child route:
    • []: tab 0 (first tab)
    • [/1]: tab 1 (second tab)
    • [/2]: tab 2 (third tab)

Example code (below) for CollectionContainer, sets up the first level routes that are described above:

'use strict';

import * as React from 'react';
import { Component, Router } from 'ix-tools-ux-core';
import { CollectionList from './CollectionList.react';
import CollectionDetails from './CollectionDetails.react';
import CollectionAdd from './CollectionAdd.react';

@Router.Decorator()
@Component.Decorator({ pure: true })
export default class CollectionContainer extends React.Component<{}, {}> {

    constructor(props?: {}, context?: any) {
        super(props, context);
    }

    render() {
        return (
        <Router.Routes>
            <Router.Route path='' elementSupplier={
                (params, query) => <CollectionList filter={ query && JSON.parse(query) } />
            } />
            // the above line sets up the index route (empty string), which matches
            // if no other route matches the elementProvider property is a function
            // that return the element to be rendered given the params and query in
            // the URL. This function is only called when the route matches; the
            // params object will be populated with the URL parameters (if any) and
            // the query string will contain the URL query (the string following the
            // ?, if any). In this case, the params object will be empty, and the
            // query string is parsed as a JSON object and passed to the element. In
            // a real application, there should be error handling for non JSON syntax
            <Router.Route path=':id' elementSupplier={
                (params) => <CollectionDetails id={ params.id } />
            } />

            // the above line sets up the parametrised id path and uses the
            // elementProvider property to supply an element with the id property set
            // from the URL params. Note that in this case, the id can never be an
            // empty string, since we are matching the empty path. If the previous
            // route was not defined, then an empty path would match and the id
            // parameter would be an empty string
            <Router.Route path='add'><CollectionAdd /></Router.Route>

            // the above line sets up the add route. Note that since no parameters
            // need to be passed, we can opt to provide the element as a child instead
            // of through the elementProvider property, making the definition slightly
            // less verbose. In this mode, a params and query property are still set on
            // the element by the router, so the previous routes could have been written
            // in this way if the components are aware of these properties
        </Router.Routes>
        );
    }
}

And here is the code for CollectionList:

'use strict';

import * as React from 'react';
import { Component, Router } from 'ix-tools-ux-core';

export interface CollectionListProps {
    filter?: any
}

@Router.Decorator()
@Component.Decorator({ pure: true })
export default class CollectionList extends React.Component<CollectionListProps, {}> {

    constructor(props?: CollectionListProps, context?: any) {
        super(props, context);
    }

    render() {
        return (
        <div>
            filter: { this.props.filter }
            // the filter prop will have beenset by the parent route
            // in a real application, this would be used to do some backend call fetching
            // a filtered list
            <ul>
                <li><Router.Link href="1">Item 1</Router.Link></li>
                <li><Router.Link href="2">Item 2</Router.Link></li>
                <li><Router.Link href="3">Item 3</Router.Link></li>
                <li><Router.Link href="4">Item 4</Router.Link></li>
                // the above lines create a relative link to each item in the list

                <Router.Link href="add">Add</Router.Link>
                // the above line creates a relative link to the add route
            </ul>
        </div>
        );
    }

}

And for CollectionAdd:

'use strict';

import * as React from 'react';
import { Component, Router } from 'ix-tools-ux-core';

@Router.Decorator()
@Component.Decorator({ pure: true })
export default class CollectionAdd extends React.Component<{}, {}> {

    constructor(props?: {}, context?: any) {
        super(props, context);
    }

    render() {
        return (
        <div>ADD ITEM<Router.Link href="..">Back to List</Router.Link></div>;
        // the above line defines a relative link to the parent
        // in a real application, this would be the form to add an item, with
        // interaction with a backend to store the item
    }

}

The code for CollectionDetails including nested routes for tabs:

'use strict';

import * as React from 'react';
import { Component, Template, Router } from 'ix-tools-ux-core';

export interface CollectionDetailsProps {
    id: string
}

@Router.Decorator()
@Component.Decorator({ pure: true, bindPrefixes: ['_on'] })
export default class CollectionDetails
  extends React.Component<CollectionDetailsProps, { tab: number }> {

    constructor(props?: CollectionDetailsProps, context?: any) {
        super(props, context);
        this.state = { tab: 0 };
    }

    render() {
        return (
        <div>ITEM { this.props.id } <Router.Link href="..">Back to List</Router.Link>

            <Router.Link href=''>No Tab</Router.Link>
            <Router.Link href='tab1'>Tab1</Router.Link>
            <Router.Link href='tab2'>Tab2</Router.Link>
            // the above are relative links to the tabs

            <Router.Routes>
                <Router.Route path='' inHandler={ this._onTab0 } />
                <Router.Route path='tab1' inHandler={ this._onTab1 } />
                <Router.Route path='tab2' inHandler={ this._onTab2 } />
                // The above routes match the tabs as in the example above.
                // The inHandler property can be used in place of either the
                // elementSupplier or the child in cases where the route does
                // not equate to rendering a component, or more logic is involved
                <Router.Otherwise redirect='' />
                // the above sets up an otherwise route, which matches anything
                // not matched by the other routes. In this case, it redirects
                // back to the first tab
            </Router.Routes>

            { Template.If(this.state.tab == 0, () => <CollectionTab0 />) }
            { Template.If(this.state.tab == 1, () => <CollectionTab1 />) }
            { Template.If(this.state.tab == 2, () => <CollectionTab2 />) }
            // the above is the logic to show the appropriate tab based on state
        </div>
        );
    }

    _onTab0() {
        this.setState({ tab: 0 });
    }

    _onTab1() {
        this.setState({ tab: 1 });
    }

    _onTab2() {
        this.setState({ tab: 2 });
    }

}

// the below are stateless components for rendering the tabs
function CollectionTab0() {
    return <span>Tab 0</span>
}

function CollectionTab1() {
    return <span>Tab 1</span>
}

function CollectionTab2() {
    return <span>Tab 2</span>
}