UNPKG

@cmdap/ng-wizard

Version:

A simple wizard/stepper component for Angular 9 utilizing Angular Routing for navigation.

475 lines (461 loc) 22.4 kB
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 &#123; path: '', component: {{ error.wizardComponentName }} &#125;,\n];\n\n@NgModule(&#123;\n imports: [RouterModule.forRoot(routes)],\n exports: [RouterModule]\n&#125;)\nexport class AppRoutingModule &#123; &#125;</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&#123; path: '{{ error.wizardPath }}', component: {{ error.wizardComponentName }}, children: [\n &#123; path: 'step1', component: Step1Component &#125;,\n &#123; path: '**', redirectTo: 'step1' &#125;,\n] &#125;,</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 &#123;\n constructor() &#123;\n super();\n &#125;\n&#125;</pre><br/>\n or<br/>\n <pre>\n@NgComponent(...)\nexport class {{ error.stepComponentName }} implements NgWizardStepInterface &#123;\n wsIsValid() &#123;\n return true;\n &#125;\n wsOnNext() &#123; &#125;\n wsOnPrevious() &#123; &#125;\n&#125;</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