diff --git a/src/index.ts b/src/index.ts index edc6e025..e30e82c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,8 +21,11 @@ export {FreeNavigationMode} from './lib/navigation/free-navigation-mode'; export {NavigationMode} from './lib/navigation/navigation-mode.interface'; export {SemiStrictNavigationMode} from './lib/navigation/semi-strict-navigation-mode'; export {StrictNavigationMode} from './lib/navigation/strict-navigation-mode'; +export {BaseNavigationMode} from './lib/navigation/base-navigation-mode.interface'; export {WizardState} from './lib/navigation/wizard-state.model'; -export {navigationModeFactory} from './lib/navigation/navigation-mode.provider'; +export {NavigationModeInput} from './lib/navigation/navigation-mode-input.interface'; +export {NavigationModeFactory} from './lib/navigation/navigation-mode-factory.interface'; +export {BaseNavigationModeFactory} from './lib/navigation/base-navigation-mode-factory.provider'; // export the utility functions export {MovingDirection} from './lib/util/moving-direction.enum'; @@ -34,4 +37,4 @@ export {WizardCompletionStep} from './lib/util/wizard-completion-step.interface' export {WizardStep} from './lib/util/wizard-step.interface'; // export the module -export {ArchwizardModule} from './lib/archwizard.module'; +export {ArchwizardModule, ArchwizardModuleConfig} from './lib/archwizard.module'; diff --git a/src/lib/archwizard.module.ts b/src/lib/archwizard.module.ts index c6b04d50..4922b32d 100644 --- a/src/lib/archwizard.module.ts +++ b/src/lib/archwizard.module.ts @@ -15,6 +15,25 @@ import {WizardCompletionStepDirective} from './directives/wizard-completion-step import {WizardStepSymbolDirective} from './directives/wizard-step-symbol.directive'; import {WizardStepTitleDirective} from './directives/wizard-step-title.directive'; import {WizardStepDirective} from './directives/wizard-step.directive'; +import {NAVIGATION_MODE_FACTORY, NavigationModeFactory} from './navigation/navigation-mode-factory.interface'; +import {BaseNavigationModeFactory} from './navigation/base-navigation-mode-factory.provider'; + + +/** + * Configuration object for the `angular-archwizard` module. + * + * Allows to customize global settings. + */ +export interface ArchwizardModuleConfig { + + /** + * Custom factory of [[NavigationMode]] instances. + * + * You may need a custom factory in order to support custom navigation modes. + * By default, [[BaseNavigationModeFactory]] is used. + */ + navigationModeFactory?: NavigationModeFactory; +} /** * The module defining all the content inside `angular-archwizard` @@ -62,7 +81,12 @@ import {WizardStepDirective} from './directives/wizard-step.directive'; }) export class ArchwizardModule { /* istanbul ignore next */ - static forRoot(): ModuleWithProviders { - return {ngModule: ArchwizardModule, providers: []}; + public static forRoot(config?: ArchwizardModuleConfig): ModuleWithProviders { + return { + ngModule: ArchwizardModule, + providers: [ + { provide: NAVIGATION_MODE_FACTORY, useValue: config && config.navigationModeFactory || new BaseNavigationModeFactory() }, + ] + }; } } diff --git a/src/lib/components/wizard-completion-step.component.spec.ts b/src/lib/components/wizard-completion-step.component.spec.ts index 29aefd91..ed95f965 100644 --- a/src/lib/components/wizard-completion-step.component.spec.ts +++ b/src/lib/components/wizard-completion-step.component.spec.ts @@ -5,7 +5,6 @@ import {ArchwizardModule} from '../archwizard.module'; import {NavigationMode} from '../navigation/navigation-mode.interface'; import {WizardState} from '../navigation/wizard-state.model'; import {MovingDirection} from '../util/moving-direction.enum'; -import {WizardCompletionStepComponent} from './wizard-completion-step.component'; @Component({ selector: 'aw-test-wizard', @@ -29,11 +28,11 @@ class WizardTestComponent { public eventLog: Array = []; - enterInto(direction: MovingDirection, destination: number): void { + public enterInto(direction: MovingDirection, destination: number): void { this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); } - exitFrom(direction: MovingDirection, source: number): void { + public exitFrom(direction: MovingDirection, source: number): void { this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); } } diff --git a/src/lib/components/wizard-step.component.spec.ts b/src/lib/components/wizard-step.component.spec.ts index 6a6b37ff..2a442f8e 100644 --- a/src/lib/components/wizard-step.component.spec.ts +++ b/src/lib/components/wizard-step.component.spec.ts @@ -5,7 +5,6 @@ import {ArchwizardModule} from '../archwizard.module'; import {NavigationMode} from '../navigation/navigation-mode.interface'; import {WizardState} from '../navigation/wizard-state.model'; import {MovingDirection} from '../util/moving-direction.enum'; -import {WizardStepComponent} from './wizard-step.component'; @Component({ selector: 'aw-test-wizard', @@ -29,11 +28,11 @@ class WizardTestComponent { public eventLog: Array = []; - enterInto(direction: MovingDirection, destination: number): void { + public enterInto(direction: MovingDirection, destination: number): void { this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); } - exitFrom(direction: MovingDirection, source: number): void { + public exitFrom(direction: MovingDirection, source: number): void { this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); } } diff --git a/src/lib/components/wizard.component.spec.ts b/src/lib/components/wizard.component.spec.ts index ad318dca..38b9368e 100644 --- a/src/lib/components/wizard.component.spec.ts +++ b/src/lib/components/wizard.component.spec.ts @@ -40,7 +40,7 @@ class WizardTestComponent implements AfterViewInit { constructor(private _changeDetectionRef: ChangeDetectorRef) { } - ngAfterViewInit(): void { + public ngAfterViewInit(): void { // Force another change detection in order to fix an occuring ExpressionChangedAfterItHasBeenCheckedError this._changeDetectionRef.detectChanges(); } diff --git a/src/lib/components/wizard.component.ts b/src/lib/components/wizard.component.ts index d78a2a91..7dca2ae1 100644 --- a/src/lib/components/wizard.component.ts +++ b/src/lib/components/wizard.component.ts @@ -7,9 +7,14 @@ import { OnChanges, QueryList, SimpleChanges, - ViewEncapsulation + ViewEncapsulation, + Inject, + Optional } from '@angular/core'; import {NavigationMode} from '../navigation/navigation-mode.interface'; +import {NavigationModeInput} from '../navigation/navigation-mode-input.interface'; +import {NavigationModeFactory, NAVIGATION_MODE_FACTORY} from '../navigation/navigation-mode-factory.interface'; +import {BaseNavigationModeFactory} from '../navigation/base-navigation-mode-factory.provider'; import {WizardState} from '../navigation/wizard-state.model'; import {WizardStep} from '../util/wizard-step.interface'; @@ -85,10 +90,20 @@ export class WizardComponent implements OnChanges, AfterContentInit { /** * The navigation mode used for transitioning between different steps. - * The navigation mode can be either `strict`, `semi-strict` or `free` + * + * The input value can be either a navigation mode name or a function. + * + * A set of supported mode names is determined by the configured navigation mode factory. + * The default navigation mode factory recognizes `strict`, `semi-strict` and `free`. + * + * If the value is a function, the function will be called during the initialization of the wizard + * component and must return an instance of [[NavigationMode]] to be used in the component. + * + * If the input is not configured or set to a falsy value, a default mode will be chosen by the navigation mode factory. + * For the default navigation mode factory, the default mode is `strict`. */ @Input() - public navigationMode = 'strict'; + public navigationMode: NavigationModeInput; /** * The initially selected step, represented by its index @@ -106,8 +121,15 @@ export class WizardComponent implements OnChanges, AfterContentInit { * Constructor * * @param model The model for this wizard component + * @param navigationModeFactory Navigation mode factory for this wizard component */ - constructor(public model: WizardState) { + constructor( + public model: WizardState, + // Using @Optional() in order not to break applications which import ArchwizardModule without calling forRoot(). + @Optional() @Inject(NAVIGATION_MODE_FACTORY) private navigationModeFactory: NavigationModeFactory) { + if (!this.navigationModeFactory) { + this.navigationModeFactory = new BaseNavigationModeFactory(); + } } /** @@ -144,7 +166,7 @@ export class WizardComponent implements OnChanges, AfterContentInit { * * @param changes The detected changes */ - ngOnChanges(changes: SimpleChanges) { + public ngOnChanges(changes: SimpleChanges) { for (const propName of Object.keys(changes)) { const change = changes[propName]; @@ -157,7 +179,7 @@ export class WizardComponent implements OnChanges, AfterContentInit { this.model.disableNavigationBar = change.currentValue; break; case 'navigationMode': - this.model.updateNavigationMode(change.currentValue); + this.updateNavigationMode(change.currentValue); break; /* istanbul ignore next */ default: @@ -169,7 +191,7 @@ export class WizardComponent implements OnChanges, AfterContentInit { /** * Initialization work */ - ngAfterContentInit(): void { + public ngAfterContentInit(): void { // add a subscriber to the wizard steps QueryList to listen to changes in the DOM this.wizardSteps.changes.subscribe(changedWizardSteps => { this.model.updateWizardSteps(changedWizardSteps.toArray()); @@ -179,9 +201,20 @@ export class WizardComponent implements OnChanges, AfterContentInit { this.model.disableNavigationBar = this.disableNavigationBar; this.model.defaultStepIndex = this.defaultStepIndex; this.model.updateWizardSteps(this.wizardSteps.toArray()); - this.model.updateNavigationMode(this.navigationMode); + this.updateNavigationMode(this.navigationMode); // finally reset the whole wizard state this.navigation.reset(); } + + /** + * Updates the navigation mode for this wizard component. + * + * Initially the wizard component uses the navigation mode specified in the [[navigationMode]] input + * or the default navigation mode if the [[navigationMode]] input is not defined. + * Use this method to select a different navigation mode after the wizard component is initialized. + */ + public updateNavigationMode(navigationModeInput: NavigationModeInput) { + this.model.updateNavigationMode(this.navigationModeFactory.create(this, navigationModeInput)); + } } diff --git a/src/lib/directives/enable-back-links.directive.spec.ts b/src/lib/directives/enable-back-links.directive.spec.ts index 40309120..286364e7 100644 --- a/src/lib/directives/enable-back-links.directive.spec.ts +++ b/src/lib/directives/enable-back-links.directive.spec.ts @@ -31,11 +31,11 @@ class WizardTestComponent { public completionStepExit: (direction: MovingDirection, source: number) => void = this.exitFrom; - enterInto(direction: MovingDirection, destination: number): void { + public enterInto(direction: MovingDirection, destination: number): void { this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); } - exitFrom(direction: MovingDirection, source: number): void { + public exitFrom(direction: MovingDirection, source: number): void { this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); } } diff --git a/src/lib/directives/enable-back-links.directive.ts b/src/lib/directives/enable-back-links.directive.ts index a985bbed..6009a2d9 100644 --- a/src/lib/directives/enable-back-links.directive.ts +++ b/src/lib/directives/enable-back-links.directive.ts @@ -45,7 +45,7 @@ export class EnableBackLinksDirective implements OnInit { /** * Initialization work */ - ngOnInit(): void { + public ngOnInit(): void { this.completionStep.canExit = true; this.completionStep.stepExit = this.stepExit; } diff --git a/src/lib/directives/go-to-step.directive.spec.ts b/src/lib/directives/go-to-step.directive.spec.ts index 8ddb97ea..569d9a9e 100644 --- a/src/lib/directives/go-to-step.directive.spec.ts +++ b/src/lib/directives/go-to-step.directive.spec.ts @@ -47,7 +47,7 @@ class WizardTestComponent { public eventLog: Array = []; - finalizeStep(stepIndex: number): void { + public finalizeStep(stepIndex: number): void { this.eventLog.push(`finalize ${stepIndex}`); } } diff --git a/src/lib/directives/next-step.directive.spec.ts b/src/lib/directives/next-step.directive.spec.ts index d1079a25..c67e669f 100644 --- a/src/lib/directives/next-step.directive.spec.ts +++ b/src/lib/directives/next-step.directive.spec.ts @@ -31,7 +31,7 @@ import {NextStepDirective} from './next-step.directive'; class WizardTestComponent { public eventLog: Array = []; - finalizeStep(stepIndex: number): void { + public finalizeStep(stepIndex: number): void { this.eventLog.push(`finalize ${stepIndex}`); } } diff --git a/src/lib/directives/optional-step.directive.ts b/src/lib/directives/optional-step.directive.ts index 9089d92b..54eac01a 100644 --- a/src/lib/directives/optional-step.directive.ts +++ b/src/lib/directives/optional-step.directive.ts @@ -38,7 +38,7 @@ export class OptionalStepDirective implements OnInit { /** * Initialization work */ - ngOnInit(): void { + public ngOnInit(): void { this.wizardStep.optional = true; } } diff --git a/src/lib/directives/previous-step.directive.spec.ts b/src/lib/directives/previous-step.directive.spec.ts index 24d5873d..3d764ac0 100644 --- a/src/lib/directives/previous-step.directive.spec.ts +++ b/src/lib/directives/previous-step.directive.spec.ts @@ -31,7 +31,7 @@ import {PreviousStepDirective} from './previous-step.directive'; class WizardTestComponent { public eventLog: Array = []; - finalizeStep(stepIndex: number): void { + public finalizeStep(stepIndex: number): void { this.eventLog.push(`finalize ${stepIndex}`); } } diff --git a/src/lib/directives/selected-step.directive.ts b/src/lib/directives/selected-step.directive.ts index 998ed740..b5182d42 100644 --- a/src/lib/directives/selected-step.directive.ts +++ b/src/lib/directives/selected-step.directive.ts @@ -29,7 +29,7 @@ export class SelectedStepDirective implements OnInit { /** * Initialization work */ - ngOnInit(): void { + public ngOnInit(): void { this.wizardStep.defaultSelected = true; } } diff --git a/src/lib/directives/wizard-completion-step.directive.spec.ts b/src/lib/directives/wizard-completion-step.directive.spec.ts index 33e00150..a4b07ad2 100644 --- a/src/lib/directives/wizard-completion-step.directive.spec.ts +++ b/src/lib/directives/wizard-completion-step.directive.spec.ts @@ -29,11 +29,11 @@ class WizardTestComponent { public eventLog: Array = []; - enterInto(direction: MovingDirection, destination: number): void { + public enterInto(direction: MovingDirection, destination: number): void { this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); } - exitFrom(direction: MovingDirection, source: number): void { + public exitFrom(direction: MovingDirection, source: number): void { this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); } } diff --git a/src/lib/directives/wizard-step.directive.spec.ts b/src/lib/directives/wizard-step.directive.spec.ts index 9bb300bd..0ab79a1d 100644 --- a/src/lib/directives/wizard-step.directive.spec.ts +++ b/src/lib/directives/wizard-step.directive.spec.ts @@ -29,11 +29,11 @@ class WizardTestComponent { public eventLog: Array = []; - enterInto(direction: MovingDirection, destination: number): void { + public enterInto(direction: MovingDirection, destination: number): void { this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); } - exitFrom(direction: MovingDirection, source: number): void { + public exitFrom(direction: MovingDirection, source: number): void { this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); } } diff --git a/src/lib/navigation/base-navigation-mode-factory.provider.ts b/src/lib/navigation/base-navigation-mode-factory.provider.ts new file mode 100644 index 00000000..be218d21 --- /dev/null +++ b/src/lib/navigation/base-navigation-mode-factory.provider.ts @@ -0,0 +1,72 @@ +import {FreeNavigationMode} from './free-navigation-mode'; +import {NavigationMode} from './navigation-mode.interface'; +import {SemiStrictNavigationMode} from './semi-strict-navigation-mode'; +import {StrictNavigationMode} from './strict-navigation-mode'; +import {WizardComponent} from '../components/wizard.component'; +import {NavigationModeInput} from './navigation-mode-input.interface'; +import {NavigationModeFactory} from './navigation-mode-factory.interface'; + +/** + * A factory used to create [[NavigationMode]] instances + */ +export class BaseNavigationModeFactory implements NavigationModeFactory { + + /** + * @inheritDoc + */ + public create(wizard: WizardComponent, navigationModeInput: NavigationModeInput): NavigationMode { + let navigationModeName: string; + if (typeof navigationModeInput === 'function') { + // input is a function + return navigationModeInput(wizard); + } else { + // input is a name + navigationModeName = navigationModeInput; + } + // create NavigationMode by name + return this.createByName(wizard, navigationModeName); + } + + /** + * Create a [[NavigationMode]] for the given wizard instance by a navigation mode name + * + * @param wizard The wizard componenent where the created [[NavigationMode]] will be used + * @param navigationModeInput The name of a built-in navigation mode or a custom navigation mode + * @returns The created [[NavigationMode]] + */ + protected createByName(wizard: WizardComponent, navigationModeInput: string): NavigationMode { + switch (navigationModeInput) { + case 'free': + return new FreeNavigationMode(wizard.model); + case 'semi-strict': + return new SemiStrictNavigationMode(wizard.model); + case 'strict': + return new StrictNavigationMode(wizard.model); + default: + return !navigationModeInput ? this.createDefault(wizard) : this.createUnknown(wizard, navigationModeInput); + } + } + + /** + * Create a [[NavigationMode]] for the given wizard instance which does not have a configured navigation mode + * + * @param wizard The wizard componenent where the created [[NavigationMode]] will be used + * @returns The created [[NavigationMode]] + */ + protected createDefault(wizard: WizardComponent): NavigationMode { + return new StrictNavigationMode(wizard.model); + } + + /** + * Create a [[NavigationMode]] for the given wizard instance by a not recognized navigation mode name + * + * The base implementation always throws an Error. + * + * @param wizard The wizard componenent where the created [[NavigationMode]] will be used + * @param navigationModeInput The name of a custom navigation mode + * @returns The created [[NavigationMode]] + */ + protected createUnknown(wizard: WizardComponent, navigationModeInput: string): NavigationMode { + throw new Error(`Unknown navigation mode name: ${navigationModeInput}`); + } +} diff --git a/src/lib/navigation/base-navigation-mode.interface.ts b/src/lib/navigation/base-navigation-mode.interface.ts new file mode 100644 index 00000000..33f9cb63 --- /dev/null +++ b/src/lib/navigation/base-navigation-mode.interface.ts @@ -0,0 +1,190 @@ +import {EventEmitter} from '@angular/core'; +import {WizardState} from './wizard-state.model'; +import {MovingDirection} from '../util/moving-direction.enum'; +import {NavigationMode} from './navigation-mode.interface'; + +/** + * Base implementation of [[NavigationMode]] + * + * @author Marc Arndt + */ +export abstract class BaseNavigationMode implements NavigationMode { + constructor(protected wizardState: WizardState) { + } + + /** + * Checks, whether a wizard step, as defined by the given destination index, can be transitioned to. + * + * This method controls navigation by [[goToStep]], [[goToPreviousStep]], and [[goToNextStep]] directives. + * Navigation by navigation bar is governed by [[isNavigable]]. + * + * In this implementation, a destination wizard step can be entered if: + * - it exists + * - the current step can be exited in the direction of the destination step + * - the destination step can be entered in the direction from the current step + * + * Subclasses can impose additional restrictions, see [[canTransitionToStep]]. + * + * @param destinationIndex The index of the destination step + * @returns A [[Promise]] containing `true`, if the destination step can be transitioned to and false otherwise + */ + public canGoToStep(destinationIndex: number): Promise { + const hasStep = this.wizardState.hasStep(destinationIndex); + + const movingDirection = this.wizardState.getMovingDirection(destinationIndex); + + const canExitCurrentStep = (previous: boolean) => { + return previous && this.wizardState.currentStep.canExitStep(movingDirection); + }; + + const canEnterDestinationStep = (previous: boolean) => { + return previous && this.wizardState.getStepAtIndex(destinationIndex).canEnterStep(movingDirection); + }; + + const canTransitionToStep = (previous: boolean) => { + return previous && this.canTransitionToStep(destinationIndex); + }; + + return Promise.resolve(hasStep) + .then(canTransitionToStep) + // Apply user-defined checks at the end. They can involve user interaction + // which is better to be avoided if navigation mode does not actually allow the transition + // (`canTransitionToStep` returns `false`). + .then(canExitCurrentStep) + .then(canEnterDestinationStep); + } + + /** + * Imposes additional restrictions for `canGoToStep` in current navigation mode. + * + * The base implementation allows transition iff the given step is navigable from the navigation bar (see `isNavigable`). + * However, in some navigation modes `canTransitionToStep` can be more relaxed to allow navigation to certain steps + * by previous/next buttons, but not using the navigation bar. + */ + protected canTransitionToStep(destinationIndex: number): boolean { + return this.isNavigable(destinationIndex); + } + + /** + * Tries to transition to the wizard step, as denoted by the given destination index. + * + * When entering the destination step, the following actions are done: + * - the old current step is set as completed + * - the old current step is set as unselected + * - the old current step is exited + * - the destination step is set as selected + * - the destination step is entered + * + * When the destination step couldn't be entered, the following actions are done: + * - the current step is exited and entered in the direction `MovingDirection.Stay` + * + * @param destinationIndex The index of the destination wizard step, which should be entered + * @param preFinalize An event emitter, to be called before the step has been transitioned + * @param postFinalize An event emitter, to be called after the step has been transitioned + */ + public goToStep(destinationIndex: number, preFinalize?: EventEmitter, postFinalize?: EventEmitter): void { + this.canGoToStep(destinationIndex).then(navigationAllowed => { + if (navigationAllowed) { + // the current step can be exited in the given direction + const movingDirection: MovingDirection = this.wizardState.getMovingDirection(destinationIndex); + + /* istanbul ignore if */ + if (preFinalize) { + preFinalize.emit(); + } + + // leave current step + this.wizardState.currentStep.completed = true; + this.wizardState.currentStep.exit(movingDirection); + this.wizardState.currentStep.selected = false; + + this.transition(destinationIndex); + + // go to next step + this.wizardState.currentStep.enter(movingDirection); + this.wizardState.currentStep.selected = true; + + /* istanbul ignore if */ + if (postFinalize) { + postFinalize.emit(); + } + } else { + // if the current step can't be left, reenter the current step + this.wizardState.currentStep.exit(MovingDirection.Stay); + this.wizardState.currentStep.enter(MovingDirection.Stay); + } + }); + } + + /** + * Transitions the wizard to the given step index. + * + * Can perform additional actions in particular navigation mode implementations. + * + * @param destinationIndex The index of the destination wizard step + */ + protected transition(destinationIndex: number): void { + this.wizardState.currentStepIndex = destinationIndex; + } + + /** + * @inheritDoc + */ + public abstract isNavigable(destinationIndex: number): boolean; + + /** + * Resets the state of this wizard. + * + * A reset transitions the wizard automatically to the first step and sets all steps as incomplete. + * In addition the whole wizard is set as incomplete. + */ + public reset(): void { + if (!this.checkReset()) { + return; + } + + // reset the step internal state + this.wizardState.wizardSteps.forEach(step => { + step.completed = false; + step.selected = false; + }); + + // set the first step as the current step + this.wizardState.currentStepIndex = this.wizardState.defaultStepIndex; + this.wizardState.currentStep.selected = true; + this.wizardState.currentStep.enter(MovingDirection.Forwards); + } + + /** + * Checks if wizard configuration allows to perform reset. + * + * A check failure can be indicated either by `false` return value or by throwing + * an `Error` with the message discribing the discovered misconfiguration issue. + * + * Can include additional checks in particular navigation mode implementations. + * + * @returns True if wizard configuration is correct and reset can be performed, false otherwise + * @throws An `Error` is thrown, if a micconfiguration issue is discovered. + */ + protected checkReset(): boolean { + // the wizard doesn't contain a step with the default step index + if (!this.wizardState.hasStep(this.wizardState.defaultStepIndex)) { + throw new Error(`The wizard doesn't contain a step with index ${this.wizardState.defaultStepIndex}`); + } + return true; + } + + /** + * @inheritDoc + */ + public goToPreviousStep(preFinalize?: EventEmitter, postFinalize?: EventEmitter): void { + this.goToStep(this.wizardState.currentStepIndex - 1, preFinalize, postFinalize); + } + + /** + * @inheritDoc + */ + public goToNextStep(preFinalize?: EventEmitter, postFinalize?: EventEmitter): void { + this.goToStep(this.wizardState.currentStepIndex + 1, preFinalize, postFinalize); + } +} diff --git a/src/lib/navigation/free-navigation-mode.ts b/src/lib/navigation/free-navigation-mode.ts index 9dec8e6a..5cdaca03 100644 --- a/src/lib/navigation/free-navigation-mode.ts +++ b/src/lib/navigation/free-navigation-mode.ts @@ -1,7 +1,4 @@ -import {EventEmitter} from '@angular/core'; -import {MovingDirection} from '../util/moving-direction.enum'; -import {NavigationMode} from './navigation-mode.interface'; -import {WizardState} from './wizard-state.model'; +import {BaseNavigationMode} from './base-navigation-mode.interface'; /** * A [[NavigationMode]], which allows the user to navigate without any limitations, @@ -9,117 +6,9 @@ import {WizardState} from './wizard-state.model'; * * @author Marc Arndt */ -export class FreeNavigationMode extends NavigationMode { - /** - * Constructor - * - * @param wizardState The model/state of the wizard, that is configured with this navigation mode - */ - constructor(wizardState: WizardState) { - super(wizardState); - } - - /** - * Checks whether the wizard can be transitioned to the given destination step. - * A destination wizard step can be entered if: - * - it exists - * - the current step can be exited in the direction of the destination step - * - * @param destinationIndex The index of the destination wizard step - * @returns True if the destination wizard step can be entered, false otherwise - */ - canGoToStep(destinationIndex: number): Promise { - const hasStep = this.wizardState.hasStep(destinationIndex); - - const movingDirection = this.wizardState.getMovingDirection(destinationIndex); - - const canExitCurrentStep = (previous: boolean) => { - return previous ? this.wizardState.currentStep.canExitStep(movingDirection) : Promise.resolve(false); - }; - - const canEnterDestinationStep = (previous: boolean) => { - return previous ? this.wizardState.getStepAtIndex(destinationIndex).canEnterStep(movingDirection) : Promise.resolve(false); - }; - - return Promise.resolve(hasStep) - .then(canExitCurrentStep) - .then(canEnterDestinationStep); - } - - /** - * Tries to enter the wizard step with the given destination index. - * When entering the destination step, the following actions are done: - * - the old current step is set as completed - * - the old current step is set as unselected - * - the old current step is exited - * - the destination step is set as selected - * - the destination step is entered - * - * When the destination step couldn't be entered, the following actions are done: - * - the current step is exited and entered in the direction `MovingDirection.Stay` - * - * @param destinationIndex The index of the destination wizard step, which should be entered - * @param preFinalize An event emitter, to be called before the step has been transitioned - * @param postFinalize An event emitter, to be called after the step has been transitioned - */ - goToStep(destinationIndex: number, preFinalize?: EventEmitter, postFinalize?: EventEmitter): void { - this.canGoToStep(destinationIndex).then(navigationAllowed => { - if (navigationAllowed) { - // the current step can be exited in the given direction - const movingDirection: MovingDirection = this.wizardState.getMovingDirection(destinationIndex); - - /* istanbul ignore if */ - if (preFinalize) { - preFinalize.emit(); - } +export class FreeNavigationMode extends BaseNavigationMode { - // leave current step - this.wizardState.currentStep.completed = true; - this.wizardState.currentStep.exit(movingDirection); - this.wizardState.currentStep.selected = false; - - this.wizardState.currentStepIndex = destinationIndex; - - // go to next step - this.wizardState.currentStep.enter(movingDirection); - this.wizardState.currentStep.selected = true; - - /* istanbul ignore if */ - if (postFinalize) { - postFinalize.emit(); - } - } else { - // if the current step can't be left, reenter the current step - this.wizardState.currentStep.exit(MovingDirection.Stay); - this.wizardState.currentStep.enter(MovingDirection.Stay); - } - }); - } - - isNavigable(destinationIndex: number): boolean { + public isNavigable(destinationIndex: number): boolean { return true; } - - /** - * Resets the state of this wizard. - * A reset transitions the wizard automatically to the first step and sets all steps as incomplete. - * In addition the whole wizard is set as incomplete - */ - reset(): void { - // the wizard doesn't contain a step with the default step index - if (!this.wizardState.hasStep(this.wizardState.defaultStepIndex)) { - throw new Error(`The wizard doesn't contain a step with index ${this.wizardState.defaultStepIndex}`); - } - - // reset the step internal state - this.wizardState.wizardSteps.forEach(step => { - step.completed = false; - step.selected = false; - }); - - // set the first step as the current step - this.wizardState.currentStepIndex = this.wizardState.defaultStepIndex; - this.wizardState.currentStep.selected = true; - this.wizardState.currentStep.enter(MovingDirection.Forwards); - } } diff --git a/src/lib/navigation/navigation-mode-factory.interface.ts b/src/lib/navigation/navigation-mode-factory.interface.ts new file mode 100644 index 00000000..b8de2ab8 --- /dev/null +++ b/src/lib/navigation/navigation-mode-factory.interface.ts @@ -0,0 +1,27 @@ +import {InjectionToken} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; +import {NavigationMode} from './navigation-mode.interface'; +import { NavigationModeInput } from './navigation-mode-input.interface'; + + +/** + * The injection token to provide a particular implmentation of [[NavigationModeFactory]] + */ +export const NAVIGATION_MODE_FACTORY = new InjectionToken('NavigationModeFactory'); + + +/** + * A factory used to create [[NavigationMode]] instances + */ +export interface NavigationModeFactory { + + /** + * Create a [[NavigationMode]] for the given wizard instance + * + * @param wizard The wizard componenent where the created [[NavigationMode]] will be used + * @param navigationModeInput The name of a built-in navigation mode or a function returning + * such name or a created [[NavigationMode]] instance + * @returns The created [[NavigationMode]] + */ + create(wizard: WizardComponent, navigationModeInput: NavigationModeInput): NavigationMode; +} diff --git a/src/lib/navigation/navigation-mode-input.interface.ts b/src/lib/navigation/navigation-mode-input.interface.ts new file mode 100644 index 00000000..7c26891f --- /dev/null +++ b/src/lib/navigation/navigation-mode-input.interface.ts @@ -0,0 +1,16 @@ +import {WizardComponent} from '../components/wizard.component'; +import {NavigationMode} from './navigation-mode.interface'; + +/** + * Type of [[WizardComponent]]'s [[navigationMode]] input: either a navigation mode name or a function. + * + * A set of supported mode names is determined by the configured navigation mode factory. + * The default navigation mode factory recognizes `strict`, `semi-strict` and `free`. + * + * Alternatively, an input can take a function which will be called during the initialization of the wizard + * component. The function must return an instance of [[NavigationMode]] to be used in the component. + * + * If the [[navigationMode]] input is not configured or set to a falsy value, a default mode will be chosen by the navigation mode factory. + * For the default navigation mode factory, the default mode is `strict`. + */ +export type NavigationModeInput = string|((wizard: WizardComponent) => NavigationMode); diff --git a/src/lib/navigation/navigation-mode-selection.spec.ts b/src/lib/navigation/navigation-mode-selection.spec.ts new file mode 100644 index 00000000..17bf85f7 --- /dev/null +++ b/src/lib/navigation/navigation-mode-selection.spec.ts @@ -0,0 +1,103 @@ +import {TestBed, async} from '@angular/core/testing'; +import {Component, ViewChild} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {ArchwizardModule} from '../archwizard.module'; +import {WizardState} from './wizard-state.model'; +import {WizardComponent} from '../components/wizard.component'; +import {FreeNavigationMode} from './free-navigation-mode'; +import {BaseNavigationMode} from './base-navigation-mode.interface'; + +class CustomNavigationMode extends BaseNavigationMode { + public isNavigable(destinationIndex: number): boolean { + return true; + } +} + +@Component({ + selector: 'aw-test-wizard', + template: ` + + Step 1 + Step 2 + Step 3 + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + private wizard: WizardComponent; +} + +@Component({ + selector: 'aw-wizard-with-free-nav-mode', + template: ` + + Step 1 + Step 2 + Step 3 + + ` +}) +class WizardWithFreeNavigationModeComponent { + @ViewChild(WizardComponent) + private wizard: WizardComponent; + + public navigationModeFactory() { return 'free'; } +} + +@Component({ + selector: 'aw-wizard-with-custom-nav-mode', + template: ` + + Step 1 + Step 2 + Step 3 + + ` +}) +class WizardWithCustomNavigationModeComponent { + @ViewChild(WizardComponent) + private wizard: WizardComponent; + + public navigationModeFactory(wizard: WizardComponent) { return new CustomNavigationMode(wizard.model); } +} + +describe('NavigationMode', () => { + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent, WizardWithFreeNavigationModeComponent, WizardWithCustomNavigationModeComponent], + imports: [ArchwizardModule] + }).compileComponents(); + })); + + it('can be created with a function', () => { + const wizardTestFixture = TestBed.createComponent(WizardWithCustomNavigationModeComponent); + wizardTestFixture.detectChanges(); + + const wizardState = wizardTestFixture.debugElement.query(By.css('aw-wizard')).injector.get(WizardState); + const navigationMode = wizardState.navigationMode; + + expect(navigationMode).toEqual(jasmine.any(CustomNavigationMode)); + }); + + it('can be assigned with updateNavigationMode', () => { + const wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTestFixture.detectChanges(); + + const wizardState = wizardTestFixture.debugElement.query(By.css('aw-wizard')).injector.get(WizardState); + const navigationMode = new CustomNavigationMode(wizardState); + wizardState.updateNavigationMode(navigationMode); + expect(wizardState.navigationMode).toEqual(navigationMode); + }); + + it('can be assigned with updateNavigationMode by name', () => { + const wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTestFixture.detectChanges(); + + const wizard = wizardTestFixture.debugElement.query(By.css('aw-wizard')).injector.get(WizardComponent); + const wizardState = wizardTestFixture.debugElement.query(By.css('aw-wizard')).injector.get(WizardState); + wizard.updateNavigationMode('free'); + expect(wizardState.navigationMode).toEqual(jasmine.any(FreeNavigationMode)); + }); +}); diff --git a/src/lib/navigation/navigation-mode.interface.ts b/src/lib/navigation/navigation-mode.interface.ts index dae9fd0f..673cfc2c 100644 --- a/src/lib/navigation/navigation-mode.interface.ts +++ b/src/lib/navigation/navigation-mode.interface.ts @@ -1,64 +1,61 @@ -import {WizardState} from './wizard-state.model'; import {EventEmitter} from '@angular/core'; /** - * An interface describing the basic functionality, which must be provided by a navigation mode. + * An interface containing the basic functionality, which must be provided by a navigation mode. * A navigation mode manages the navigation between different wizard steps, this contains the validation, if a step transition can be done * + * For base implementation see [[BaseNavigationMode]]. + * * @author Marc Arndt */ -export abstract class NavigationMode { - constructor(protected wizardState: WizardState) { - } +export interface NavigationMode { /** * Checks, whether a wizard step, as defined by the given destination index, can be transitioned to. * + * This method controls navigation by [[goToStep]], [[goToPreviousStep]], and [[goToNextStep]] directives. + * Navigation by navigation bar is governed by [[isNavigable]]. + * * @param destinationIndex The index of the destination step * @returns A [[Promise]] containing `true`, if the destination step can be transitioned to and false otherwise */ - abstract canGoToStep(destinationIndex: number): Promise; + canGoToStep(destinationIndex: number): Promise; /** * Tries to transition to the wizard step, as denoted by the given destination index. - * If this is not possible, the current wizard step should be exited and then reentered with `MovingDirection.Stay` * - * @param destinationIndex The index of the destination step + * @param destinationIndex The index of the destination wizard step, which should be entered * @param preFinalize An event emitter, to be called before the step has been transitioned * @param postFinalize An event emitter, to be called after the step has been transitioned */ - abstract goToStep(destinationIndex: number, preFinalize?: EventEmitter, postFinalize?: EventEmitter): void; + goToStep(destinationIndex: number, preFinalize?: EventEmitter, postFinalize?: EventEmitter): void; /** - * Checks, whether the wizard step, located at the given index, is can be navigated to + * Checks, whether the wizard step, located at the given index, can be navigated to using the navigation bar. * * @param destinationIndex The index of the destination step * @returns True if the step can be navigated to, false otherwise */ - abstract isNavigable(destinationIndex: number): boolean; + isNavigable(destinationIndex: number): boolean; /** * Resets the state of this wizard. - * A reset transitions the wizard automatically to the first step and sets all steps as incomplete. - * In addition the whole wizard is set as incomplete */ - abstract reset(): void; + reset(): void; /** * Tries to transition the wizard to the previous step from the `currentStep` + * + * @param preFinalize An event emitter, to be called before the step has been transitioned + * @param postFinalize An event emitter, to be called after the step has been transitioned */ - goToPreviousStep(preFinalize?: EventEmitter, postFinalize?: EventEmitter): void { - if (this.wizardState.hasPreviousStep()) { - this.goToStep(this.wizardState.currentStepIndex - 1, preFinalize, postFinalize); - } - } + goToPreviousStep(preFinalize?: EventEmitter, postFinalize?: EventEmitter); /** * Tries to transition the wizard to the next step from the `currentStep` + * + * @param preFinalize An event emitter, to be called before the step has been transitioned + * @param postFinalize An event emitter, to be called after the step has been transitioned */ - goToNextStep(preFinalize?: EventEmitter, postFinalize?: EventEmitter): void { - if (this.wizardState.hasNextStep()) { - this.goToStep(this.wizardState.currentStepIndex + 1, preFinalize, postFinalize); - } - } + goToNextStep(preFinalize?: EventEmitter, postFinalize?: EventEmitter); } diff --git a/src/lib/navigation/navigation-mode.provider.ts b/src/lib/navigation/navigation-mode.provider.ts deleted file mode 100644 index 6631fdd6..00000000 --- a/src/lib/navigation/navigation-mode.provider.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {FreeNavigationMode} from './free-navigation-mode'; -import {NavigationMode} from './navigation-mode.interface'; -import {SemiStrictNavigationMode} from './semi-strict-navigation-mode'; -import {StrictNavigationMode} from './strict-navigation-mode'; -import {WizardState} from './wizard-state.model'; - -/** - * A factory method used to create [[NavigationMode]] instances - * - * @param navigationMode The name of the to be used navigation mode - * @param wizardState The wizard state of the wizard - * @returns The created [[NavigationMode]] - */ -export function navigationModeFactory(navigationMode: string, wizardState: WizardState): NavigationMode { - switch (navigationMode) { - case 'free': - return new FreeNavigationMode(wizardState); - case 'semi-strict': - return new SemiStrictNavigationMode(wizardState); - case 'strict': - default: - return new StrictNavigationMode(wizardState); - } -} diff --git a/src/lib/navigation/semi-strict-navigation-mode.ts b/src/lib/navigation/semi-strict-navigation-mode.ts index 6d1edd59..4c52337c 100644 --- a/src/lib/navigation/semi-strict-navigation-mode.ts +++ b/src/lib/navigation/semi-strict-navigation-mode.ts @@ -1,8 +1,5 @@ -import {EventEmitter} from '@angular/core'; -import {MovingDirection} from '../util/moving-direction.enum'; +import {BaseNavigationMode} from './base-navigation-mode.interface'; import {WizardCompletionStep} from '../util/wizard-completion-step.interface'; -import {NavigationMode} from './navigation-mode.interface'; -import {WizardState} from './wizard-state.model'; /** * A [[NavigationMode]], which allows the user to navigate with some limitations. @@ -12,116 +9,16 @@ import {WizardState} from './wizard-state.model'; * * @author Marc Arndt */ -export class SemiStrictNavigationMode extends NavigationMode { - /** - * Constructor - * - * @param wizardState The model/state of the wizard, that is configured with this navigation mode - */ - constructor(wizardState: WizardState) { - super(wizardState); - } - - /** - * Checks whether the wizard can be transitioned to the given destination step. - * A destination wizard step can be entered if: - * - it exists - * - the current step can be exited in the direction of the destination step - * - all "normal" wizard steps have been completed, are optional or selected, or the destination step isn't a completion step - * - * @param destinationIndex The index of the destination wizard step - * @returns True if the destination wizard step can be entered, false otherwise - */ - canGoToStep(destinationIndex: number): Promise { - const hasStep = this.wizardState.hasStep(destinationIndex); - - const movingDirection = this.wizardState.getMovingDirection(destinationIndex); - - const canExitCurrentStep = (previous: boolean) => { - return previous ? this.wizardState.currentStep.canExitStep(movingDirection) : Promise.resolve(false); - }; - - const canEnterDestinationStep = (previous: boolean) => { - return previous ? this.wizardState.getStepAtIndex(destinationIndex).canEnterStep(movingDirection) : Promise.resolve(false); - }; - - // provide the destination step as a lambda in case the index doesn't exist (i.e. hasStep === false) - const destinationStep = (previous: boolean) => { - if (previous) { - const allNormalStepsCompleted = this.wizardState.wizardSteps - .filter((step, index) => index < destinationIndex) - .every(step => step.completed || step.optional || step.selected); - - return Promise.resolve( - !(this.wizardState.getStepAtIndex(destinationIndex) instanceof WizardCompletionStep) || allNormalStepsCompleted); - } else { - return Promise.resolve(false); - } - }; - - return Promise.resolve(hasStep) - .then(canExitCurrentStep) - .then(canEnterDestinationStep) - .then(destinationStep); - } - - /** - * Tries to enter the wizard step with the given destination index. - * When entering the destination step, the following actions are done: - * - the old current step is set as completed - * - the old current step is set as unselected - * - the old current step is exited - * - the destination step is set as selected - * - the destination step is entered - * - * When the destination step couldn't be entered, the following actions are done: - * - the current step is exited and entered in the direction `MovingDirection.Stay` - * - * @param destinationIndex The index of the destination wizard step, which should be entered - * @param preFinalize An event emitter, to be called before the step has been transitioned - * @param postFinalize An event emitter, to be called after the step has been transitioned - */ - goToStep(destinationIndex: number, preFinalize?: EventEmitter, postFinalize?: EventEmitter): void { - this.canGoToStep(destinationIndex).then(navigationAllowed => { - if (navigationAllowed) { - // the current step can be exited in the given direction - const movingDirection: MovingDirection = this.wizardState.getMovingDirection(destinationIndex); - - /* istanbul ignore if */ - if (preFinalize) { - preFinalize.emit(); - } - - // leave current step - this.wizardState.currentStep.completed = true; - this.wizardState.currentStep.exit(movingDirection); - this.wizardState.currentStep.selected = false; - - this.wizardState.currentStepIndex = destinationIndex; - - // go to next step - this.wizardState.currentStep.enter(movingDirection); - this.wizardState.currentStep.selected = true; - - /* istanbul ignore if */ - if (postFinalize) { - postFinalize.emit(); - } - } else { - // if the current step can't be left, reenter the current step - this.wizardState.currentStep.exit(MovingDirection.Stay); - this.wizardState.currentStep.enter(MovingDirection.Stay); - } - }); - } +export class SemiStrictNavigationMode extends BaseNavigationMode { /** * @inheritDoc */ - isNavigable(destinationIndex: number): boolean { + public isNavigable(destinationIndex: number): boolean { if (this.wizardState.getStepAtIndex(destinationIndex) instanceof WizardCompletionStep) { // a completion step can only be entered, if all previous steps have been completed, are optional, or selected - return this.wizardState.wizardSteps.filter((step, index) => index < destinationIndex) + return this.wizardState.wizardSteps + .filter((step, index) => index < destinationIndex) .every(step => step.completed || step.optional || step.selected); } else { // a "normal" step can always be entered @@ -132,29 +29,17 @@ export class SemiStrictNavigationMode extends NavigationMode { /** * @inheritDoc */ - reset(): void { - // the wizard doesn't contain a step with the default step index - if (!this.wizardState.hasStep(this.wizardState.defaultStepIndex)) { - throw new Error(`The wizard doesn't contain a step with index ${this.wizardState.defaultStepIndex}`); + protected checkReset(): boolean { + if (!super.checkReset()) { + return false; } // the default step is a completion step and the wizard contains more than one step - const defaultCompletionStep = this.wizardState.getStepAtIndex(this.wizardState.defaultStepIndex) instanceof WizardCompletionStep && - this.wizardState.wizardSteps.length !== 1; - - if (defaultCompletionStep) { + const defaultCompletionStep = this.wizardState.getStepAtIndex(this.wizardState.defaultStepIndex) instanceof WizardCompletionStep; + if (defaultCompletionStep && this.wizardState.wizardSteps.length !== 1) { throw new Error(`The default step index ${this.wizardState.defaultStepIndex} references a completion step`); } - // reset the step internal state - this.wizardState.wizardSteps.forEach(step => { - step.completed = false; - step.selected = false; - }); - - // set the first step as the current step - this.wizardState.currentStepIndex = this.wizardState.defaultStepIndex; - this.wizardState.currentStep.selected = true; - this.wizardState.currentStep.enter(MovingDirection.Forwards); + return true; } } diff --git a/src/lib/navigation/strict-navigation-mode.ts b/src/lib/navigation/strict-navigation-mode.ts index 5d2c534d..77f6a726 100644 --- a/src/lib/navigation/strict-navigation-mode.ts +++ b/src/lib/navigation/strict-navigation-mode.ts @@ -1,155 +1,60 @@ -import {EventEmitter} from '@angular/core'; -import {MovingDirection} from '../util/moving-direction.enum'; -import {NavigationMode} from './navigation-mode.interface'; -import {WizardState} from './wizard-state.model'; +import {BaseNavigationMode} from './base-navigation-mode.interface'; /** * A [[NavigationMode]], which allows the user to navigate with strict limitations. * The user can only navigation to a given destination step, if: * - the current step can be exited in the direction of the destination step - * - all previous steps to the destination step have been completed or are optional * * @author Marc Arndt */ -export class StrictNavigationMode extends NavigationMode { +export class StrictNavigationMode extends BaseNavigationMode { + /** - * Constructor - * - * @param wizardState The state of the wizard, that is configured with this navigation mode + * @inheritDoc */ - constructor(wizardState: WizardState) { - super(wizardState); + protected canTransitionToStep(destinationIndex: number): boolean { + // navigation with [goToStep] is permitted if all previous steps to the destination step have been completed or are optional + return this.wizardState.wizardSteps + .filter((step, index) => index < destinationIndex && index !== this.wizardState.currentStepIndex) + .every(step => step.completed || step.optional); } /** - * Checks whether the wizard can be transitioned to the given destination step. - * A destination wizard step can be entered if: - * - it exists - * - the current step can be exited in the direction of the destination step - * - all previous steps to the destination step have been completed or are optional - * - * @param destinationIndex The index of the destination wizard step - * @returns True if the destination wizard step can be entered, false otherwise + * @inheritDoc */ - canGoToStep(destinationIndex: number): Promise { - const hasStep = this.wizardState.hasStep(destinationIndex); - - const movingDirection = this.wizardState.getMovingDirection(destinationIndex); - - const canExitCurrentStep = (previous: boolean) => { - return previous ? this.wizardState.currentStep.canExitStep(movingDirection) : Promise.resolve(false); - }; - - const canEnterDestinationStep = (previous: boolean) => { - return previous ? this.wizardState.getStepAtIndex(destinationIndex).canEnterStep(movingDirection) : Promise.resolve(false); - }; + protected transition(destinationIndex: number): void { + // set all steps after the destination step to incomplete + this.wizardState.wizardSteps + .filter((step, index) => this.wizardState.currentStepIndex > destinationIndex && index > destinationIndex) + .forEach(step => step.completed = false); - const allPreviousStepsComplete = (previous: boolean) => { - if (previous) { - return Promise.resolve(this.wizardState.wizardSteps - .filter((step, index) => index < destinationIndex && index !== this.wizardState.currentStepIndex) - .every(step => step.completed || step.optional) - ); - } else { - return Promise.resolve(false); - } - }; - - return Promise.resolve(hasStep) - .then(canExitCurrentStep) - .then(canEnterDestinationStep) - .then(allPreviousStepsComplete); + super.transition(destinationIndex); } /** - * Tries to enter the wizard step with the given destination index. - * When entering the destination step, the following actions are done: - * - the old current step is set as completed - * - the old current step is set as unselected - * - the old current step is exited - * - all steps between the old current step and the destination step are marked as incomplete - * - the destination step is set as selected - * - the destination step is entered - * - * When the destination step couldn't be entered, the following actions are done: - * - the current step is exited and entered in the direction `MovingDirection.Stay` - * - * @param destinationIndex The index of the destination wizard step, which should be entered - * @param preFinalize An event emitter, to be called before the step has been transitioned - * @param postFinalize An event emitter, to be called after the step has been transitioned + * @inheritDoc */ - goToStep(destinationIndex: number, preFinalize?: EventEmitter, postFinalize?: EventEmitter): void { - this.canGoToStep(destinationIndex).then(navigationAllowed => { - if (navigationAllowed) { - const movingDirection: MovingDirection = this.wizardState.getMovingDirection(destinationIndex); - - /* istanbul ignore if */ - if (preFinalize) { - preFinalize.emit(); - } - - // leave current step - this.wizardState.currentStep.completed = true; - this.wizardState.currentStep.exit(movingDirection); - this.wizardState.currentStep.selected = false; - - // set all steps after the destination step to incomplete - this.wizardState.wizardSteps - .filter((step, index) => this.wizardState.currentStepIndex > destinationIndex && index > destinationIndex) - .forEach(step => step.completed = false); - - this.wizardState.currentStepIndex = destinationIndex; - - // go to next step - this.wizardState.currentStep.enter(movingDirection); - this.wizardState.currentStep.selected = true; - - /* istanbul ignore if */ - if (postFinalize) { - postFinalize.emit(); - } - } else { - // if the current step can't be left, reenter the current step - this.wizardState.currentStep.exit(MovingDirection.Stay); - this.wizardState.currentStep.enter(MovingDirection.Stay); - } - }); - } - - isNavigable(destinationIndex: number): boolean { + public isNavigable(destinationIndex: number): boolean { // a wizard step can be navigated to through the navigation bar, iff it's located before the current wizard step return destinationIndex < this.wizardState.currentStepIndex; } /** - * Resets the state of this wizard. - * A reset transitions the wizard automatically to the first step and sets all steps as incomplete. - * In addition the whole wizard is set as incomplete + * @inheritDoc */ - reset(): void { - // the wizard doesn't contain a step with the default step index - if (!this.wizardState.hasStep(this.wizardState.defaultStepIndex)) { - throw new Error(`The wizard doesn't contain a step with index ${this.wizardState.defaultStepIndex}`); + protected checkReset(): boolean { + if (!super.checkReset()) { + return false; } // at least one step is before the default step, that is not optional const illegalDefaultStep = this.wizardState.wizardSteps .filter((step, index) => index < this.wizardState.defaultStepIndex) .some(step => !step.optional); - if (illegalDefaultStep) { throw new Error(`The default step index ${this.wizardState.defaultStepIndex} is located after a non optional step`); } - // reset the step internal state - this.wizardState.wizardSteps.forEach(step => { - step.completed = false; - step.selected = false; - }); - - // set the first step as the current step - this.wizardState.currentStepIndex = this.wizardState.defaultStepIndex; - this.wizardState.currentStep.selected = true; - this.wizardState.currentStep.enter(MovingDirection.Forwards); + return true; } } diff --git a/src/lib/navigation/wizard-state.model.ts b/src/lib/navigation/wizard-state.model.ts index 92d1b2ef..4b63cdee 100644 --- a/src/lib/navigation/wizard-state.model.ts +++ b/src/lib/navigation/wizard-state.model.ts @@ -2,7 +2,6 @@ import {Injectable} from '@angular/core'; import {MovingDirection} from '../util/moving-direction.enum'; import {WizardStep} from '../util/wizard-step.interface'; import {NavigationMode} from './navigation-mode.interface'; -import {navigationModeFactory} from './navigation-mode.provider'; /** * The internal model/state of a wizard. @@ -99,12 +98,12 @@ export class WizardState { } /** - * Updates the navigation mode to the navigation mode with the given name + * Updates the navigation mode * - * @param updatedNavigationMode The name of the new navigation mode + * @param navigationMode An already constructed `NavigationMode` instance */ - updateNavigationMode(updatedNavigationMode: string): void { - this.navigationMode = navigationModeFactory(updatedNavigationMode, this); + public updateNavigationMode(navigationMode: NavigationMode): void { + this.navigationMode = navigationMode; } /** @@ -112,7 +111,7 @@ export class WizardState { * * @param updatedWizardSteps The updated wizard steps */ - updateWizardSteps(updatedWizardSteps: Array): void { + public updateWizardSteps(updatedWizardSteps: Array): void { // the wizard is currently not in the initialization phase if (this.wizardSteps.length > 0 && this.currentStepIndex > -1) { this.currentStepIndex = updatedWizardSteps.indexOf(this.wizardSteps[this.currentStepIndex]); @@ -127,7 +126,7 @@ export class WizardState { * @param stepIndex The to be checked index of a step inside this wizard * @returns True if the given `stepIndex` is contained inside this wizard, false otherwise */ - hasStep(stepIndex: number): boolean { + public hasStep(stepIndex: number): boolean { return this.wizardSteps.length > 0 && 0 <= stepIndex && stepIndex < this.wizardSteps.length; } @@ -136,7 +135,7 @@ export class WizardState { * * @returns True if this wizard has a previous step before the current step */ - hasPreviousStep(): boolean { + public hasPreviousStep(): boolean { return this.hasStep(this.currentStepIndex - 1); } @@ -145,7 +144,7 @@ export class WizardState { * * @returns True if this wizard has a next step after the current step */ - hasNextStep(): boolean { + public hasNextStep(): boolean { return this.hasStep(this.currentStepIndex + 1); } @@ -154,7 +153,7 @@ export class WizardState { * * @returns True if the wizard is currently inside its last step */ - isLastStep(): boolean { + public isLastStep(): boolean { return this.wizardSteps.length > 0 && this.currentStepIndex === this.wizardSteps.length - 1; } @@ -166,7 +165,7 @@ export class WizardState { * @returns The found [[WizardStep]] at the given index `stepIndex` * @throws An `Error` is thrown, if the given index `stepIndex` doesn't exist */ - getStepAtIndex(stepIndex: number): WizardStep { + public getStepAtIndex(stepIndex: number): WizardStep { if (!this.hasStep(stepIndex)) { throw new Error(`Expected a known step, but got stepIndex: ${stepIndex}.`); } @@ -181,7 +180,7 @@ export class WizardState { * @param stepId The given step id * @returns The found index of a step with the given step id, or `-1` if no step with the given id is included in the wizard */ - getIndexOfStepWithId(stepId: string): number { + public getIndexOfStepWithId(stepId: string): number { return this.wizardSteps.findIndex(step => step.stepId === stepId); } @@ -192,7 +191,7 @@ export class WizardState { * @param step The given [[WizardStep]] * @returns The found index of `step` or `-1` if the step is not included in the wizard */ - getIndexOfStep(step: WizardStep): number { + public getIndexOfStep(step: WizardStep): number { return this.wizardSteps.indexOf(step); } @@ -202,7 +201,7 @@ export class WizardState { * @param destinationStep The given destination step * @returns The calculated [[MovingDirection]] */ - getMovingDirection(destinationStep: number): MovingDirection { + public getMovingDirection(destinationStep: number): MovingDirection { let movingDirection: MovingDirection; if (destinationStep > this.currentStepIndex) { diff --git a/src/lib/util/step-id.interface.spec.ts b/src/lib/util/step-id.interface.spec.ts index c9cdba15..dc5e2c12 100644 --- a/src/lib/util/step-id.interface.spec.ts +++ b/src/lib/util/step-id.interface.spec.ts @@ -36,7 +36,7 @@ class WizardTestComponent { public eventLog: Array = []; - finalizeStep(stepIndex: number): void { + public finalizeStep(stepIndex: number): void { this.eventLog.push(`finalize ${stepIndex}`); } } diff --git a/src/lib/util/step-index.interface.spec.ts b/src/lib/util/step-index.interface.spec.ts index 7f5759a0..0150d53d 100644 --- a/src/lib/util/step-index.interface.spec.ts +++ b/src/lib/util/step-index.interface.spec.ts @@ -36,7 +36,7 @@ class WizardTestComponent { public eventLog: Array = []; - finalizeStep(stepIndex: number): void { + public finalizeStep(stepIndex: number): void { this.eventLog.push(`finalize ${stepIndex}`); } } diff --git a/src/lib/util/step-offset.interface.spec.ts b/src/lib/util/step-offset.interface.spec.ts index cf258e90..11a3fa76 100644 --- a/src/lib/util/step-offset.interface.spec.ts +++ b/src/lib/util/step-offset.interface.spec.ts @@ -36,7 +36,7 @@ class WizardTestComponent { public eventLog: Array = []; - finalizeStep(stepIndex: number): void { + public finalizeStep(stepIndex: number): void { this.eventLog.push(`finalize ${stepIndex}`); } } diff --git a/src/lib/util/wizard-completion-step.interface.spec.ts b/src/lib/util/wizard-completion-step.interface.spec.ts index e2b777bc..8257418a 100644 --- a/src/lib/util/wizard-completion-step.interface.spec.ts +++ b/src/lib/util/wizard-completion-step.interface.spec.ts @@ -28,11 +28,11 @@ class WizardTestComponent { public eventLog: Array = []; - enterInto(direction: MovingDirection, destination: number): void { + public enterInto(direction: MovingDirection, destination: number): void { this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); } - exitFrom(direction: MovingDirection, source: number): void { + public exitFrom(direction: MovingDirection, source: number): void { this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); } } diff --git a/src/lib/util/wizard-step.interface.spec.ts b/src/lib/util/wizard-step.interface.spec.ts index 3f091e07..f16f901e 100644 --- a/src/lib/util/wizard-step.interface.spec.ts +++ b/src/lib/util/wizard-step.interface.spec.ts @@ -40,11 +40,11 @@ class WizardTestComponent { public eventLog: Array = []; - enterInto(direction: MovingDirection, destination: number): void { + public enterInto(direction: MovingDirection, destination: number): void { this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); } - exitFrom(direction: MovingDirection, source: number): void { + public exitFrom(direction: MovingDirection, source: number): void { this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); } } diff --git a/tslint.json b/tslint.json index 95040123..d0b7f272 100644 --- a/tslint.json +++ b/tslint.json @@ -31,7 +31,7 @@ true, 140 ], - "member-access": false, + "member-access": true, "member-ordering": [ true, {