Skip to content

Instantly share code, notes, and snippets.

@ScottWhittaker
Last active December 4, 2016 19:14
Show Gist options
  • Save ScottWhittaker/c4b5282d6f43a7254d0a2eba9c1fa259 to your computer and use it in GitHub Desktop.
Save ScottWhittaker/c4b5282d6f43a7254d0a2eba9c1fa259 to your computer and use it in GitHub Desktop.
Aurelia MultiStepView Demo
<template>
<require from="nav-bar.html"></require>
<nav-bar router.bind="router"></nav-bar>
<div class="page-host">
<router-view></router-view>
</div>
</template>
export class App {
configureRouter(config, router) {
config.title = 'Aurelia';
config.map([
{ route: 'home', name: 'home', moduleId: 'home', nav: true, title: 'Home' },
{ route: ['', 'register'], name: 'register', moduleId: 'register', nav: true, title: 'Register' },
{ route: 'register-complete', name: 'register-complete', moduleId: 'register-complete', title: 'Registration Complete' }
]);
this.router = router;
}
}
<template>
<h2>Home</h2>
</template>
export class Home {
}
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body aurelia-app="main">
<h1>Loading...</h1>
<script src="https://jdanyow.github.io/rjs-bundle/node_modules/requirejs/require.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/config.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/aurelia.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/babel.js"></script>
<script>
require(['aurelia-bootstrapper']);
</script>
</body>
</html>
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.plugin('aurelia-validation')
aurelia.start().then(() => aurelia.setRoot());
}
<template>
<multi-step-view-header>
<h2>${heading}</h2>
<span>${currentStep} of ${totalSteps}</span>
</multi-step-view-header>
<multi-step-view-body>
<compose view-model="${currentViewModelPath}" model.bind="model" compose.ref="composeRef" containerless></compose>
</multi-step-view-body>
<multi-step-view-footer>
<div>
<button click.delegate="cancel()">Cancel</button>
</div>
<div>
<button if.bind="backButtonActive" click.delegate="back()">Back</button>
<button if.bind="nextButtonActive" click.delegate="next()">Next</button>
<button if.bind="completeButtonActive" click.delegate="complete({multiStepViewData: model})">Finish</button>
</div>
</multi-step-view-footer>
</template>
import {bindable} from 'aurelia-framework';
export class MultiStepViewCustomElement {
@bindable cancel;
@bindable complete;
@bindable heading;
@bindable viewModels;
backButtonActive = false;
completeButtonActive = false;
nextButtonActive = true;
currentViewModelPath;
currentStep = 1;
totalSteps = 0;
composeRef;
model = {};
bind() {
if (!this.cancel) {
throw new Error('bindable property `cancel` must be defined on MultiStepViewCustomElement');
}
if (!this.complete) {
throw new Error('bindable property `complete` must be defined on MultiStepViewCustomElement');
}
if (!this.viewModels) {
throw new Error('bindable property `viewModels` must be defined on MultiStepViewCustomElement');
}
this.totalSteps = this.viewModels.length;
this._update();
}
back() {
this.currentStep--;
this._update();
}
next() {
this._isValidView()
.then(isValid => {
if (isValid) {
this.currentStep++;
this._update();
}
});
}
_update() {
this._setButtons();
this._setViewModel();
}
_setViewModel() {
this.currentViewModelPath = this.viewModels[this.currentStep - 1];
}
_setButtons() {
this.backButtonActive = this.currentStep > 1;
this.nextButtonActive = this.currentStep < this.totalSteps;
this.completeButtonActive = this.currentStep === this.totalSteps;
}
_isValidView() {
if (!this.composeRef.currentViewModel.isValid) {
throw new Error(`${this.composeRef.viewModel} must implement an isValid method returning a boolean`);
}
return this.composeRef.currentViewModel.isValid();
}
}
<template bindable="router">
<nav>
<ul>
<li repeat.for="row of router.navigation" class="${row.isActive ? 'active' : ''}">
<a href.bind="row.href">${row.title}</a>
</li>
</ul>
</nav>
</template>
<template>
<h2>Registration Complete</h2>
</template>
export class RegisterComplete {
}
<template>
<form>
<div class="form-group" validation-errors.bind="firstNameErrors">
<label class="control-label" for="first-name">First Name</label>
<input type="text" id="first-name" value.bind="firstName & validate">
<span class="help-block" repeat.for="errorInfo of firstNameErrors">
${errorInfo.error.message}
</span>
</div>
<div class="form-group" validation-errors.bind="lastNameErrors">
<label for="last-name">Last Name</label>
<input type="text" id="last-name" value.bind="lastName & validate">
<span class="help-block" repeat.for="errorInfo of lastNameErrors">
${errorInfo.error.message}
</span>
</div>
</form>
</template>
import {inject, NewInstance} from 'aurelia-dependency-injection';
import {ValidationRules} from 'aurelia-validation';
import {ValidationController, validateTrigger} from 'aurelia-validation';
@inject(NewInstance.of(ValidationController))
export class RegisterStepOne {
firstName = '';
lastName = '';
controller = null;
constructor(controller) {
this.controller = controller;
this.controller.validateTrigger = validateTrigger.blur;
}
activate(model) {
this.model = model;
this._prepopulate();
}
isValid() {
return this.controller.validate()
.then(errors => {
if (errors.length) return false;
this._updateModel();
return true;
});
}
_prepopulate() {
this.firstName = this.model.firstName;
this.lastName = this.model.lastName;
}
_updateModel() {
this.model.firstName = this.firstName.trim();
this.model.lastName = this.lastName.trim();
}
}
ValidationRules
.ensure('firstName').required()
.ensure('lastName').required()
.on(RegisterStepOne);
<template>
<p>Please check your details...</p>
<dl>
<dt>Name</dt>
<dd>${model.firstName} ${model.lastName}</dd>
<dt>Email</dt>
<dd>${model.email}</dd>
</dl>
</template>
export class RegisterStepThree {
isValid() {
return true;
}
}
<template>
<p>Hi ${model.firstName}, please provide your email address...</p><br>
<form>
<div class="form-group" validation-errors.bind="emailErrors">
<label class="control-label" for="email">Email</label>
<input type="email" id="email" value.bind="email & validate">
<span class="help-block" repeat.for="errorInfo of emailErrors">
${errorInfo.error.message}
</span>
</div>
</form>
</template>
import {inject, NewInstance} from 'aurelia-dependency-injection';
import {ValidationRules} from 'aurelia-validation';
import {ValidationController, validateTrigger} from 'aurelia-validation';
@inject(NewInstance.of(ValidationController))
export class RegisterStepTwo {
email = '';
constructor(controller) {
this.controller = controller;
this.controller.validateTrigger = validateTrigger.blur;
}
activate(model) {
this.model = model;
this._prepopulate();
}
isValid() {
return this.controller.validate()
.then(errors => {
if (errors.length) return false;
this._updateModel();
return true;
});
}
_prepopulate() {
this.email = this.model.email;
}
_updateModel() {
this.model.email = this.email.trim();
}
}
ValidationRules
.ensure('email').required().email()
.on(RegisterStepTwo);
<template>
<require from="multi-step-view"></require>
<multi-step-view
view-models.bind="viewModels"
cancel.call="multiStepViewCancel()"
complete.call="multiStepViewComplete(multiStepViewData)"
heading.bind="'Register'">
</multi-step-view>
</template>
import {inject} from 'aurelia-framework';
import {Router} from 'aurelia-router';
@inject(Router)
export class Register {
viewModels = [
'register-step-one',
'register-step-two',
'register-step-three'
];
constructor(router) {
this.router = router;
}
multiStepViewCancel() {
this.router.navigate('home');
}
multiStepViewComplete(multiStepViewData) {
// TODO do something with data
this.router.navigate('register-complete');
}
}
:root {
--accent: rgba(236,12,104,.8);
--white: #fff;
--black: #000;
--grey-light: #666;
--grey-medium: #444;
--grey-dark: #333;
--blue-light: #8ebbdc;
--blue-medium: #3c90cc;
--blue-dark: #27638e;
--blue-deep: #205377;
--default-font-size: 18px;
--default-border-radius: 4px;
--default-border-width: 1px;
--default-border-color: #555;
--default-border: var(--default-border-width) solid var(--default-border-color);
--navigation-bgc: var(--grey-light);
--navigation-item-hover-bgc: var(--grey-medium);
--navigation-item-active-bgc: var(--accent);
--navigation-item-border-color: var(--default-border-color);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
color: var(--grey-dark);
font-size: var(--default-font-size);
}
p {
margin: .5em 0;
}
ul {
overflow: hidden;
list-style-type: none;
}
a {
display: inline-block;
text-decoration: none;
}
.page-host {
margin: 2em;
}
nav {
display: block;
background-color: var(--navigation-bgc);
}
nav li:not(.active):hover {
background-color: var(--navigation-item-hover-bgc);
}
nav li {
float: left;
border-right: var(--default-border-width) solid var(--navigation-item-border-color);
}
nav a {
padding: 1em 2em;
color: var(--white);
}
nav .active {
background-color: var(--navigation-item-active-bgc);
}
button {
padding: 0.5em 1em;
color: var(--white);
background-color: var(--grey-light);
border: 4px solid var(--grey-light);
border-radius: var(--default-border-radius);
font-size: var(--default-font-size);
font-weight: bold;
}
button:hover {
border: 4px solid var(--grey-medium);
}
dl {
margin: 1em 0;
}
dt {
color: var(--blue-light);
}
dd {
margin: .5em 0;
padding: .5em;
background-color: var(--blue-deep);
border-radius: var(--default-border-radius);
word-wrap: break-word;
}
/* forms
------------------------------*/
form {
width: 100%;
}
.form-group {
margin-bottom: 2em;;
}
label {
display: block;
margin-bottom: .5em;
font-size: 20px;
}
input {
padding: .5em;
width: 100%;
border: none;
border-radius: var(--default-border-radius);
font-size: 100%;
}
.help-block {
display: inline-block;
margin-top: 8px;
color: var(--blue-light);
}
.help-block:before {
margin-right: 4px;
content: "\02718";
}
/*
multi-step-view
------------------------------*/
multi-step-view {
display: flex;
flex-direction: column;
margin: 0 auto;
width: 500px;
min-height: 400px;
background-color: var(--blue-dark);
color: var(--white);
}
multi-step-view-header,
multi-step-view-body,
multi-step-view-footer {
display: flex;
padding: 2em;
}
multi-step-view-header,
multi-step-view-footer {
justify-content: space-between;
}
multi-step-view-body {
flex: auto;
flex-direction: column;
}
multi-step-view-header {
background-color: var(--grey-medium);
}
multi-step-view-footer {
background-color: var(--blue-medium);
}
multi-step-view button {
border: 4px solid var(--blue-dark);
background-color: var(--blue-dark);
}
multi-step-view button:hover {
border: 4px solid var(--blue-deep);
}
multi-step-view-header span {
padding: 8px;
border-radius: var(--default-border-radius);
background-color: var(--grey-light);
}
/* ai-dialog
------------------------------*/
ai-dialog-overlay.active {
opacity: 0.5 !important;
background-color: var(--black);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment