Giter VIP home page Giter VIP logo

routing's Introduction

The @dojo/routing repository has been deprecated and merged into @dojo/framework

You can read more about this change on our blog. We will continue providing patches for routing and other Dojo 2 repositories, and a CLI migration tool is available to aid in migrating projects from v2 to v3.


@dojo/routing

Build Status codecov.io npm version

A routing library for Dojo 2 applications.

Usage

To use @dojo/routing, install the package along with its required peer dependencies:

npm install @dojo/routing

# peer dependencies
npm install @dojo/core
npm install @dojo/has
npm install @dojo/shim
npm install @dojo/widget-core

Features

Widgets are a fundamental concept for any Dojo 2 application and as such Dojo 2 Routing provides a collection of components that integrate directly with existing widgets within an application. These components enable widgets to be registered against a route without requiring any knowledge of the Router. Routing in a Dojo 2 application consists of:

  • Outlet widget wrappers that are assigned a specific outlet key and represent the view for a specific route
  • a configuration of individual Routes that map paths to outlet keys
  • a Router that resolves a Route based on the current path
  • a History provider that notifies the Router of path changes
  • a Registry that injects the Router into the widget ecosystem

Route Configuration

Application routes are registered using a RouteConfig, which defines a route's path, the associated outlet, and nested child RouteConfigs. The full routes are recursively constructed from the nested route structure.

Example routing configuration:

import { RouteConfig } from '@dojo/routing/interfaces';

const config: RouteConfig[] = [
	{
		path: 'foo',
		outlet: 'root',
		children: [
			{
				path: 'bar',
				outlet: 'bar'
			},
			{
				path: 'baz',
				outlet: 'baz',
				children: [
					{
						path: 'qux',
						outlet: 'qux',
					}
				]
			}
		]
	}
]

This configuration would register the following routes and outlets:

Route Outlet
/foo root
/foo/bar bar
/foo/baz baz
/foo/baz/qux qux

Path Parameters

Path parameters can be defined in a path using curly braces in the path attribute of a RouteConfig. Parameters will match any segment and the value of that segment is made available to matching outlets via the mapParams Outlet options. The parameters provided to child outlets will include any parameters from matching parent routes.

const config = [
	{
		path: 'foo/{foo}',
		outlet: 'foo'
	}
]

For routes with path parameters, a map of default params can be specified for each route. These parameters are used as a fallback when generating a link from an outlet without specifying parameters, or when parameters do not exist in the current route.

const config = [
	{
		path: 'foo/{foo}',
		outlet: 'foo',
		defaultParams: {
			foo: 'bar'
		}
	}
]

A default route can be specified using the optional configuration property defaultRoute, which will be used if the current route does not match a registered route.

const config = [
	{
		path: 'foo/{foo}',
		outlet: 'foo',
		defaultRoute: true
	}
]

Callbacks for onEnter and onExit can be set on the route configuration, these callbacks get called when an outlet is entered and exited.

const config = [
	{
		path: 'foo/{foo}',
		outlet: 'foo',
		onEnter: () => {
			console.log('outlet foo entered');
		},
		onExit: () => {
			console.log('outlet foo exited');
		}
	}
]

Router

A Router registers a route configuration which is passed to the router on construction:

const router = new Router(config);

The router will automatically be registered with a HashHistory history manager. This can be overridden by passing a different history manager as the second parameter.

import { MemoryHistory } from '@dojo/routing/MemoryHistory';

const router = new Router(config, MemoryHistory);

Once the router has been created with the application route configuration, it needs to be made available to all the components within your application. This is done using a Registry from @dojo/widget-core/Registry and defining an Injector that contains the router instance as the payload. This Injector is defined using a known key, by default the key is router but this can be overridden if desired.

import { Registry } from '@dojo/widget-core/Registry';
import { Injector } from '@dojo/widget-core/Injector';

const registry = new Registry();

// Assuming we have the router instance available
registry.defineInjector('router', new Injector(router));

Finally, the registry needs to be made available to all widgets within the application by setting it as a property to the application's top-level Projector instance.

const projector = new Projector();
projector.setProperties({ registry });

History Managers

Routing comes with three history managers for monitoring and changing the navigation state, HashHistory, StateHistory and MemoryHistory. By default the HashHistory is used, however, this can be overridden by passing a different HistoryManager when creating the Router.

const router = new Router(config, MemoryHistory);
Hash History

The hash-based manager uses the fragment identifier to store navigation state and is the default manager used within @dojo/routing.

import { Router } from '@dojo/routing/Router';
import { HashHistory } from '@dojo/routing/history/HashHistory';

const router = new Router(config, HashHistory);

The history manager has current getter, set(path: string) and prefix(path: string) APIs. The HashHistory class assumes the global object is a browser window object, but an explicit object can be provided. The manager uses window.location.hash and adds an event listener for the hashchange event. The current getter returns the current path, without a # prefix.

State History

The state history uses the browser's history API, pushState() and replaceState(), to add or modify history entries. The state history manager requires server-side support to work effectively.

Memory History

The MemoryHistory does not rely on any browser API but keeps its own internal path state. It should not be used in production applications but is useful for testing routing.

import { Router } from '@dojo/routing/Router';
import { MemoryHistory } from '@dojo/routing/history/MemoryHistory';

const router = new Router(config, MemoryHistory);

Router Context Injection

The RouterInjector module exports a helper function, registerRouterInjector, that combines the instantiation of a Router instance, registering route configuration and defining injector in the provided registry. The router instance is returned.

import { Registry } from '@dojo/widget-core/Registry';
import { registerRouterInjector } from '@dojo/routing/RoutingInjector';

const registry = new Registry();
const router = registerRouterInjector(config, registry);

The defaults can be overridden using RouterInjectorOptions:

import { Registry } from '@dojo/widget-core/Registry';
import { registerRouterInjector } from '@dojo/routing/RoutingInjector';
import { MemoryHistory } from './history/MemoryHistory';

const registry = new Registry();
const history = new MemoryHistory();

const router = registerRouterInjector(config, registry, { history, key: 'custom-router-key' });

Outlets

The primary concept for the routing integration is an outlet, a unique identifier associated with the registered application route. Dojo 2 Widgets can then be configured with these outlet identifiers using the Outlet higher order component. Outlet returns a new widget that can be used like any other widget within a render method, e.g. w(MyFooOutlet, { }).

Properties can be passed to an Outlet widget in the same way as if the original widget was being used. However, all properties are made optional to allow the properties to be injected using the mapParams function described below.

The number of widgets that can be mapped to a single outlet identifier is not restricted. All configured widgets for a single outlet will be rendered when the route associated to the outlet is matched by the router and the outlets are part of the current widget hierarchy.

The following example configures a stateless widget with an outlet called foo. The resulting FooOutlet can be used in a widgets render in the same way as any other Dojo 2 Widget.

import { Outlet } from '@dojo/routing/Outlet';
import { MyViewWidget } from './MyViewWidget';

const FooOutlet = Outlet(MyViewWidget, 'foo');

Example usage of FooOutlet, where the widget will only be rendered when the route registered against outlet foo is matched.

class App extends WidgetBase {
	protected render(): DNode {
		return v('div', [
			w(FooOutlet, {})
		]);
	}
}

Outlet Component Types

When registering an outlet a different widget can be configured for each match type of a route:

Type Description
index This is an exact match for the registered route. E.g. Navigating to foo/bar with a registered route foo/bar.
main Any match other than an index match, for example, foo/bar would partially match foo/bar/qux, but only if foo/bar/qux was also a registered route. Otherwise, it would be an ERROR match.
error When a partial match occurs but there is no match for the next section of the route.

To do this, instead of passing a widget as the first argument to the Outlet, use the OutletComponents object.

import { MyViewWidget, MyErrorWidget } from './MyWidgets';

const fooWidgets: OutletComponents = {
	main: MyViewWidget,
	error: MyErrorWidget
};

const FooOutlet = Outlet(fooWidgets, 'foo');

It is important to note that a widget registered against match type error will not be used if the outlet also has a widget registered for match type index.

Outlet Options

Outlet Options of mapParams, onEnter, onExit, and key can be passed as an optional third argument to an Outlet.

Map Parameters

When a widget is configured for an outlet it is possible to provide a callback function that is used to inject properties that will be available during render lifecycle of the widget.

mapParams(type: 'error | index | main', location: string, params: {[key: string]: any}, router: Router)
Argument Description
type The MatchType that caused the outlet to render
params Key/Value object of the params that were parsed from the matched route
router The router instance that can be used to provide functions that go to other routes/outlets

The following example uses mapParams to inject an onClose function that will go to the route registered against the other-outlet route and id property extracted from params in the MyViewWidget properties:

const mapParams = (options: MapParamsOptions) {
	const { type, params, router } = options;

	return {
		onClose() {
			// This creates a link for another outlet and sets the path
			router.setPath(router.link('other-outlet'));
		},
		id: params.id
	}
}

const FooOutlet = Outlet(MyViewWidget, 'foo', { mapParams });
Key

The key is the identifier used to locate the router from the registry, throughout the routing library this defaults to router.

Global Error Outlet

Whenever a match type of error is registered a global outlet is automatically added to the matched outlets called errorOutlet. This outlet can be used to render a widget for any unknown routes.

const ErrorOutlet = Outlet(ErrorWidget, 'errorOutlet');

Link

The Link component is a wrapper around an a DOM element that enables consumers to specify an outlet to create a link to. It is also possible to use a static route by setting the isOutlet property to false.

If the generated link requires specific path or query parameters that are not in the route, they can be passed via the params property.

import { Link } from '@dojo/routing/Link';

render() {
	return v('div', [
		w(Link, { to: 'foo', params: { foo: 'bar' }}, [ 'Link Text' ]),
		w(Link, { to: '#/static-route', isOutlet: false, [ 'Other Link Text' ])
	]);
}

All the standard VNodeProperties are available for the Link component as they would be creating an a DOM Element using v() with @dojo/widget-core.

How do I contribute?

We appreciate your interest! Please see the Dojo 2 Meta Repository for the Contributing Guidelines.

Code Style

This repository uses prettier for code styling rules and formatting. A pre-commit hook is installed automatically and configured to run prettier against all staged files as per the configuration in the project's package.json.

An additional npm script to run prettier (with write set to true) against all src and test project files is available by running:

npm run prettier

Installation

To start working with this package, clone the repository and run npm install.

In order to build the project run grunt dev or grunt dist.

Testing

Test cases MUST be written using Intern using the Object test interface and Assert assertion interface.

90% branch coverage MUST be provided for all code submitted to this repository, as reported by istanbulโ€™s combined coverage results for all supported platforms.

To test locally in node run:

grunt test

To test against browsers with a local selenium server run:

grunt test:local

To test against BrowserStack or Sauce Labs run:

grunt test:browserstack

or

grunt test:saucelabs

Licensing information

ยฉ 2018 JS Foundation & contributors. New BSD license.

routing's People

Contributors

agubler avatar bryanforbes avatar devpaul avatar dylans avatar edhager avatar frediana avatar jdonaghue avatar kitsonk avatar maier49 avatar matt-gadd avatar mwistrand avatar nicknisi avatar novemberborn avatar rishson avatar rorticus avatar smhigley avatar umaar avatar

Stargazers

 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

routing's Issues

Circular dependency between Router and Route

Bug

There is an unresolvable circular dependency between Router and Route that was accidentally introduced by Router depending on Route. There was a legacy dependency of Route on Router that shouldn't have likely been there.

Package Version: v2.0.0-beta2.2

Type the routing "nav" event

Bug

Add typings for the routing event - nav:

export interface NavEvent extends EventObject<string> {
	outlet: string | undefined;
	context: OutletContext | undefined;
}

Report dispatch errors

Errors that occur while dispatching should be reported. Let's say as an error event on the router.

  • Should we set up a default listener which writes such errors to console.error?
  • Alternatively, if we tackle dojo/shim#21 we could take care to ensure the error becomes an unhandled rejection, and browsers will report it

Use a generic for the Context

Like parameters, the context type should be provided using a generic. Otherwise users will end up casting context access.

General Routing Usage

Is there a use case for dojo/routing to be used outside of a Dojo 2 application?

Initially the routing library was created to be standalone and provided programmatic functionality that is not used (or needed) with the later declarative Outlet routing that integrates with widgets created using dojo/widget-core.

If it is decided that dojo/routing is specifically for usage within Dojo 2 applications, we should consider refactoring the package to ensure the Outlet routing is the first class functionality from the ground up as opposed to being built on top of the initial programmatic routing solution (which meant there were some design/implementation sacrifices).

Base pathname should be configurable

Both in the History and in the Router, the base pathname should be configurable. E.g. the base could be https://example.com/division/project/, allowing routes to be defined for content/5. These would then match https://example.com/division/project/content/5.

Use TS2.2 classes

Enhancement

Instead of using compose, we should change routing over to use TS 2.2 classes and mixins.

Add `onEnter` for Outlets

Enhancement

Should be able to define an onEnter callback for outlets that can use properties from the selected output to perform an action (such as load data for the route.)

First router 'nav' event is fired before listeners have subscribed

Bug

The history manager is registered with an onChange listener in the constructor. This means that the _onChange method may be fired within the constructor, emitting the 'nav' event before listeners have been registered.

Previously, this was handled through a .start method but could be a constructor option or use a queued event manager.

Package Version: latest

Code

router.on('nav', () => {
	console.log('nav');
});

Expected behavior:

'nav' should be logged on page load.

Actual behavior:

'nav' is not logged on page load.

guard() should be able to navigate to a different path

Currently guard() can prevent a route from being selected. It should also be able to force a navigation to a different path. E.g. if the guard knows a user doesn't have access to a specific section it can redirect them to an alternative location.

When forcing navigation, the current dispatch should be aborted.

Improve main, public modules

The new approach is for main.ts to export the public API, so in plain Node you can do require('dojo-routing'). However for TypeScript we'd still want you to be able to consume dojo-routing/createRoute.

  • Remove interface exports from main.ts
  • Export history from main.ts
  • Rename _createRoute to createRoute
  • Rename _createRouter to createRouter
  • Update README

Generate links for routes

It'd be great if we could create a link for a route:

const router = createRouter();
const blog = createRoute({ path: '/blog' });

router.append(blog);

router.link(blog) === '/blog';

Routes should know who they've been appended to (and they cannot be appended more than once):

const create = createRoute({ path: '/new' });
blog.append(create);

router.link(create) === '/blog/new';

Routes may contain parameters:

const show = createRoute({ path: '/{id}' });
blog.append(show);

router.link(show, { id: '5' }) === '/blog/5';

Perhaps the router can keep track of the currently selected routes and their raw parameters, to avoid having to repeat parameters. Then, if it can determine which selected route directly contains the route it needs to create the link for:

const edit = createRoute({ path: '/edit' });
post.append(edit);

router.dispatch({}, '/blog/5');

router.link(edit) === '/blog/5/edit';
router.link(show, { id: '6' }) === '/blog/6';
router.link(show) === '/blog/5';

Throw if parameters are missing along the hierarchy. Assume it's OK if two routes in the hierarchy use the same parameter and fill in the same value. Make sure not to repeat query parameters.

Thoughts?

Bug: Change `event` triggered twice in createHashHistory

When using createHashHistory#set() a Router#dispatch() will be triggered twice (if wired) because the createHashHistory#set() emits a change, as well as the _onHashchange listener triggering a change itself. This is currently evident in todo-mvc.

Routing Component

Enhancement

A routing component that can hook into registered routes against a router instance that will be available via the registry. The registry will be either global or locally scoped, however for routing, using the global registry will be more common.

The creation of the application routes will be configuration driven using a simple nested structure:

const applicationRoutes = [
    {
        path: 'foo',
        children: [
            {
                path: 'bar',
            },
            {
                path: 'qux'
             }
        ]
    },
   {
        path: 'baz'
    }
];

This configuration would support the following routes:

  1. /foo
  2. /baz
  3. /foo/bar
  4. /foo/qux

Routing will provide a utility functions to create a RouterContext, register these routes against a specific router instance and allow the consumer to explicitly register the RouterContext in the desired registry.

// assuming the routes configuration above
const router = new Router(/* with history manager of choice */);
const context: RouterContext = createRouterContext(router);

registerRoutes(routes, router);

//ROUTING_KEY - symbol exported by routing
registry.define(ROUTING_KEY, Injector(RouterInjector, context));

Additionally easy to provide a further function that encapsulates all this logic and simply returns the router instance.

The routes will be controlled by using the higher order component Routeable for the component that requires routing. Routable accepts and an options argument with path, onEnter & onExit properties.

  • path
    • the path chunk of the route. e.g Routeable(MyWidget, { path: 'foo' });
  • onEnter
    • callback to execute when a route is entered. The route and the params will be passed as arguments
  • onExit
    • callback to execute when route is exited. The route will be passed as an argument.

When a Route "matches" the current segment of the route it will render the "wrapped" component (and it's children).

Potentially usage example (assuming the route configuration about):

//MyWidget.ts

export interface MyWidgetProperties extends WidgetProperties {
    label: string;
}

export MyWidget extends WidgetBase<MyWidgetPropertoes> {
    protected render(): DNode {
        return properties.label;
    }
}
// App.ts
import { Routeable } from '@dojo/routing/Routeable';
import { MyWidget } from './MyWidget';


export RouteableMyWidgetFoo = Routeable(MyWidget, { path: 'foo' });
export RouteableMyWidgetBar = Routeable(MyWidget, { path: 'bar' });
export RouteableMyWidgetBaz = Routeable(MyWidget, { path: 'baz' });
export RouteableMyWidgetQux = Routeable(MyWidget, { path: 'qux' });

class App extends WidgetBase {
    render() {
        return v('div', [
            w(RouteableMyWidgetFoo, { label: 'foo' }, [
                w(RouteableMyWidgetFoo, { label: 'foo' }), `foo/foo` not registered so will not ever match
                w(RouteableMyWidgetBar, { label: 'bar' }),
                w(RouteableMyWidgetQux, { label: 'qux' }),
            ]),
            w(RouteableMyWidgetBaz, { label: 'baz' })
        ]);
    }
}

Compose data from parent -> children routes

Router currently composes routes. An example configuration for comments a generalized comments handler might look like:

const commentsGroup = new RouteGroup({
    path: 'comments/{id}',
    defaultRoute: new DefaultRoute({
        enter() {}
    })
});
const articlesGroup = new RouteGroup({
    path: 'articles/{id}/',
    routes: [
        commentsGroup,
    ]
});
const userGroup = new RouteGroup({
    path: 'users/{id}',
    routes: [
        commentsGroup,
    ]
});

The current router will call enter() on commentsGroup and require it to handle the entire route transition. This means that commentsGroup will need to be able to resolve the entire chain and be able to fetch data for articles or users based on the path context. In this model the leaves must have complete knowledge of its entire context and as paths become longer and are reused the leaf-route's implementation becomes more complex.

Instead, I think it makes sense to allow composed routes to also compose data from parent -> child. We could do this by appending an additional method or allowing enter() to return data in a promise. In the above case if a user were to go to /articles/{id}/comments/{id} then the router would call:

  1. articlesGroup.passthrough() => Promise({ model: CommentsModel })
  2. commentsGroup.enter({ model: CommentsModel })

if a user were to go to /users/{id}/comments/{id} the router would call:

  1. usersGroup.passthrough() => Promise({ model: CommentsModel })
  2. commentsGroup.enter({ model: CommentsModel })

By transversing through the routes and collecting data we can decouple (or loosely couple) CommentsGroup from the previous routes.

Take another example: building an Express router. Express passes a http request, result, and next parameter to each of its routes. By composing routes and data together we could have a root route that initializes parameters and passes them through. For example a HTTPRequest to /users/{id}/comments/{id} in it's most complex form could look like:

  1. rootHttpRouteGroup.passthrough() => Promise({ req: HTTPRequest, res: HTTPResult, stopPropigation: Boolean })
  2. usersRouteGroup.passthrough() => Promise({ req, res, ..., model: UserStore })
  3. userRouteGroup.passthrough() => Promise({ req, res, ..., model: User extends Commenter })
  4. commentGroup.enter({ req, res, model: Commenter }) => Promise()

In the above example the Commenter type would define a property with a CommentStore and commentGroup.enter() would be able to look at the request and see what type of request it is (POST, GET) and either add to the comment store or get from the comment store. Without the ability to pass data through the route graph things quickly get complex to break apart at the leaf routes.

Should be able to define an `Outlet` component for multiple outlet names

Enhancement

In certain scenarios it is helpful to be able to define a single Outlet that will render for multiple (or all outlets) and just inject params when from url when they exists.

Currently usage restricts the mapping of a an Outlet component to a single outlet from the routing configuration.

// example routing config
const routingConfig = [
    {
        path: '/foo',
        outlet: 'foo'
    }
];
const myOutlet = Outlet(MyWidget, 'foo');

But there are use-case where being able to create an outlet component that maps to multiple outlets within the routing config would be helpful, this could be done by accepting an array string[] as well as a single string. This will probably need a change to the Router#getOutlet function to accept the same types and when it's an array return the matching outlet if there is one.

// example routing config
const routingConfig = [
    {
        path: '/foo',
        outlet: 'foo'
    }
    {
        path: '/bar',
        outlet: 'bar'
    }
    {
        path: '/baz',
        outlet: 'baz'
    }
];
const myOutlet = Outlet(MyWidget, [ 'foo', 'bar', 'baz' ]);

Would need to think about what to do if more than one of the specified outlets match? And if that's even a scenario that should be supported.

Limit redirects

Now that routes can request redirects we should limit how many redirects are allowed. According to this old StackOverflow post Chrome's limit is 20, which seems reasonable.

This should only be done in Router#start(). Every time a dispatch does not result in a redirect the counter should be reset.

API Doc Review for routing

Review the generated API documentation for routing at http://dojo.io/api/. If any of the information is not accurate, please make the necessary adjustments to the API documentation within this repo.

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.