@cmdap/ng-wizard
Version:
A simple wizard/stepper component for Angular 9 utilizing Angular Routing for navigation.
475 lines (461 loc) • 22.4 kB
JavaScript
import { __decorate, __metadata } from 'tslib';
import { Injectable, Component, Input, NgModule } from '@angular/core';
import { Router, ActivatedRoute, RouterModule } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
/**
* Returns true if the component extends the NgWizardStep class or implements the NgWizardStepInterface.
*
* @param componentRef The reference to the component to verify
*/
function componentImplementsNgWizardStepInterface(componentRef) {
return 'wsIsValid' in componentRef && 'wsOnNext' in componentRef && 'wsOnPrevious' in componentRef;
}
/**
* Returns the NgWizardStepData with the given path in the stepData list or undefined if none is
* found.
*
* @param stepData The list with NgWizardStepDatas
* @param path The path you want to get the NgWizardStepData for
*/
function getStepDataForPath(stepData, path) {
return stepData.find(data => data.path === path);
}
/**
* Returns the NgWizardStepData for the given url in the stepData list or undefined if none is
* found.
*
* @param stepData The list with NgWizardStepDatas
* @param url The url which you want to get the NgWizardStepData for
*/
function getStepDataForUrl(stepData, url) {
// gets 'path' in the url '/wizard/path?key=value'
const path = url.split('/').pop().split('?')[0];
return getStepDataForPath(stepData, path);
}
/**
* Returns the default wizard options.
*/
function getDefaultWizardOptions() {
return {
name: '',
navBar: {
icons: {
previous: '<i class="material-icons ng-wizard-icon">done</i>',
current: '<i class="material-icons ng-wizard-icon">create</i>',
next: '<i class="material-icons ng-wizard-icon">lock</i>',
},
},
buttons: {
previous: {
label: '<i class="material-icons ng-wizard-icon">chevron_left</i> Previous',
},
next: {
label: 'Next <i class="material-icons ng-wizard-icon">chevron_right</i>',
},
}
};
}
/**
* Merges the wizard options in the wizard route's config with the default wizard options.
*
* @param wizardOptions The wizard options in the wizard route's config
*/
function mergeWizardOptions(wizardOptions) {
if (!wizardOptions) {
return getDefaultWizardOptions();
}
return Object.assign(Object.assign({}, getDefaultWizardOptions()), wizardOptions);
}
/**
* Returns the options passed to the wizard step route with an added title attribute.
*
* @param route The wizard step route configuration
*/
function getWizardStepOptions(route) {
if (!route.data) {
return { title: getStepTitleFromRoute(route) };
}
return Object.assign(Object.assign({}, route.data), { title: getStepTitleFromRoute(route) });
}
/**
* Returns the step title based on the Route configuration.
* If the route has a data.title attribute it will be returned.
* Else the path will be capitalized and '-' or '_' characters will be replaces by spaces.
*
* @param route The Angular Route object
*/
function getStepTitleFromRoute(route) {
if (route.data && route.data.title) {
return route.data.title;
}
return capitalize(insertSpaces(route.path));
}
/**
* Capitalizes the first character of the passed value.
*/
function capitalize(value) {
if (!value) {
return value;
}
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
}
/**
* Replaces '-' and '_' characters by spaces.
*/
function insertSpaces(value) {
if (!value) {
return value;
}
return value.replace(/[-_]/g, ' ').trim();
}
// TODO: evaluate if this is useful
var NgWizardErrorType;
(function (NgWizardErrorType) {
NgWizardErrorType[NgWizardErrorType["NO_WIZARD_ROUTE"] = 0] = "NO_WIZARD_ROUTE";
NgWizardErrorType[NgWizardErrorType["NO_CHILD_ROUTES"] = 1] = "NO_CHILD_ROUTES";
NgWizardErrorType[NgWizardErrorType["NO_WS_INTERFACE"] = 2] = "NO_WS_INTERFACE";
})(NgWizardErrorType || (NgWizardErrorType = {}));
class NgWizardError extends Error {
constructor(type, message) {
super(message);
this.wizardComponentName = '';
this.wizardPath = '';
this.stepComponentName = '';
this.type = type;
}
}
class NoWizardRoute extends NgWizardError {
constructor(wizardComponentName) {
super(NgWizardErrorType.NO_WIZARD_ROUTE, `No route configuration for the ${wizardComponentName} found.`);
this.wizardComponentName = wizardComponentName;
}
}
class NoChildRoutes extends NgWizardError {
constructor(wizardComponentName, wizardPath) {
super(NgWizardErrorType.NO_CHILD_ROUTES, `No child routes for the ${wizardComponentName} found.`);
this.wizardComponentName = wizardComponentName;
this.wizardPath = wizardPath;
}
}
class NoWsInterface extends NgWizardError {
constructor(stepComponentName) {
super(NgWizardErrorType.NO_WS_INTERFACE, `The ${stepComponentName} does not extend the NgWizardStep class or implement the NgWizardStepInterface.`);
this.stepComponentName = stepComponentName;
}
}
let NgWizardService = class NgWizardService {
constructor(router) {
this.router = router;
this.stepData = [];
this.stepDataChanges = new BehaviorSubject([]);
}
/**
* Initializes the wizard by parsing the wizard's child routes found in Angular's router config
* into a list of NgWizardStepData.
* @param wizardName The unique name of the wizard
*/
loadWizardRoutes(wizardName) {
this.wizardRoute = this.getWizardRoute(wizardName);
if (!this.wizardRoute) {
throw new NoWizardRoute(wizardName);
}
this.wizardOptions = mergeWizardOptions(this.wizardRoute.data);
this.loadChildRoutes(this.wizardRoute);
}
/**
* Checks and stores the currently displayed component.
* @param componentRef A reference to the currently displayed component
*/
registerActiveComponent(componentRef) {
if (!componentImplementsNgWizardStepInterface(componentRef)) {
throw new NoWsInterface(componentRef.constructor.name);
}
// Cast to unknown before casting to NgWizardStep to let typescript know we checked and approve
// this conversion.
this.currentComponent = componentRef;
this.currentStepData = getStepDataForUrl(this.stepData, this.router.url);
this.currentStepData.componentRef = componentRef;
this.resetCurrentStep();
this.currentStepData.isCurrent = true;
this.onStepDataChange();
}
/**
* Calls the current component's wsOnPrevious() or wsOnNext()) methods and navigates to the given
* step if the component does not abort or its state is invalid (for next navigation).
*
* @param stepData The NgWizardStepData of the the step to navigate to
*/
navigateToStep(stepData) {
let goAhead;
if (this.currentStepData.order > stepData.order) {
goAhead = this.currentComponent.wsOnPrevious();
}
else {
goAhead = this.currentComponent.wsIsValid() && this.currentComponent.wsOnNext();
}
if (goAhead === false) {
return;
}
// If the wizard is added to a specific path in the application we have to join that path and
// the step's path as the path to navigate to.
// The Angular Router's relativeTo option does not seem to work when using the hash location
// strategy.
// The path is based on the current route to allow route parameter
const routeFragment = this.router.url.split('/');
routeFragment.pop();
routeFragment.push(stepData.path);
const stepPath = routeFragment.join('/');
if (stepData.options.cleanQueryParameters) {
return this.router.navigate([stepPath], { queryParams: {} });
}
return this.router.navigate([stepPath], { queryParamsHandling: 'merge' });
}
/**
* Utility method to navigate to the next step.
*/
navigateToNextStep() {
const nextStepData = getStepDataForPath(this.stepData, this.currentStepData.nextStep);
return this.navigateToStep(nextStepData);
}
/**
* Utility method to navigate to the previous step.
*/
navigateToPreviousStep() {
const previousStepData = getStepDataForPath(this.stepData, this.currentStepData.previousStep);
return this.navigateToStep(previousStepData);
}
/**
* Utility method to navigate to a specific route path (external to the wizard)
*/
navigateToPath(path) {
return this.router.navigate([path], { queryParamsHandling: 'merge' });
}
/**
* Returns the step data changes as an observable.
*/
getStepDataChangesAsObservable() {
return this.stepDataChanges.asObservable();
}
/**
* Returns the current step data as an observable.
*/
// TODO: Write a unit test for this method
getCurrentStepDataAsObservable() {
return this.getStepDataChangesAsObservable().pipe(map((stepDatas) => stepDatas.find((stepData) => stepData.isCurrent)));
}
/**
* Returns the Angular Route for the wizard component found in Angular's router config.
* @param wizardName The unique name of the wizard
*/
getWizardRoute(wizardName) {
const wizardRoutes = this.getAllWizardRoutes(this.router.config, wizardName);
return wizardRoutes.find((route) => route.data && route.data.name === wizardName);
}
/**
* From a given array of routes config, returns an array of routes config whose component is wizardComponentName.
* Recursively look down every children route config
* @param routes An array of route config
* @param wizardName The name of the wizard to look for
*/
getAllWizardRoutes(routes, wizardName) {
let wizardRoutes = routes.filter((route) => route.data && route.data.name === wizardName);
// Recursive search in child routes
routes.filter((route) => route.children && route.children.length > 0).forEach((routeWithChildren) => {
const childWizardRoutes = this.getAllWizardRoutes(routeWithChildren.children, wizardName);
wizardRoutes = wizardRoutes.concat(childWizardRoutes);
});
// Recursive search in lazily loaded child routes
routes.filter((route) => { var _a, _b; return ((_b = (_a = route._loadedConfig) === null || _a === void 0 ? void 0 : _a.routes) === null || _b === void 0 ? void 0 : _b.length) > 0; }).forEach((routeWithChildren) => {
const childWizardRoutes = this.getAllWizardRoutes(routeWithChildren._loadedConfig.routes, wizardName);
wizardRoutes = wizardRoutes.concat(childWizardRoutes);
});
return wizardRoutes;
}
/**
* Parses the child routes of the wizard component route and stores them as a list of
* NgWizardStepData.
* @param wizardRoute The Angular Route for the wizard component
*/
loadChildRoutes(wizardRoute) {
if (!wizardRoute.children) {
throw new NoChildRoutes(wizardRoute.component.name, wizardRoute.path);
}
const childRoutes = wizardRoute.children.filter((route) => route.component);
this.stepData = [];
for (let i = 0; i < childRoutes.length; i++) {
this.registerStep(i, childRoutes[i], childRoutes[i - 1], childRoutes[i + 1]);
}
}
/**
* Stores a child route as an NgWizardStepData.
* @param index The index in the list of child routes
* @param stepRoute The child route
* @param previousStep The previous child route (undefined if first child route)
* @param nextStep The next child route (undefined if last child route)
*/
registerStep(index, stepRoute, previousStep, nextStep) {
this.stepData.push({
order: index + 1,
componentName: stepRoute.component.name,
path: stepRoute.path,
previousStep: previousStep ? previousStep.path : undefined,
nextStep: nextStep ? nextStep.path : undefined,
isCurrent: false,
options: getWizardStepOptions(stepRoute),
});
this.onStepDataChange();
}
/**
* Emits a step data change event to the subscribers when the step data changes.
*/
onStepDataChange() {
this.stepDataChanges.next(this.stepData);
}
/**
* Sets the isCurrent attribute of all step data to false.
*/
resetCurrentStep() {
this.stepData.map((stepData) => {
stepData.isCurrent = false;
return stepData;
});
}
};
NgWizardService = __decorate([
Injectable(),
__metadata("design:paramtypes", [Router])
], NgWizardService);
let NgWizardComponent = class NgWizardComponent {
constructor(service, route) {
this.service = service;
this.route = route;
try {
this.route.data.subscribe(data => {
this.wizardName = data.name;
});
this.service.loadWizardRoutes(this.wizardName);
}
catch (error) {
this.error = error;
}
}
onActivate(componentRef) {
try {
this.service.registerActiveComponent(componentRef);
}
catch (error) {
this.error = error;
}
}
};
NgWizardComponent = __decorate([
Component({
selector: 'ng-wizard',
template: "<div class=\"ng-wizard-container\">\n <ng-wizard-error [error]=\"error\"></ng-wizard-error>\n <ng-wizard-navigation></ng-wizard-navigation>\n <div class=\"ng-wizard-content-container\">\n <router-outlet (activate)=\"onActivate($event)\"></router-outlet>\n </div>\n <ng-wizard-buttons></ng-wizard-buttons>\n</div>\n"
}),
__metadata("design:paramtypes", [NgWizardService,
ActivatedRoute])
], NgWizardComponent);
let NgWizardErrorComponent = class NgWizardErrorComponent {
constructor() {
this.NgWizardErrorType = NgWizardErrorType;
}
};
__decorate([
Input(),
__metadata("design:type", NgWizardError)
], NgWizardErrorComponent.prototype, "error", void 0);
NgWizardErrorComponent = __decorate([
Component({
selector: 'ng-wizard-error',
template: "<div *ngIf=\"error\" class=\"ng-wizard-error\">\n <div [ngSwitch]=\"error.type\" class=\"ng-wizard-error-message\">\n\n <!-- NO_WIZARD_ROUTE error -->\n\n <span *ngSwitchCase=\"NgWizardErrorType.NO_WIZARD_ROUTE\" class=\"no-wizard-route\">\n No route configuration for the {{ error.wizardComponentName }} found.<br/>\n Add a route for the {{ error.wizardComponentName }} to your AppRoutingModule.<br/>\n <pre>\nconst routes: Routes = [\n { path: '', component: {{ error.wizardComponentName }} },\n];\n\n@NgModule({\n imports: [RouterModule.forRoot(routes)],\n exports: [RouterModule]\n})\nexport class AppRoutingModule { }</pre>\n </span>\n\n <!-- NO_CHILD_ROUTES error -->\n\n <span *ngSwitchCase=\"NgWizardErrorType.NO_CHILD_ROUTES\" class=\"no-child-routes\">\n No child routes for the {{ error.wizardComponentName }} found.<br/>\n Add at least one child route for the {{ error.wizardComponentName }} to your AppRoutingModule.<br/>\n <pre>\n{ path: '{{ error.wizardPath }}', component: {{ error.wizardComponentName }}, children: [\n { path: 'step1', component: Step1Component },\n { path: '**', redirectTo: 'step1' },\n] },</pre>\n </span>\n\n <!-- NO_WS_INTERFACE error -->\n\n <span *ngSwitchCase=\"NgWizardErrorType.NO_WS_INTERFACE\" class=\"no-ws-interface\">\n The {{ error.stepComponentName}} does not extend the NgWizardStep class or implement the\n NgWizardStepInterface.<br/>\n Extend the NgWizardStep class in your {{ error.stepComponentName}} or implement the\n NgWizardStepInterface.<br/>\n <pre>\n@NgComponent(...)\nexport class {{ error.stepComponentName }} extends NgWizardStep {\n constructor() {\n super();\n }\n}</pre><br/>\n or<br/>\n <pre>\n@NgComponent(...)\nexport class {{ error.stepComponentName }} implements NgWizardStepInterface {\n wsIsValid() {\n return true;\n }\n wsOnNext() { }\n wsOnPrevious() { }\n}</pre>\n </span>\n\n </div>\n</div>\n",
styles: [".ng-wizard-error{border:1px solid #d71117;border-radius:5px;color:#d71117;padding:10px 10px 0;margin-top:10px}.ng-wizard-error .ng-wizard-error-message pre{display:inline-block;border-radius:3px;background-color:rgba(218,215,197,.4);color:#4d4d4d;padding:15px}"]
})
], NgWizardErrorComponent);
let NgWizardNavigationComponent = class NgWizardNavigationComponent {
constructor(service) {
this.service = service;
}
ngOnInit() {
this.stepData$ = this.service.getStepDataChangesAsObservable();
this.service.getCurrentStepDataAsObservable().subscribe(stepData => this.currentStepData = stepData);
this.wizardOptions = this.service.wizardOptions;
}
goToStep(stepData) {
if ((this.currentStepData && this.currentStepData.options && this.currentStepData.options.disableNavigation)
|| stepData.order >= this.currentStepData.order) {
return;
}
this.service.navigateToStep(stepData);
}
};
NgWizardNavigationComponent = __decorate([
Component({
selector: 'ng-wizard-navigation',
template: "<div class=\"ng-wizard-navigation-container\">\n <nav class=\"ng-wizard-navigation\">\n <ul class=\"ng-wizard-navigation-list\">\n <li *ngFor=\"let stepData of stepData$ | async\" class=\"ng-wizard-navigation-list-item\" (click)=\"goToStep(stepData);\">\n <div *ngIf=\"stepData?.order < currentStepData?.order\"\n [ngClass]=\"{\n 'ng-wizard-navigation-link': !currentStepData?.options.disableNavigation,\n 'ng-wizard-navigation-disabled': currentStepData?.options.disableNavigation\n }\">\n <span [innerHTML]=\"stepData.options.icon || wizardOptions.navBar.icons.previous\" class=\"ng-wizard-navigation-icon\"></span>\n <span class=\"ng-wizard-navigation-label\">\n {{ stepData.options.title }}\n </span>\n </div>\n\n <div\n *ngIf=\"stepData?.order === currentStepData?.order\"\n class=\"ng-wizard-navigation-active\">\n <span [innerHTML]=\"stepData.options.icon || wizardOptions.navBar.icons.current\" class=\"ng-wizard-navigation-icon\"></span>\n <span class=\"ng-wizard-navigation-label\">\n {{ stepData.options.title }}\n </span>\n </div>\n\n <div *ngIf=\"stepData?.order > currentStepData?.order\"\n class=\"ng-wizard-navigation-disabled\">\n <span [innerHTML]=\"stepData.options.icon || wizardOptions.navBar.icons.next\" class=\"ng-wizard-navigation-icon\"></span>\n <span class=\"ng-wizard-navigation-label\">\n {{ stepData.options.title }}\n </span>\n </div>\n </li>\n </ul>\n </nav>\n</div>\n",
styles: [""]
}),
__metadata("design:paramtypes", [NgWizardService])
], NgWizardNavigationComponent);
let NgWizardButtonsComponent = class NgWizardButtonsComponent {
constructor(service) {
this.service = service;
}
ngOnInit() {
this.currentStepData$ = this.service.getCurrentStepDataAsObservable();
this.wizardOptions = this.service.wizardOptions;
}
goToNextStep() {
this.service.navigateToNextStep();
}
goToPreviousStep() {
this.service.navigateToPreviousStep();
}
goToPath(path) {
this.service.navigateToPath(path);
}
};
NgWizardButtonsComponent = __decorate([
Component({
selector: 'ng-wizard-buttons',
template: "<div class=\"ng-wizard-buttons-container\">\n <div *ngIf=\"currentStepData$ | async as currentStepData\" class=\"ng-wizard-buttons\">\n <button *ngIf=\"currentStepData.options.buttons?.extra\"\n (click)=\"goToPath(currentStepData.options.buttons?.extra.path)\"\n class=\"ng-wizard-button-extra\">\n <span\n [innerHTML]=\"currentStepData.options.buttons.extra.label\"\n class=\"ng-wizard-button-label\"></span>\n </button>\n <button *ngIf=\"currentStepData.previousStep && !currentStepData.options.buttons?.previous?.hidden\"\n (click)=\"goToPreviousStep()\"\n class=\"ng-wizard-button-previous\">\n <span\n [innerHTML]=\"currentStepData.options.buttons?.previous?.label || wizardOptions.buttons.previous.label\"\n class=\"ng-wizard-button-label\"></span>\n </button>\n <button *ngIf=\"currentStepData.nextStep && !currentStepData.options.buttons?.next?.hidden\"\n (click)=\"goToNextStep()\"\n class=\"ng-wizard-button-next\">\n <span\n [innerHTML]=\"currentStepData.options.buttons?.next?.label || wizardOptions.buttons.next.label\"\n class=\"ng-wizard-button-label\"></span>\n </button>\n </div>\n</div>\n",
styles: [""]
}),
__metadata("design:paramtypes", [NgWizardService])
], NgWizardButtonsComponent);
let NgWizardModule = class NgWizardModule {
};
NgWizardModule = __decorate([
NgModule({
declarations: [
NgWizardComponent,
NgWizardErrorComponent,
NgWizardNavigationComponent,
NgWizardButtonsComponent,
],
imports: [CommonModule, RouterModule],
providers: [NgWizardService],
exports: [NgWizardComponent],
})
], NgWizardModule);
class NgWizardStep {
wsIsValid() {
return true;
}
wsOnNext() {
return;
}
wsOnPrevious() {
return;
}
}
/*
* Public API Surface of ng-wizard
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgWizardComponent, NgWizardModule, NgWizardService, NgWizardStep, NgWizardErrorComponent as ɵa, NgWizardNavigationComponent as ɵb, NgWizardButtonsComponent as ɵc };
//# sourceMappingURL=cmdap-ng-wizard.js.map