From 76e0f91b3935f65ad2efa5a027811b6c3d2d7546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Wenzel?= Date: Wed, 18 Oct 2017 00:48:15 +0200 Subject: [PATCH] feat(router): stateful routes 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 aurelia/router#536. Closes aurelia/router#534. --- package.json | 6 +- src/navigation-instruction.js | 8 ++- src/navigation-plan.js | 104 +++++++++++++++++++++++++++++++++- src/route-loading.js | 6 +- src/router.js | 20 +++++++ test/navigation-plan.spec.js | 18 +++--- 6 files changed, 144 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index eac1e997..17089054 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "aurelia-history": "^1.1.0", "aurelia-logging": "^1.0.0", "aurelia-path": "^1.0.0", - "aurelia-route-recognizer": "^1.2.0" + "aurelia-route-recognizer": "^1.2.0", + "aurelia-templating": "^1.6.0" }, "devDependencies": { "aurelia-pal-browser": "^1.0.0-rc.1.0.0", @@ -59,7 +60,8 @@ "aurelia-history": "^1.1.0", "aurelia-logging": "^1.0.0", "aurelia-path": "^1.0.0", - "aurelia-route-recognizer": "^1.2.0" + "aurelia-route-recognizer": "^1.2.0", + "aurelia-templating": "^1.6.0" }, "devDependencies": { "aurelia-tools": "0.2.4", diff --git a/src/navigation-instruction.js b/src/navigation-instruction.js index 03189549..4eb5e03c 100644 --- a/src/navigation-instruction.js +++ b/src/navigation-instruction.js @@ -121,7 +121,7 @@ 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, @@ -129,7 +129,8 @@ export class NavigationInstruction { 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; @@ -224,6 +225,9 @@ export class NavigationInstruction { } })); } + } + else if (viewPortInstruction.active && !viewPortInstruction.childNavigationInstruction) { + delaySwaps.push({viewPort, viewPortInstruction}); } else { if (viewPortInstruction.childNavigationInstruction) { loads.push(viewPortInstruction.childNavigationInstruction._commitChanges(waitToSwap)); diff --git a/src/navigation-plan.js b/src/navigation-plan.js index fff2edd0..73f8c042 100644 --- a/src/navigation-plan.js +++ b/src/navigation-plan.js @@ -1,4 +1,5 @@ -import { Redirect } from './navigation-commands'; +import {Redirect} from './navigation-commands'; +import {_resolveUrl} from './util'; /** * The strategy to use when activating modules during navigation. @@ -109,7 +110,19 @@ export function _buildNavigationPlan(instruction: NavigationInstruction, forceLi } } - return Promise.all(pending).then(() => plan); + 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]) { + 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]]; + } + } } for (let viewPortName in config.viewPorts) { @@ -124,7 +137,92 @@ export function _buildNavigationPlan(instruction: NavigationInstruction, forceLi }; } - return Promise.resolve(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) { + let plan = {}; + let prev = instruction.previousInstruction; + let config = instruction.config; + let configViewPortName = viewPortName; + let prevViewPortInstruction = prev ? prev.viewPortInstructions[viewPortName] : undefined; + let nextViewPortConfig = !previous ? viewPorts[configViewPortName] : undefined; + + if (config.explicitViewPorts && nextViewPortConfig === undefined) { + nextViewPortConfig = null; + } + + plan['name'] = viewPortName; + let viewPortPlan = plan['plan'] = { + name: viewPortName + }; + if (prevViewPortInstruction) { + viewPortPlan.prevComponent = prevViewPortInstruction.component; + viewPortPlan.prevModuleId = prevViewPortInstruction.moduleId; + } + if (nextViewPortConfig !== undefined) { + viewPortPlan.config = nextViewPortConfig; + viewPortPlan.active = true; + } + else { + viewPortPlan.config = prevViewPortInstruction.config; + } + + if (!prevViewPortInstruction) { + viewPortPlan.strategy = activationStrategy.replace; + } else if (nextViewPortConfig === null) { // null value means deliberately cleared! + viewPortPlan.strategy = activationStrategy.replace; + } else if (nextViewPortConfig === undefined) { // undefined (left out in config) means keep same + viewPortPlan.strategy = activationStrategy.noChange; + } + 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); + } else if (config.activationStrategy) { + viewPortPlan.strategy = config.activationStrategy; + } else if (newParams || forceLifecycleMinimum) { + viewPortPlan.strategy = activationStrategy.invokeLifecycle; + } else { + viewPortPlan.strategy = activationStrategy.noChange; + } + + if (viewPortPlan.strategy !== activationStrategy.replace && prevViewPortInstruction.childRouter) { + let path = instruction.getWildcardPath(); + let task = prevViewPortInstruction.childRouter + ._createNavigationInstruction(path, instruction).then(childInstruction => { // eslint-disable-line no-loop-func + viewPortPlan.childNavigationInstruction = childInstruction; + + return _buildNavigationPlan( + childInstruction, + viewPortPlan.strategy === activationStrategy.invokeLifecycle) + .then(childPlan => { + childInstruction.plan = childPlan; + }); + }); + + plan['task'] = task; + } + + return plan; } function hasDifferentParameterValues(prev: NavigationInstruction, next: NavigationInstruction): boolean { diff --git a/src/route-loading.js b/src/route-loading.js index fc545d54..91284b59 100644 --- a/src/route-loading.js +++ b/src/route-loading.js @@ -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; @@ -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) { diff --git a/src/router.js b/src/router.js index 03844d09..9a7d6a91 100644 --- a/src/router.js +++ b/src/router.js @@ -1,6 +1,7 @@ import {RouteRecognizer} from 'aurelia-route-recognizer'; import {Container} from 'aurelia-dependency-injection'; import {History, NavigationOptions} from 'aurelia-history'; +import {TemplatingEngine} from 'aurelia-templating'; import {NavigationInstruction} from './navigation-instruction'; import {NavModel} from './nav-model'; import {RouterConfiguration} from './router-configuration'; @@ -584,6 +585,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 generateBaseUrl(router: Router, instruction: NavigationInstruction) { diff --git a/test/navigation-plan.spec.js b/test/navigation-plan.spec.js index 57ed5104..13f1a1b4 100644 --- a/test/navigation-plan.spec.js +++ b/test/navigation-plan.spec.js @@ -35,7 +35,7 @@ describe('NavigationPlanStep', () => { fragment: 'first', config: { viewPorts: { default: { moduleId: './first' }}}, params: { id: '1' }, - router + router: {} }); sameAsFirstInstruction = new NavigationInstruction({ @@ -43,14 +43,14 @@ describe('NavigationPlanStep', () => { config: { viewPorts: { default: { moduleId: './first' }}}, previousInstruction: firstInstruction, params: { id: '1' }, - router + router: {} }); secondInstruction = new NavigationInstruction({ fragment: 'second', config: { viewPorts: { default: { moduleId: './second' }}}, previousInstruction: firstInstruction, - router + router: {} }); }); @@ -204,8 +204,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); @@ -216,7 +216,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(() => { @@ -228,7 +228,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(() => { @@ -241,7 +241,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(() => { @@ -255,7 +255,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(() => {