Skip to content

Instantly share code, notes, and snippets.

@Scapal
Last active April 21, 2016 13:34
Show Gist options
  • Save Scapal/20518b932c4c8e3c1940 to your computer and use it in GitHub Desktop.
Save Scapal/20518b932c4c8e3c1940 to your computer and use it in GitHub Desktop.
Aurelia navigation-view
import {
inject, Container, ViewSlot, ViewLocator,
customElement, noView, CompositionTransaction,
CompositionEngine, bindable
} from 'aurelia-framework';
@noView()
@customElement('navigation-view')
@inject(Element, Container, ViewSlot, ViewLocator, CompositionEngine, CompositionTransaction)
export class NavigationView {
@bindable viewModel;
@bindable model;
@bindable title;
viewChanged = null;
subscribers = [];
stack = [];
busy = false;
constructor(element, container, viewSlot, viewLocator, compositionEngine, compositionTransaction) {
this.element = element;
this.container = container;
this.viewSlot = viewSlot;
this.viewLocator = viewLocator;
this.compositionEngine = compositionEngine;
this.compositionTransaction = compositionTransaction;
this.controller = null;
if (!('initialComposition' in compositionTransaction)) {
compositionTransaction.initialComposition = true;
this.compositionTransactionNotifier = compositionTransaction.enlist();
}
}
get length() {
return this.stack.length;
}
_getViewModel(instruction) {
if (typeof instruction.viewModel === 'function') {
instruction.viewModel = Origin.get(instruction.viewModel).moduleId;
}
if (typeof instruction.viewModel === 'string') {
return this.compositionEngine.ensureViewModel(instruction);
}
return Promise.resolve(instruction);
}
created(owningView) {
this.owningView = owningView;
}
bind(bindingContext, overrideContext) {
this.container.viewModel = bindingContext;
this.overrideContext = overrideContext;
}
attached() {
this.navigate(this.viewModel, this.title, this.model);
}
_navigate(controller, title, swapDirection) {
let oldController = this.controller;
this.controller = controller;
let classList = this.element.classList;
this.title = title;
let compositionHandler = () => {
if (this.compositionTransactionNotifier) {
this.compositionTransactionNotifier.done();
this.compositionTransactionNotifier = null;
}
classList.remove(`nav-${swapDirection}`);
this.busy = false;
};
if (!oldController) {
return Promise.resolve(this.viewSlot.add(this.controller.view)).then( () => compositionHandler() );
} else {
classList.add(`nav-${swapDirection}`);
this.invokeSubscribers(swapDirection);
return Promise.all([
this.viewSlot.add(this.controller.view),
this.viewSlot.remove(oldController.view, true)
]).then( () => compositionHandler() );
}
}
navigate(viewModel, title, model) {
if (this.busy) return;
this.busy = true;
let childContainer = this.container.createChild();
let instruction = {
viewModel: viewModel,
container: this.container,
childContainer: childContainer,
model: model,
skipActivation: true
};
this._getViewModel(instruction).then(returnedInstruction => {
this.invokeLifecycle(returnedInstruction.viewModel, 'canActivate', model).then(canActivate => {
if (canActivate) {
this.compositionEngine.createController(returnedInstruction).then(controller => {
this.invokeLifecycle(returnedInstruction.viewModel, 'activate', model).then( () => {
if (!this.compositionTransactionNotifier) {
this.compositionTransactionOwnershipToken = this.compositionTransaction.tryCapture();
}
controller.automate(this.overrideContext, this.owningView);
if (this.compositionTransactionOwnershipToken) {
return this.compositionTransactionOwnershipToken.waitForCompositionComplete().then(() => {
this.compositionTransactionOwnershipToken = null;
if (this.controller !== null) {
this.stack.push({controller: this.controller, title: this.title});
}
this._navigate(controller, title, 'forward');
});
}
});
});
}
});
});
}
navigateBack() {
if (!this.busy && this.stack.length > 0) {
this.busy = true;
let previousState = this.stack.pop();
let controller = previousState.controller;
controller.bind(this);
this._navigate(controller, previousState.title, 'back');
}
}
subscribe(callback) {
let subscribers = this.subscribers;
subscribers.push(callback);
return {
dispose() {
let idx = subscribers.indexOf(callback);
if (idx !== -1) {
subscribers.splice(idx, 1);
}
}
};
}
invokeSubscribers(direction) {
let i = this.subscribers.length;
while (i--) {
this.subscribers[i](direction);
}
}
invokeLifecycle(instance, name, model) {
if (typeof instance[name] === 'function') {
let result = instance[name](model, this);
if (result instanceof Promise) {
return result;
}
if (result !== null && result !== undefined) {
return Promise.resolve(result);
}
return Promise.resolve(true);
}
return Promise.resolve(true);
}
}
@Scapal
Copy link
Author

Scapal commented Mar 23, 2016

Sample usage:

<template>
  <require from="./services/navigation-view"></require>

  <a href="#" click.delegate="nav.navigate('welcome', {title: 'test'})">Welcome</a>
  <a href="#" if.bind="nav.length > 0" click.delegate="nav.navigateBack()">Back</a>
  <navigation-view navigation-view.ref="nav" view-model="users" model="{name: 'test'}" title="Github Users"></navigation-view>
</template>

@Scapal
Copy link
Author

Scapal commented Mar 24, 2016

The activate method of child viewModels are called passing the model as first argument and the NavigationView as second:

  activate(model, nav) {
    this.nav = nav;
  }

It can then be used to navigate to the following view from either the view or the viewModel:
nav.navigate('welcome', {title: 'test'})

@Scapal
Copy link
Author

Scapal commented Apr 13, 2016

Example animation:

/* animate page transitions */

.nav-forward div.page.au-enter-active {
  -webkit-animation: fadeInRight 1s;
  animation: fadeInRight 1s;
}
.nav-forward div.page.au-leave-active {
  -webkit-animation: fadeOutLeft 1s;
  animation: fadeOutLeft 1s;
}

.nav-back div.page.au-enter-active {
  -webkit-animation: fadeInLeft 1s;
  animation: fadeInLeft 1s;
}
.nav-back div.page.au-leave-active {
  -webkit-animation: fadeOutRight 1s;
  animation: fadeOutRight 1s;
}

@Scapal
Copy link
Author

Scapal commented Apr 15, 2016

Make the Navbar title animate when navigating:

<div class="center" ref="titleElement">${nav.title}</div>
.animate-title-forward-add {
  -webkit-animation: fadeInRight 1s;
  animation: fadeInRight 1s;
}

.animate-title-back-add {
  -webkit-animation: fadeInRight 1s;
  animation: fadeInLeft 1s;
}
  navChanged() {
    this.subscription = this.nav.subscribe((direction) => {
      return this.cssAnimator.animate(this.titleElement, `animate-title-${direction}`);
    });
  }

  detached() {
    if (this.subscription) this.subscription.dispose();
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment