From c4a21b2541f914d4f31ae4b7852f525c48f8e4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Wenzel?= Date: Sun, 15 Oct 2017 00:53:15 +0200 Subject: [PATCH] feat(templating-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. Required by aurelia/router#538. Closes aurelia/router#534. --- src/route-loader.js | 19 ++++-- src/router-view.js | 154 +++++++++++++++++++++++++++++--------------- 2 files changed, 116 insertions(+), 57 deletions(-) diff --git a/src/route-loader.js b/src/route-loader.js index 2d1e29d..7777e30 100644 --- a/src/route-loader.js +++ b/src/route-loader.js @@ -1,5 +1,5 @@ import {inject} from 'aurelia-dependency-injection'; -import {CompositionEngine, useView, customElement} from 'aurelia-templating'; +import {CompositionEngine, useView, inlineView, customElement} from 'aurelia-templating'; import {RouteLoader, Router} from 'aurelia-router'; import {relativeToFile} from 'aurelia-path'; import {Origin} from 'aurelia-metadata'; @@ -15,9 +15,13 @@ export class TemplatingRouteLoader extends RouteLoader { loadRoute(router, config) { let childContainer = router.container.createChild(); - let viewModel = /\.html/.test(config.moduleId) - ? createDynamicClass(config.moduleId) - : relativeToFile(config.moduleId, Origin.get(router.container.viewModel.constructor).moduleId); + let viewModel = config === null + ? createEmptyClass() + : /\.html/.test(config.moduleId) + ? createDynamicClass(config.moduleId) + : relativeToFile(config.moduleId, Origin.get(router.container.viewModel.constructor).moduleId); + + config = config || {}; let instruction = { viewModel: viewModel, @@ -55,3 +59,10 @@ function createDynamicClass(moduleId) { return DynamicClass; } + +function createEmptyClass() { + @inlineView('') + class EmptyClass { } + + return EmptyClass; +} diff --git a/src/router-view.js b/src/router-view.js index 8d56e2d..0357b40 100644 --- a/src/router-view.js +++ b/src/router-view.js @@ -1,6 +1,6 @@ import {Container, inject} from 'aurelia-dependency-injection'; import {createOverrideContext} from 'aurelia-binding'; -import {ViewSlot, ViewLocator, customElement, noView, BehaviorInstruction, bindable, CompositionTransaction, CompositionEngine, ShadowDOM, SwapStrategies} from 'aurelia-templating'; +import {ViewSlot, ViewLocator, customElement, noView, BehaviorInstruction, bindable, CompositionTransaction, CompositionEngine, ShadowDOM, SwapStrategies, SwapStrategiesStateful} from 'aurelia-templating'; import {Router} from 'aurelia-router'; import {Origin} from 'aurelia-metadata'; import {DOM} from 'aurelia-pal'; @@ -14,6 +14,10 @@ export class RouterView { @bindable layoutViewModel; @bindable layoutModel; element; + name; + stateful; + nonStatefulName; + hidden = false; constructor(element, container, viewSlot, router, viewLocator, compositionTransaction, compositionEngine) { this.element = element; @@ -23,7 +27,10 @@ export class RouterView { this.viewLocator = viewLocator; this.compositionTransaction = compositionTransaction; this.compositionEngine = compositionEngine; - this.router.registerViewPort(this, this.element.getAttribute('name')); + this.name = this.element.getAttribute('name') || 'default'; + this.stateful = this.name.indexOf('.') !== -1; + this.nonStatefulName = this.name.split('.')[0]; + this.router.registerViewPort(this, this.name); if (!('initialComposition' in compositionTransaction)) { compositionTransaction.initialComposition = true; @@ -47,7 +54,7 @@ export class RouterView { let viewModelResource = component.viewModelResource; let metadata = viewModelResource.metadata; let config = component.router.currentInstruction.config; - let viewPort = config.viewPorts ? config.viewPorts[viewPortInstruction.name] : {}; + let viewPort = (config.viewPorts ? config.viewPorts[viewPortInstruction.name] : {}) || {}; childContainer.get(RouterViewLocator)._notify(this); @@ -67,44 +74,72 @@ export class RouterView { } return metadata.load(childContainer, viewModelResource.value, null, viewStrategy, true) - .then(viewFactory => { - if (!this.compositionTransactionNotifier) { - this.compositionTransactionOwnershipToken = this.compositionTransaction.tryCapture(); - } - - if (layoutInstruction.viewModel || layoutInstruction.view) { - viewPortInstruction.layoutInstruction = layoutInstruction; - } - - viewPortInstruction.controller = metadata.create(childContainer, - BehaviorInstruction.dynamic( - this.element, - viewModel, - viewFactory - ) - ); - - if (waitToSwap) { - return null; - } - - this.swap(viewPortInstruction); - }); + .then(viewFactory => { + if (!this.compositionTransactionNotifier) { + this.compositionTransactionOwnershipToken = this.compositionTransaction.tryCapture(); + } + + if (layoutInstruction.viewModel || layoutInstruction.view) { + viewPortInstruction.layoutInstruction = layoutInstruction; + } + + viewPortInstruction.controller = metadata.create(childContainer, + BehaviorInstruction.dynamic( + this.element, + viewModel, + viewFactory + ) + ); + + if (waitToSwap) { + return null; + } + + this.swap(viewPortInstruction); + }); } swap(viewPortInstruction) { let layoutInstruction = viewPortInstruction.layoutInstruction; let previousView = this.view; + let viewPort = this.router.viewPorts[viewPortInstruction.name]; - let work = () => { - let swapStrategy = SwapStrategies[this.swapOrder] || SwapStrategies.after; - let viewSlot = this.viewSlot; + let siblingViewPorts = []; + for (let vpName in this.router.viewPorts) { + let vp = this.router.viewPorts[vpName]; + if (vp !== viewPort && vp.nonStatefulName === viewPort.nonStatefulName) { + siblingViewPorts.push(vp); + } + } - swapStrategy(viewSlot, previousView, () => { - return Promise.resolve(viewSlot.add(this.view)); - }).then(() => { - this._notify(); - }); + let work = () => { + if (siblingViewPorts.length > 0) { + let swapStrategy = SwapStrategiesStateful[this.swapOrder] || SwapStrategiesStateful.after; + let viewSlot = this.viewSlot; + + let previous = []; + if (viewPortInstruction.active) { + previous = siblingViewPorts; + } + if (!viewPort.stateful && viewPortInstruction.strategy === 'replace') { + previous.push(viewPort); + } + return swapStrategy(this, previous, () => { + return Promise.resolve(viewPortInstruction.strategy === 'replace' ? viewSlot.add(this.view) : undefined); + }).then(() => { + this._notify(); + }); + } + else { + let swapStrategy = SwapStrategies[this.swapOrder] || SwapStrategies.after; + let viewSlot = this.viewSlot; + + swapStrategy(viewSlot, previousView, () => { + return Promise.resolve(viewSlot.add(this.view)); + }).then(() => { + this._notify(); + }); + } }; let ready = owningView => { @@ -119,29 +154,42 @@ export class RouterView { return work(); }; - if (layoutInstruction) { - if (!layoutInstruction.viewModel) { - // createController chokes if there's no viewmodel, so create a dummy one - // should we use something else for the view model here? - layoutInstruction.viewModel = {}; + if (viewPortInstruction.strategy === 'replace') { + if (layoutInstruction) { + if (!layoutInstruction.viewModel) { + // createController chokes if there's no viewmodel, so create a dummy one + // should we use something else for the view model here? + layoutInstruction.viewModel = {}; + } + + return this.compositionEngine.createController(layoutInstruction).then(controller => { + ShadowDOM.distributeView(viewPortInstruction.controller.view, controller.slots || controller.view.slots); + controller.automate(createOverrideContext(layoutInstruction.viewModel), this.owningView); + controller.view.children.push(viewPortInstruction.controller.view); + return controller.view || controller; + }).then(newView => { + this.view = newView; + return ready(newView); + }); } - return this.compositionEngine.createController(layoutInstruction).then(controller => { - ShadowDOM.distributeView(viewPortInstruction.controller.view, controller.slots || controller.view.slots); - controller.automate(createOverrideContext(layoutInstruction.viewModel), this.owningView); - controller.view.children.push(viewPortInstruction.controller.view); - return controller.view || controller; - }).then(newView => { - this.view = newView; - return ready(newView); - }); - } - - this.view = viewPortInstruction.controller.view; + this.view = viewPortInstruction.controller.view; - return ready(this.owningView); + return ready(this.owningView); + } + else { + return work(); + } } - + + hide(hide_: boolean) { + if (this.hidden !== hide_) { + this.hidden = hide_; + return this.viewSlot.hide(hide_); + } + return Promise.resolve(); + } + _notify() { if (this.compositionTransactionNotifier) { this.compositionTransactionNotifier.done();