Skip to content

Commit

Permalink
feat(templating-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.

Required by aurelia/router#538.
Closes aurelia/router#534.
  • Loading branch information
jwx committed Oct 20, 2017
1 parent 54c84f8 commit c4a21b2
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 57 deletions.
19 changes: 15 additions & 4 deletions src/route-loader.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -55,3 +59,10 @@ function createDynamicClass(moduleId) {

return DynamicClass;
}

function createEmptyClass() {
@inlineView('<template></template>')
class EmptyClass { }

return EmptyClass;
}
154 changes: 101 additions & 53 deletions src/router-view.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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);

Expand All @@ -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 => {
Expand All @@ -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();
Expand Down

0 comments on commit c4a21b2

Please sign in to comment.