Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/routing/ActiveLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class ActiveLink extends WidgetBase<ActiveLinkProperties> {
if (item) {
const router = item.injector();
this._outletHandle = router.on('outlet', ({ outlet }) => {
if (outlet === to) {
if (outlet.id === to) {
this.invalidate();
}
});
Expand Down
41 changes: 2 additions & 39 deletions src/routing/Outlet.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DNode } from '../widget-core/interfaces';
import { WidgetBase } from '../widget-core/WidgetBase';
import { alwaysRender } from '../widget-core/decorators/alwaysRender';
import { MatchDetails, OnExit, OutletContext, OnEnter, Params } from './interfaces';
import { MatchDetails } from './interfaces';
import { Router } from './Router';
import { diffProperty } from '../widget-core/decorators/diffProperty';
import { Handle } from '../core/Destroyable';
Expand All @@ -15,9 +15,6 @@ export interface OutletProperties {
@alwaysRender()
export class Outlet extends WidgetBase<OutletProperties> {
private _handle: Handle | undefined;
private _matched = false;
private _matchedParams: Params = {};
private _onExit?: OnExit;

@diffProperty('routerKey')
protected onRouterKeyChange(current: OutletProperties, next: OutletProperties) {
Expand All @@ -35,39 +32,12 @@ export class Outlet extends WidgetBase<OutletProperties> {
}
}

protected onDetach() {
this._onExit && this._onExit();
}

protected onAttach() {
if (!this._handle) {
this.onRouterKeyChange(this.properties, this.properties);
}
}

private _hasRouteChanged(params: Params): boolean {
if (!this._matched) {
return true;
}
const newParamKeys = Object.keys(params);
for (let i = 0; i < newParamKeys.length; i++) {
const key = newParamKeys[i];
if (this._matchedParams[key] !== params[key]) {
return true;
}
}
return false;
}

private _onEnter(outletContext: OutletContext, onEnterCallback?: OnEnter) {
const { params, type } = outletContext;
if (this._hasRouteChanged(params)) {
onEnterCallback && onEnterCallback(params, type);
this._matched = true;
this._matchedParams = params;
}
}

protected render(): DNode | DNode[] {
const { renderer, id, routerKey = 'router' } = this.properties;
const item = this.registry.getInjector<Router>(routerKey);
Expand All @@ -76,20 +46,13 @@ export class Outlet extends WidgetBase<OutletProperties> {
const router = item.injector();
const outletContext = router.getOutlet(id);
if (outletContext) {
const { queryParams, params, type, onEnter, onExit, isError, isExact } = outletContext;
this._onExit = onExit;
const { queryParams, params, type, isError, isExact } = outletContext;
const result = renderer({ queryParams, params, type, isError, isExact, router });
if (result) {
this._onEnter(outletContext, onEnter);
return result;
}
}
}
if (this._matched) {
this._onExit && this._onExit();
this._onExit = undefined;
this._matched = false;
}
return null;
}
}
Expand Down
32 changes: 15 additions & 17 deletions src/routing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Routing for Dojo applications.
- [Route Configuration](#route-configuration)
- [Router](#router)
- [History Managers](#history-managers)
- [Outlet Event](#outlet-event)
- [Router Context Injection](#router-context-injection)
- [Outlets](#outlets)
- [Global Error Outlet](#global-error-outlet)
Expand Down Expand Up @@ -107,23 +108,6 @@ const config = [
];
```

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

```ts
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](#route-configuration) which is passed to the router on construction:
Expand Down Expand Up @@ -195,6 +179,20 @@ import { MemoryHistory } from '@dojo/framework/routing/history/MemoryHistory';
const router = new Router(config, MemoryHistory);
```

#### Outlet Event

The `outlet` event is emitted from the `router` instance each time an outlet is entered or exited. The outlet context is provided with the event payload along with the `enter` or `exit` action.

```ts
router.on('outlet', ({ outlet, action }) => {
if (action === 'enter') {
if (outlet.id === 'my-outlet') {
// do something, perhaps fetch data or set state
}
}
});
```

### 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.
Expand Down
39 changes: 24 additions & 15 deletions src/routing/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,21 @@ export interface NavEvent extends EventObject<string> {
}

export interface OutletEvent extends EventObject<string> {
outlet: string;
outlet: OutletContext;
action: 'enter' | 'exit';
}

const ROUTE_SEGMENT_SCORE = 7;
const DYNAMIC_SEGMENT_PENALTY = 2;

function matchingParams({ params: previousParams }: OutletContext, { params }: OutletContext) {
const matching = Object.keys(previousParams).every((key) => previousParams[key] === params[key]);
if (!matching) {
return false;
}
return Object.keys(params).every((key) => previousParams[key] === params[key]);
}

export class Router extends QueuingEvented<{ nav: NavEvent; outlet: OutletEvent }> implements RouterInterface {
private _routes: Route[] = [];
private _outletMap: { [index: string]: Route } = Object.create(null);
Expand Down Expand Up @@ -138,7 +146,7 @@ export class Router extends QueuingEvented<{ nav: NavEvent; outlet: OutletEvent
private _register(config: RouteConfig[], routes?: Route[], parentRoute?: Route): void {
routes = routes ? routes : this._routes;
for (let i = 0; i < config.length; i++) {
let { onEnter, onExit, path, outlet, children, defaultRoute = false, defaultParams = {} } = config[i];
let { path, outlet, children, defaultRoute = false, defaultParams = {} } = config[i];
let [parsedPath, queryParamString] = path.split('?');
let queryParams: string[] = [];
parsedPath = this._stripLeadingSlash(parsedPath);
Expand All @@ -154,9 +162,7 @@ export class Router extends QueuingEvented<{ nav: NavEvent; outlet: OutletEvent
fullPath: parentRoute ? `${parentRoute.fullPath}/${parsedPath}` : parsedPath,
fullParams: [],
fullQueryParams: [],
onEnter,
score: parentRoute ? parentRoute.score : 0,
onExit
score: parentRoute ? parentRoute.score : 0
};
if (defaultRoute) {
this._defaultOutlet = outlet;
Expand Down Expand Up @@ -288,23 +294,24 @@ export class Router extends QueuingEvented<{ nav: NavEvent; outlet: OutletEvent
matchedOutletName = matchedRoute.route.outlet;
while (matchedRoute) {
let { type, params, parent, route } = matchedRoute;

if (!previousMatchedOutlets[route.outlet]) {
this.emit({ type: 'outlet', outlet: route.outlet, action: 'enter' });
}
this._matchedOutlets[matchedRoute.route.outlet] = {
const matchedOutlet = {
id: route.outlet,
queryParams: this._currentQueryParams,
params,
type,
isError: () => type === 'error',
isExact: () => type === 'index',
onEnter: route.onEnter,
onExit: route.onExit
isExact: () => type === 'index'
};
const previousMatchedOutlet = previousMatchedOutlets[route.outlet];
if (!previousMatchedOutlet || !matchingParams(previousMatchedOutlet, matchedOutlet)) {
this.emit({ type: 'outlet', outlet: matchedOutlet, action: 'enter' });
}
this._matchedOutlets[route.outlet] = matchedOutlet;
matchedRoute = parent;
}
} else {
this._matchedOutlets.errorOutlet = {
id: 'errorOutlet',
queryParams: {},
params: {},
isError: () => true,
Expand All @@ -315,8 +322,10 @@ export class Router extends QueuingEvented<{ nav: NavEvent; outlet: OutletEvent

const previousMatchedOutletKeys = Object.keys(previousMatchedOutlets);
for (let i = 0; i < previousMatchedOutletKeys.length; i++) {
if (!this._matchedOutlets[previousMatchedOutletKeys[i]]) {
this.emit({ type: 'outlet', outlet: previousMatchedOutletKeys[i], action: 'exit' });
const key = previousMatchedOutletKeys[i];
const matchedOutlet = this._matchedOutlets[key];
if (!matchedOutlet || !matchingParams(previousMatchedOutlets[key], matchedOutlet)) {
this.emit({ type: 'outlet', outlet: previousMatchedOutlets[key], action: 'exit' });
}
}
if (matchedOutletName) {
Expand Down
26 changes: 4 additions & 22 deletions src/routing/interfaces.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ export interface Route {
fullQueryParams: string[];
defaultParams: Params;
score: number;
onEnter?: OnEnter;
onExit?: OnExit;
}

/**
Expand All @@ -35,8 +33,6 @@ export interface RouteConfig {
children?: RouteConfig[];
defaultParams?: Params;
defaultRoute?: boolean;
onEnter?: OnEnter;
onExit?: OnExit;
}

/**
Expand All @@ -55,6 +51,10 @@ export type MatchType = 'error' | 'index' | 'partial';
* Context stored for matched outlets
*/
export interface OutletContext {
/**
* Outlet id
*/
id: string;
/**
* The type of match for the outlet
*/
Expand All @@ -79,16 +79,6 @@ export interface OutletContext {
* Returns `true` when the route is an exact match
*/
isExact(): boolean;

/**
* On enter for the route
*/
onEnter?: OnEnter;

/**
* On exit for the route
*/
onExit?: OnExit;
}

/**
Expand Down Expand Up @@ -116,14 +106,6 @@ export interface RouterInterface {
readonly currentParams: Params;
}

export interface OnEnter {
(params: Params, type: MatchType): void;
}

export interface OnExit {
(): void;
}

export interface MatchDetails {
/**
* Query params from the matching route for the outlet
Expand Down
Loading