src/formio.wizard.js
import Promise from 'native-promise-only';
import _ from 'lodash';
import FormioForm from './formio.form';
import Formio from './formio';
import FormioUtils from './utils';
export default class FormioWizard extends FormioForm {
/**
* Constructor for wizard based forms
* @param element Dom element to place this wizard.
* @param {Object} options Options object, supported options are:
* - breadcrumbSettings.clickable: true (default) determines if the breadcrumb bar is clickable or not
* - buttonSettings.show*(Previous, Next, Cancel): true (default) determines if the button is shown or not
*/
constructor(element, options) {
super(element, options);
this.wizard = null;
this.pages = [];
this.globalComponents = [];
this.page = 0;
this.history = [];
this._nextPage = 0;
}
setPage(num) {
if (!this.wizard.full && num >= 0 && num < this.pages.length) {
this.page = num;
return super.setForm(this.currentPage());
}
else if (this.wizard.full) {
return super.setForm(this.getWizard());
}
return Promise.reject('Page not found');
}
getNextPage(data, currentPage) {
const form = this.pages[currentPage];
// Check conditional nextPage
if (form) {
let page = ++currentPage;
if (form.nextPage) {
let next = FormioUtils.evaluate(form.nextPage, {
next: page,
data,
page,
form,
instance: this
}, 'next');
if (next === null) {
return null;
}
const pageNum = parseInt(next, 10);
if (!isNaN(parseInt(pageNum, 10)) && isFinite(pageNum)) {
return pageNum;
}
return this.getPageIndexByKey(next);
}
return page;
}
return null;
}
getPreviousPage() {
const prev = this.history.pop();
if (typeof prev !== 'undefined') {
return prev;
}
return this.page - 1;
}
beforeSubmit() {
const ops = [];
const pageOptions = _.clone(this.options);
pageOptions.beforeSubmit = true;
_.each(this.pages, page => ops.push(this.createComponent(page, pageOptions).beforeSubmit()));
return Promise.all(ops);
}
nextPage() {
// Read-only forms should not worry about validation before going to next page, nor should they submit.
if (this.options.readOnly) {
this.history.push(this.page);
return this.setPage(this.getNextPage(this.submission.data, this.page)).then(() => {
this._nextPage = this.getNextPage(this.submission.data, this.page);
this.emit('nextPage', {page: this.page, submission: this.submission});
});
}
// Validate the form builed, before go to the next page
if (this.checkValidity(this.submission.data, true)) {
this.checkData(this.submission.data, {
noValidate: true
});
return this.beforeNext().then(() => {
this.history.push(this.page);
return this.setPage(this.getNextPage(this.submission.data, this.page)).then(() => {
this._nextPage = this.getNextPage(this.submission.data, this.page);
this.emit('nextPage', {page: this.page, submission: this.submission});
});
});
}
else {
return Promise.reject(this.showErrors());
}
}
prevPage() {
const prevPage = this.getPreviousPage();
return this.setPage(prevPage).then(() => {
this.emit('prevPage', {page: this.page, submission: this.submission});
});
}
cancel(noconfirm) {
if (super.cancel(noconfirm)) {
this.history = [];
return this.setPage(0);
}
else {
return this.setPage();
}
}
getPageIndexByKey(key) {
let pageIndex = 0;
_.each(this.pages, (_page, index) => {
if (_page.key === key) {
pageIndex = index;
return false;
}
});
return pageIndex;
}
addGlobalComponents(page) {
// If there are non-page components, then add them here. This is helpful to allow for hidden fields that
// can propogate between pages.
if (this.globalComponents.length) {
page.components = this.globalComponents.concat(page.components);
}
return page;
}
getPage(pageNum) {
if ((pageNum >= 0) && (pageNum < this.pages.length)) {
return this.addGlobalComponents(this.pages[pageNum]);
}
return null;
}
getWizard() {
let pageIndex = 0;
let page = null;
const wizard = _.clone(this.wizard);
wizard.components = [];
do {
page = this.getPage(pageIndex);
if (page) {
wizard.components.push(page);
}
pageIndex = this.getNextPage(this.submission.data, pageIndex);
} while (pageIndex);
// Add all other components.
_.each(this.wizard.components, (component) => {
if (component.type !== 'panel') {
wizard.components.push(component);
}
});
return wizard;
}
currentPage() {
return this.getPage(this.page);
}
buildPages(form) {
this.pages = [];
_.each(form.components, (component) => {
if (component.type === 'panel') {
// Ensure that this page can be seen.
if (FormioUtils.checkCondition(component, this.data, this.data, this.wizard, this)) {
this.pages.push(component);
}
}
else if (component.type === 'hidden') {
// Global components are hidden components that can propagate between pages.
this.globalComponents.push(component);
}
});
this.buildWizardHeader();
this.buildWizardNav();
}
get schema() {
return this.wizard;
}
setForm(form) {
if (!form) {
return;
}
this.wizard = form;
this.buildPages(this.wizard);
return this.setPage(this.page);
}
build() {
super.build();
this.formReady.then(() => {
this.buildWizardHeader();
this.buildWizardNav();
});
}
hasButton(name, nextPage) {
// Check for and initlize button settings object
this.options.buttonSettings = _.defaults(this.options.buttonSettings, {
showPrevious: true,
showNext: true,
showCancel: true
});
if (name === 'previous') {
return (this.page > 0) && this.options.buttonSettings.showPrevious;
}
nextPage = (nextPage === undefined) ? this.getNextPage(this.submission.data, this.page) : nextPage;
if (name === 'next') {
return (nextPage !== null) && (nextPage < this.pages.length) && this.options.buttonSettings.showNext;
}
if (name === 'cancel') {
return this.options.buttonSettings.showCancel;
}
if (name === 'submit') {
return (nextPage === null) || (this.page === (this.pages.length - 1));
}
return true;
}
buildWizardHeader() {
if (this.wizardHeader) {
this.wizardHeader.innerHTML = '';
}
const currentPage = this.currentPage();
if (!currentPage || this.wizard.full) {
return;
}
currentPage.breadcrumb = currentPage.breadcrumb || 'default';
if (currentPage.breadcrumb.toLowerCase() === 'none') {
return;
}
// Check for and initlize breadcrumb settings object
this.options.breadcrumbSettings = _.defaults(this.options.breadcrumbSettings, {
clickable: true
});
this.wizardHeader = this.ce('nav', {
'aria-label': 'navigation'
});
this.wizardHeaderList = this.ce('ul', {
class: 'pagination'
});
this.wizardHeader.appendChild(this.wizardHeaderList);
// Add the header to the beginning.
this.prepend(this.wizardHeader);
const showHistory = (currentPage.breadcrumb.toLowerCase() === 'history');
_.each(this.pages, (page, i) => {
// See if this page is in our history.
if (showHistory && ((this.page !== i) && !this.history.includes(i))) {
return;
}
// Set clickable based on breadcrumb settings
const clickable = this.page !== i && this.options.breadcrumbSettings.clickable;
let pageClass = 'page-item ';
pageClass += (i === this.page) ? 'active' : (clickable ? '' : 'disabled');
const pageButton = this.ce('li', {
class: pageClass,
style: (clickable) ? 'cursor: pointer;' : ''
});
// Navigate to the page as they click on it.
if (clickable) {
this.addEventListener(pageButton, 'click', (event) => {
event.preventDefault();
this.setPage(i);
});
}
const pageLabel = this.ce('span', {
class: 'page-link'
});
let pageTitle = page.title;
if (currentPage.breadcrumb.toLowerCase() === 'condensed') {
pageTitle = ((i === this.page) || showHistory) ? page.title : (i + 1);
if (!pageTitle) {
pageTitle = (i + 1);
}
}
pageLabel.appendChild(this.text(pageTitle));
pageButton.appendChild(pageLabel);
this.wizardHeaderList.appendChild(pageButton);
});
}
pageId(page) {
if (page.key) {
return page.key;
}
else if (
page.components &&
page.components.length > 0
) {
return this.pageId(page.components[0]);
}
else {
return page.title;
}
}
onChange(flags, changed) {
super.onChange(flags, changed);
// Only rebuild if there is a page change.
let pageIndex = 0;
let rebuild = false;
_.each(this.wizard.components, (component) => {
if (component.type !== 'panel') {
return;
}
if (FormioUtils.hasCondition(component)) {
const hasPage = this.pages && this.pages[pageIndex]
&& (this.pageId(this.pages[pageIndex]) === this.pageId(component));
const shouldShow = FormioUtils.checkCondition(component, this.data, this.data, this.wizard, this);
if ((shouldShow && !hasPage) || (!shouldShow && hasPage)) {
rebuild = true;
return false;
}
if (shouldShow) {
pageIndex++;
}
}
else {
pageIndex++;
}
});
if (rebuild) {
this.setForm(this.wizard);
}
// Update Wizard Nav
const nextPage = this.getNextPage(this.submission.data, this.page);
if (this._nextPage !== nextPage) {
this.buildWizardNav(nextPage);
this.emit('updateWizardNav', {oldpage: this._nextPage, newpage: nextPage, submission: this.submission});
this._nextPage = nextPage;
}
}
buildWizardNav(nextPage) {
if (this.wizardNav) {
this.wizardNav.innerHTML = '';
this.removeChild(this.wizardNav);
}
if (this.wizard.full) {
return;
}
this.wizardNav = this.ce('ul', {
class: 'list-inline'
});
this.element.appendChild(this.wizardNav);
_.each([
{name: 'cancel', method: 'cancel', class: 'btn btn-default btn-secondary'},
{name: 'previous', method: 'prevPage', class: 'btn btn-primary'},
{name: 'next', method: 'nextPage', class: 'btn btn-primary'},
{name: 'submit', method: 'submit', class: 'btn btn-primary'}
], (button) => {
if (!this.hasButton(button.name, nextPage)) {
return;
}
const buttonWrapper = this.ce('li', {
class: 'list-inline-item'
});
const buttonProp = `${button.name}Button`;
const buttonElement = this[buttonProp] = this.ce('button', {
class: `${button.class} btn-wizard-nav-${button.name}`
});
buttonElement.appendChild(this.text(this.t(button.name)));
this.addEventListener(this[buttonProp], 'click', (event) => {
event.preventDefault();
// Disable the button until done.
buttonElement.setAttribute('disabled', 'disabled');
this.setLoading(buttonElement, true);
// Call the button method, then re-enable the button.
this[button.method]().then(() => {
buttonElement.removeAttribute('disabled');
this.setLoading(buttonElement, false);
}).catch(() => {
buttonElement.removeAttribute('disabled');
this.setLoading(buttonElement, false);
});
});
buttonWrapper.appendChild(this[buttonProp]);
this.wizardNav.appendChild(buttonWrapper);
});
}
}
FormioWizard.setBaseUrl = Formio.setBaseUrl;
FormioWizard.setApiUrl = Formio.setApiUrl;
FormioWizard.setAppUrl = Formio.setAppUrl;