Skip to content

Commit

Permalink
feat(router): stateful routes
Browse files Browse the repository at this point in the history
Adds stateful routes so that route configurations can specify a module or a module in a viewport as `stateful: true`. A stateful module that's loaded in a viewport is never unloaded when navigating away, it's just not shown, and is displayed with the same state whenever a route places it in the same viewport again.

Depending on aurelia/templating-router#64 and #536.
Closes #534.
  • Loading branch information
jwx committed Oct 19, 2017
1 parent 0bfc4bc commit 87b6aa3
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 20 deletions.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"aurelia-history": "^1.0.0",
"aurelia-logging": "^1.0.0",
"aurelia-path": "^1.0.0",
"aurelia-route-recognizer": "^1.0.0"
"aurelia-route-recognizer": "^1.0.0",
"aurelia-templating": "^1.6.0"
},
"devDependencies": {
"aurelia-pal-browser": "^1.0.0-rc.1.0.0",
Expand All @@ -59,7 +60,8 @@
"aurelia-history": "^1.0.0",
"aurelia-logging": "^1.0.0",
"aurelia-path": "^1.0.0",
"aurelia-route-recognizer": "^1.0.0"
"aurelia-route-recognizer": "^1.0.0",
"aurelia-templating": "^1.6.0"
},
"devDependencies": {
"aurelia-tools": "0.2.4",
Expand Down
8 changes: 6 additions & 2 deletions src/navigation-instruction.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,16 @@ export class NavigationInstruction {
/**
* Adds a viewPort instruction.
*/
addViewPortInstruction(viewPortName: string, strategy: string, moduleId: string, component: any): any {
addViewPortInstruction(viewPortName: string, strategy: string, moduleId: string, component: any, active: boolean): any {
const config = Object.assign({}, this.lifecycleArgs[1], { currentViewPort: viewPortName });
let viewportInstruction = this.viewPortInstructions[viewPortName] = {
name: viewPortName,
strategy: strategy,
moduleId: moduleId,
component: component,
childRouter: component.childRouter,
lifecycleArgs: [].concat(this.lifecycleArgs[0], config, this.lifecycleArgs[2])
lifecycleArgs: [].concat(this.lifecycleArgs[0], config, this.lifecycleArgs[2]),
active: active
};

return viewportInstruction;
Expand Down Expand Up @@ -227,6 +228,9 @@ export class NavigationInstruction {
return undefined;
}));
}
}
else if (viewPortInstruction.active && !viewPortInstruction.childNavigationInstruction) {
delaySwaps.push({viewPort, viewPortInstruction});
} else {
if (viewPortInstruction.childNavigationInstruction) {
loads.push(viewPortInstruction.childNavigationInstruction._commitChanges(waitToSwap));
Expand Down
34 changes: 29 additions & 5 deletions src/navigation-plan.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Redirect } from './navigation-commands';
import { _resolveUrl } from './util';
import {Redirect} from './navigation-commands';
import {_resolveUrl} from './util';

/**
* The strategy to use when activating modules during navigation.
Expand Down Expand Up @@ -64,8 +64,15 @@ export function _buildNavigationPlan(instruction: NavigationInstruction, forceLi

if (config.viewPorts) {
for (let viewPortName in config.viewPorts) {
if (config.viewPorts[viewPortName] === null || config.viewPorts[viewPortName].moduleId === null) {
config.viewPorts[viewPortName] = null;
}
if (config.viewPorts[viewPortName] !== undefined || !viewPorts[viewPortName]) {
viewPorts[viewPortName] = config.viewPorts[viewPortName];
if (config.stateful || (config.viewPorts[viewPortName] && config.viewPorts[viewPortName].stateful)) {
config.viewPorts[viewPortName].stateful = true;
viewPortName = instruction.router._ensureStatefulViewPort(viewPortName, config.viewPorts[viewPortName].moduleId);
}
viewPorts[viewPortName] = config.viewPorts[viewPortName.split('.')[0]];
}
}
}
Expand All @@ -78,7 +85,21 @@ export function _buildNavigationPlan(instruction: NavigationInstruction, forceLi
}
}

return Promise.all(pending).then(() => plan);
return Promise.all(pending).then(() => {
for (let viewPortName in plan) {
if (viewPortName.indexOf('.') != -1) {
let shortName = viewPortName.split('.')[0];
if (!plan[shortName]) {
plan[shortName] = {
name: shortName,
strategy: activationStrategy.replace,
config: null
}
}
}
}
return plan;
});
}

function buildViewPortPlan(instruction: NavigationInstruction, viewPorts: any, forceLifecycleMinimum, newParams: boolean, viewPortName: string, previous: boolean) {
Expand All @@ -101,7 +122,7 @@ function buildViewPortPlan(instruction: NavigationInstruction, viewPorts: any, f
viewPortPlan.prevComponent = prevViewPortInstruction.component;
viewPortPlan.prevModuleId = prevViewPortInstruction.moduleId;
}
if (nextViewPortConfig) {
if (nextViewPortConfig !== undefined) {
viewPortPlan.config = nextViewPortConfig;
viewPortPlan.active = true;
}
Expand All @@ -118,6 +139,9 @@ function buildViewPortPlan(instruction: NavigationInstruction, viewPorts: any, f
}
else if (prevViewPortInstruction.moduleId !== nextViewPortConfig.moduleId) {
viewPortPlan.strategy = activationStrategy.replace;
}
else if (!nextViewPortConfig.stateful && !prevViewPortInstruction.active) {
viewPortPlan.strategy = activationStrategy.replace;
} else if ('determineActivationStrategy' in prevViewPortInstruction.component.viewModel) {
viewPortPlan.strategy = prevViewPortInstruction.component.viewModel
.determineActivationStrategy(...instruction.lifecycleArgs);
Expand Down
6 changes: 4 additions & 2 deletions src/route-loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ function determineWhatToLoad(navigationInstruction: NavigationInstruction, toLoa
viewPortName,
viewPortPlan.strategy,
viewPortPlan.prevModuleId,
viewPortPlan.prevComponent);
viewPortPlan.prevComponent,
viewPortPlan.active);

if (viewPortPlan.childNavigationInstruction) {
viewPortInstruction.childNavigationInstruction = viewPortPlan.childNavigationInstruction;
Expand All @@ -69,7 +70,8 @@ function loadRoute(routeLoader: RouteLoader, navigationInstruction: NavigationIn
viewPortPlan.name,
viewPortPlan.strategy,
moduleId,
component);
component,
viewPortPlan.active);

let childRouter = component.childRouter;
if (childRouter) {
Expand Down
20 changes: 20 additions & 0 deletions src/router.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {RouteRecognizer} from 'aurelia-route-recognizer';
import {Container} from 'aurelia-dependency-injection';
import {History} from 'aurelia-history';
import {TemplatingEngine} from 'aurelia-templating';
import {NavigationInstruction} from './navigation-instruction';
import {NavModel} from './nav-model';
import {RouterConfiguration} from './router-configuration';
Expand Down Expand Up @@ -504,6 +505,25 @@ export class Router {
return c;
});
}

_ensureStatefulViewPort(name, moduleId) {
let viewPort = this.viewPorts[name];
let viewPortName = `${name}.${moduleId}`;

if (!this.viewPorts[viewPortName]) {
let newElement = viewPort.element.ownerDocument.createElement('router-view');
newElement.setAttribute('name', viewPortName);
viewPort.element.insertAdjacentElement('afterend', newElement);
let templatingEngine = viewPort.container.get(TemplatingEngine);
templatingEngine.enhance({
element: newElement,
container: viewPort.container,
resources: viewPort.resources
});
}

return viewPortName;
}
}

function validateRouteConfig(config: RouteConfig, routes: Array<Object>): void {
Expand Down
21 changes: 12 additions & 9 deletions test/navigation-plan.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,23 @@ describe('NavigationPlanStep', () => {
firstInstruction = new NavigationInstruction({
fragment: 'first',
config: { viewPorts: { default: { moduleId: './first' }}},
params: { id: '1' }
params: { id: '1' },
router: {}
});

sameAsFirstInstruction = new NavigationInstruction({
fragment: 'first',
config: { viewPorts: { default: { moduleId: './first' }}},
previousInstruction: firstInstruction,
params: { id: '1' }
params: { id: '1' },
router: {}
});

secondInstruction = new NavigationInstruction({
fragment: 'second',
config: { viewPorts: { default: { moduleId: './second' }}},
previousInstruction: firstInstruction
previousInstruction: firstInstruction,
router: {}
});
});

Expand Down Expand Up @@ -95,8 +98,8 @@ describe('NavigationPlanStep', () => {
});

it('is no-change when nothing changes', (done) => {
firstInstruction.addViewPortInstruction('default', 'ignored', './first', { viewModel: {}});

firstInstruction.addViewPortInstruction('default', 'ignored', './first', { viewModel: {}}, true);
step.run(sameAsFirstInstruction, state.next)
.then(() => {
expect(state.result).toBe(true);
Expand All @@ -107,7 +110,7 @@ describe('NavigationPlanStep', () => {

it('can be determined by route config', (done) => {
sameAsFirstInstruction.config.activationStrategy = 'fake-strategy';
firstInstruction.addViewPortInstruction('default', 'ignored', './first', { viewModel: {}});
firstInstruction.addViewPortInstruction('default', 'ignored', './first', { viewModel: {}}, true);

step.run(sameAsFirstInstruction, state.next)
.then(() => {
Expand All @@ -119,7 +122,7 @@ describe('NavigationPlanStep', () => {

it('can be determined by view model', (done) => {
let viewModel = { determineActivationStrategy: () => 'vm-strategy'};
firstInstruction.addViewPortInstruction('default', 'ignored', './first', { viewModel });
firstInstruction.addViewPortInstruction('default', 'ignored', './first', { viewModel }, true);

step.run(sameAsFirstInstruction, state.next)
.then(() => {
Expand All @@ -132,7 +135,7 @@ describe('NavigationPlanStep', () => {
it('is invoke-lifecycle when only params change', (done) => {
firstInstruction.params = { id: '1' };
sameAsFirstInstruction.params = { id: '2' };
firstInstruction.addViewPortInstruction('default', 'ignored', './first', { viewModel: {}});
firstInstruction.addViewPortInstruction('default', 'ignored', './first', { viewModel: {}}, true);

step.run(sameAsFirstInstruction, state.next)
.then(() => {
Expand All @@ -146,7 +149,7 @@ describe('NavigationPlanStep', () => {
firstInstruction.queryParams = { param: 'foo' };
sameAsFirstInstruction.queryParams = { param: 'bar' };
sameAsFirstInstruction.options.compareQueryParams = true;
firstInstruction.addViewPortInstruction('default', 'ignored', './first', { viewModel: {}});
firstInstruction.addViewPortInstruction('default', 'ignored', './first', { viewModel: {}}, true);

step.run(sameAsFirstInstruction, state.next)
.then(() => {
Expand Down

0 comments on commit 87b6aa3

Please sign in to comment.