diff --git a/README.md b/README.md index 868dcfce..315efe5f 100644 --- a/README.md +++ b/README.md @@ -258,13 +258,22 @@ The only exception to this rule are optional steps, which a user can skip. Using the navigation bar, the user can navigate back to steps they already visited. You can alter this behavior by applying to the `` element an additional `[awNavigationMode]` directive, which can be used in two ways. -The easiest option is to tweak the default navigation mode with `[navigateBackward]` and/or `[navigateForward]` inputs which control the navigation bar. Valid options for these inputs are `'allow'` and `'deny`'. Take notice that the `'allow'` option still respects step exit conditions. Also, the completion step still only becomes enterable after all previous steps are completed. Example usage: +The easiest option is to tweak the default navigation mode with `[navigateBackward]` and/or `[navigateForward]` inputs which control the navigation bar and have the following options: + +| Parameter name | Possible Values | Default Value | +| ----------------------------- | ---------------------------------------------------------------------------------------------------- | ------------- | +| [navigateBackward] | `'allow'|'deny'` | `'deny'` | +| [navigateForward] | `'allow'|'deny'|'visited'` | `'allow'` | + +Take notice that the `'allow'` and `'visited'` options still respect step exit conditions. Also, the completion step still only becomes enterable after all previous steps are completed. Example usage: ```html ... ``` -If changes you need are more radical, you can define your own navigation mode. In order to do this, create a class implementing the `NavigationMode` interface and pass an instance of this class into the `[awNavigationMode]` directive. This takes priority over `[navigateBackward]` and `[navigateForward]` inputs. Example usage: +If changes you need are more radical, you can define your own navigation mode. In order to do this, create a class implementing the `NavigationMode` interface and pass an instance of this class into the `[awNavigationMode]` directive. +This takes priority over `[navigateBackward]` and `[navigateForward]` inputs. +Example usage: custom-navigation-mode.ts: ```typescript @@ -307,7 +316,7 @@ Possible `awNavigationMode` parameters: | ----------------------------- | ---------------------------------------------------------------------------------------------------- | ------------- | | [awNavigationMode] | `NavigationMode` | `null` | | [navigateBackward] | `'allow'|'deny'` | `'deny'` | -| [navigateForward] | `'allow'|'deny'` | `'allow'` | +| [navigateForward] | `'allow'|'deny'|'visited'` | `'allow'` | ### \[awEnableBackLinks\] In some cases it may be required that the user is allowed to leave an entered `aw-wizard-completion-step`. diff --git a/src/lib/directives/navigation-mode.directive.ts b/src/lib/directives/navigation-mode.directive.ts index af494c19..4a029e2c 100644 --- a/src/lib/directives/navigation-mode.directive.ts +++ b/src/lib/directives/navigation-mode.directive.ts @@ -77,9 +77,10 @@ export class NavigationModeDirective implements OnChanges { * * - `navigateForward="deny"` -- the steps are not navigable * - `navigateForward="allow"` -- the steps are navigable + * - `navigateForward="visited"` -- a step is navigable iff it was already visited before */ @Input() - public navigateForward: 'allow'|'deny'|null; + public navigateForward: 'allow'|'deny'|'visited'|null; constructor(private wizard: WizardComponent) { } @@ -95,4 +96,3 @@ export class NavigationModeDirective implements OnChanges { } } - diff --git a/src/lib/navigation/configurable-navigation-mode.ts b/src/lib/navigation/configurable-navigation-mode.ts index f54a07b1..8548669c 100644 --- a/src/lib/navigation/configurable-navigation-mode.ts +++ b/src/lib/navigation/configurable-navigation-mode.ts @@ -18,6 +18,7 @@ import {WizardCompletionStep} from '../util/wizard-completion-step.interface'; * * - `"deny"` -- the steps are not navigable * - `"allow"` -- the steps are navigable + * - `"visited"` -- a step is navigable iff it was already visited before * - If the corresponding constructor argument is omitted or is `null` or `undefined`, * then the default value is applied which is `"allow"` */ @@ -31,7 +32,7 @@ export class ConfigurableNavigationMode extends BaseNavigationMode { */ constructor( private navigateBackward: 'allow'|'deny'|null = null, - private navigateForward: 'allow'|'deny'|null = null, + private navigateForward: 'allow'|'deny'|'visited'|null = null, ) { super(); this.navigateBackward = this.navigateBackward || 'allow'; @@ -74,7 +75,8 @@ export class ConfigurableNavigationMode extends BaseNavigationMode { */ public isNavigable(wizard: WizardComponent, destinationIndex: number): boolean { // Check if the destination step can be navigated to - if (wizard.getStepAtIndex(destinationIndex) instanceof WizardCompletionStep) { + const destinationStep = wizard.getStepAtIndex(destinationIndex); + if (destinationStep instanceof WizardCompletionStep) { // A completion step can only be entered, if all previous steps have been completed, are optional, or selected const previousStepsCompleted = wizard.wizardSteps .filter((step, index) => index < destinationIndex) @@ -98,6 +100,7 @@ export class ConfigurableNavigationMode extends BaseNavigationMode { switch (this.navigateForward) { case 'allow': return true; case 'deny': return false; + case 'visited': return destinationStep.completed; default: throw new Error(`Invalid value for navigateForward: ${this.navigateForward}`); } diff --git a/src/lib/navigation/wizard-navigation-allow-forward-visited.spec.ts b/src/lib/navigation/wizard-navigation-allow-forward-visited.spec.ts new file mode 100644 index 00000000..2764266f --- /dev/null +++ b/src/lib/navigation/wizard-navigation-allow-forward-visited.spec.ts @@ -0,0 +1,197 @@ +import {Component, ViewChild} from '@angular/core'; +import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {ArchwizardModule} from '../archwizard.module'; +import {WizardComponent} from '../components/wizard.component'; +import {checkWizardState, checkWizardNavigableSteps} from '../util/test-utils'; + +@Component({ + selector: 'aw-test-wizard', + template: ` + + + Step 1 + + + Step 2 + + + Step 3 + + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; +} + +describe('Wizard navigation with navigateForward=visited', () => { + let wizardTestFixture: ComponentFixture; + + let wizardTest: WizardTestComponent; + let wizard: WizardComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [ArchwizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTestFixture.detectChanges(); + + wizardTest = wizardTestFixture.componentInstance; + wizard = wizardTest.wizard; + }); + + it('should return correct can go to step', async(() => { + wizard.canGoToStep(-1).then(result => expect(result).toBe(false)); + wizard.canGoToStep(0).then(result => expect(result).toBe(true)); + wizard.canGoToStep(1).then(result => expect(result).toBe(true)); + wizard.canGoToStep(2).then(result => expect(result).toBe(false)); + wizard.canGoToStep(3).then(result => expect(result).toBe(false)); + })); + + it('should go to step', fakeAsync(() => { + checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); + + wizard.goToStep(1); + tick(); + wizardTestFixture.detectChanges(); + + checkWizardState(wizard, 1, false, [0], false); + checkWizardNavigableSteps(wizard, 1, [0]); + + wizard.goToStep(2); + tick(); + wizardTestFixture.detectChanges(); + + checkWizardState(wizard, 2, false, [0, 1], false); + checkWizardNavigableSteps(wizard, 2, [0, 1]); + + wizard.goToStep(0); + tick(); + wizardTestFixture.detectChanges(); + + // If forward navigation is allowed, visited steps after + // the selected step are still considered completed + checkWizardState(wizard, 0, true, [0, 1, 2], true); + checkWizardNavigableSteps(wizard, 0, [1, 2]); + + wizard.goToStep(1); + tick(); + wizardTestFixture.detectChanges(); + + checkWizardState(wizard, 1, true, [0, 1, 2], true); + checkWizardNavigableSteps(wizard, 1, [0, 2]); + + wizard.goToStep(2); + tick(); + wizardTestFixture.detectChanges(); + + checkWizardState(wizard, 2, true, [0, 1, 2], true); + checkWizardNavigableSteps(wizard, 2, [0, 1]); + + wizard.goToStep(1); + tick(); + wizardTestFixture.detectChanges(); + + checkWizardState(wizard, 1, true, [0, 1, 2], true); + checkWizardNavigableSteps(wizard, 1, [0, 2]); + })); + + it('should go to next step', fakeAsync(() => { + wizard.goToNextStep(); + tick(); + wizardTestFixture.detectChanges(); + + checkWizardState(wizard, 1, false, [0], false); + checkWizardNavigableSteps(wizard, 1, [0]); + })); + + it('should go to previous step', fakeAsync(() => { + checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); + + wizard.goToStep(1); + tick(); + wizardTestFixture.detectChanges(); + + checkWizardState(wizard, 1, false, [0], false); + checkWizardNavigableSteps(wizard, 1, [0]); + + wizard.goToPreviousStep(); + tick(); + wizardTestFixture.detectChanges(); + + // If forward navigation is allowed, visited steps after + // the selected step are still considered completed + checkWizardState(wizard, 0, true, [0, 1], false); + checkWizardNavigableSteps(wizard, 0, [1]); + })); + + it('should stay at the current step', fakeAsync(() => { + expect(wizard.getStepAtIndex(0).completed).toBe(false); + + wizard.goToPreviousStep(); + tick(); + wizardTestFixture.detectChanges(); + + checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); + + wizard.goToStep(-1); + tick(); + wizardTestFixture.detectChanges(); + + checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); + + wizard.goToStep(0); + tick(); + wizardTestFixture.detectChanges(); + + checkWizardState(wizard, 0, true, [0], false); + checkWizardNavigableSteps(wizard, 0, []); + })); + + it('should reset the wizard correctly', fakeAsync(() => { + wizard.goToNextStep(); + tick(); + wizardTestFixture.detectChanges(); + + wizard.goToNextStep(); + tick(); + wizardTestFixture.detectChanges(); + + checkWizardState(wizard, 2, false, [0, 1], false); + checkWizardNavigableSteps(wizard, 2, [0, 1]); + + wizard.reset(); + + checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); + + wizard.defaultStepIndex = -1; + expect(() => wizard.reset()) + .toThrow(new Error(`The wizard doesn't contain a step with index -1`)); + + checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); + + wizard.defaultStepIndex = 1; + wizard.reset(); + + checkWizardState(wizard, 1, false, [], false); + checkWizardNavigableSteps(wizard, 1, [0]); + + wizard.defaultStepIndex = 2; + wizard.reset(); + + checkWizardState(wizard, 2, false, [], false); + checkWizardNavigableSteps(wizard, 2, [0, 1]); + })); +}); diff --git a/src/lib/navigation/wizard-navigation-allow-forward.spec.ts b/src/lib/navigation/wizard-navigation-allow-forward.spec.ts index 50074ac1..da563bdf 100644 --- a/src/lib/navigation/wizard-navigation-allow-forward.spec.ts +++ b/src/lib/navigation/wizard-navigation-allow-forward.spec.ts @@ -2,7 +2,7 @@ import {Component, ViewChild} from '@angular/core'; import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {ArchwizardModule} from '../archwizard.module'; import {WizardComponent} from '../components/wizard.component'; -import {checkWizardState} from '../util/test-utils'; +import {checkWizardState, checkWizardNavigableSteps} from '../util/test-utils'; @Component({ selector: 'aw-test-wizard', @@ -56,18 +56,21 @@ describe('Wizard navigation with navigateForward=allow', () => { it('should go to step', fakeAsync(() => { checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, [1, 2]); wizard.goToStep(1); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 1, false, [0], false); + checkWizardNavigableSteps(wizard, 1, [0, 2]); wizard.goToStep(2); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 2, false, [0, 1], false); + checkWizardNavigableSteps(wizard, 2, [0, 1]); wizard.goToStep(0); tick(); @@ -76,24 +79,28 @@ describe('Wizard navigation with navigateForward=allow', () => { // If forward navigation is allowed, visited steps after // the selected step are still considered completed checkWizardState(wizard, 0, true, [0, 1, 2], true); + checkWizardNavigableSteps(wizard, 0, [1, 2]); wizard.goToStep(1); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 1, true, [0, 1, 2], true); + checkWizardNavigableSteps(wizard, 1, [0, 2]); wizard.goToStep(2); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 2, true, [0, 1, 2], true); + checkWizardNavigableSteps(wizard, 2, [0, 1]); wizard.goToStep(1); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 1, true, [0, 1, 2], true); + checkWizardNavigableSteps(wizard, 1, [0, 2]); })); it('should go to next step', fakeAsync(() => { @@ -102,16 +109,19 @@ describe('Wizard navigation with navigateForward=allow', () => { wizardTestFixture.detectChanges(); checkWizardState(wizard, 1, false, [0], false); + checkWizardNavigableSteps(wizard, 1, [0, 2]); })); it('should go to previous step', fakeAsync(() => { checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, [1, 2]); wizard.goToStep(1); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 1, false, [0], false); + checkWizardNavigableSteps(wizard, 1, [0, 2]); wizard.goToPreviousStep(); tick(); @@ -120,6 +130,7 @@ describe('Wizard navigation with navigateForward=allow', () => { // If forward navigation is allowed, visited steps after // the selected step are still considered completed checkWizardState(wizard, 0, true, [0, 1], false); + checkWizardNavigableSteps(wizard, 0, [1, 2]); })); it('should stay at the current step', fakeAsync(() => { @@ -130,18 +141,21 @@ describe('Wizard navigation with navigateForward=allow', () => { wizardTestFixture.detectChanges(); checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, [1, 2]); wizard.goToStep(-1); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, [1, 2]); wizard.goToStep(0); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 0, true, [0], false); + checkWizardNavigableSteps(wizard, 0, [1, 2]); })); it('should reset the wizard correctly', fakeAsync(() => { @@ -154,25 +168,30 @@ describe('Wizard navigation with navigateForward=allow', () => { wizardTestFixture.detectChanges(); checkWizardState(wizard, 2, false, [0, 1], false); + checkWizardNavigableSteps(wizard, 2, [0, 1]); wizard.reset(); checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, [1, 2]); wizard.defaultStepIndex = -1; expect(() => wizard.reset()) .toThrow(new Error(`The wizard doesn't contain a step with index -1`)); checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, [1, 2]); wizard.defaultStepIndex = 1; wizard.reset(); checkWizardState(wizard, 1, false, [], false); + checkWizardNavigableSteps(wizard, 1, [0, 2]); wizard.defaultStepIndex = 2; wizard.reset(); checkWizardState(wizard, 2, false, [], false); + checkWizardNavigableSteps(wizard, 2, [0, 1]); })); }); diff --git a/src/lib/navigation/wizard-navigation.spec.ts b/src/lib/navigation/wizard-navigation.spec.ts index 91aadbe5..65de09ca 100644 --- a/src/lib/navigation/wizard-navigation.spec.ts +++ b/src/lib/navigation/wizard-navigation.spec.ts @@ -2,7 +2,7 @@ import {Component, ViewChild} from '@angular/core'; import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {ArchwizardModule} from '../archwizard.module'; import {WizardComponent} from '../components/wizard.component'; -import {checkWizardState} from '../util/test-utils'; +import {checkWizardState, checkWizardNavigableSteps} from '../util/test-utils'; @Component({ selector: 'aw-test-wizard', @@ -56,42 +56,49 @@ describe('Wizard navigation', () => { it('should go to step', fakeAsync(() => { checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); wizard.goToStep(1); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 1, false, [0], false); + checkWizardNavigableSteps(wizard, 1, [0]); wizard.goToStep(2); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 2, false, [0, 1], false); + checkWizardNavigableSteps(wizard, 2, [0, 1]); wizard.goToStep(0); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 0, true, [0], false); + checkWizardNavigableSteps(wizard, 0, []); wizard.goToStep(1); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 1, false, [0], false); + checkWizardNavigableSteps(wizard, 1, [0]); wizard.goToStep(2); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 2, false, [0, 1], false); + checkWizardNavigableSteps(wizard, 2, [0, 1]); wizard.goToStep(1); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 1, true, [0, 1], false); + checkWizardNavigableSteps(wizard, 1, [0]); })); it('should go to next step', fakeAsync(() => { @@ -100,22 +107,26 @@ describe('Wizard navigation', () => { wizardTestFixture.detectChanges(); checkWizardState(wizard, 1, false, [0], false); + checkWizardNavigableSteps(wizard, 1, [0]); })); it('should go to previous step', fakeAsync(() => { checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); wizard.goToStep(1); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 1, false, [0], false); + checkWizardNavigableSteps(wizard, 1, [0]); wizard.goToPreviousStep(); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 0, true, [0], false); + checkWizardNavigableSteps(wizard, 0, []); })); it('should stay at the current step', fakeAsync(() => { @@ -126,18 +137,21 @@ describe('Wizard navigation', () => { wizardTestFixture.detectChanges(); checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); wizard.goToStep(-1); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); wizard.goToStep(0); tick(); wizardTestFixture.detectChanges(); checkWizardState(wizard, 0, true, [0], false); + checkWizardNavigableSteps(wizard, 0, []); })); it('should reset the wizard correctly', fakeAsync(() => { @@ -150,25 +164,30 @@ describe('Wizard navigation', () => { wizardTestFixture.detectChanges(); checkWizardState(wizard, 2, false, [0, 1], false); + checkWizardNavigableSteps(wizard, 2, [0, 1]); wizard.reset(); checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); wizard.defaultStepIndex = -1; expect(() => wizard.reset()) .toThrow(new Error(`The wizard doesn't contain a step with index -1`)); checkWizardState(wizard, 0, false, [], false); + checkWizardNavigableSteps(wizard, 0, []); wizard.defaultStepIndex = 1; wizard.reset(); checkWizardState(wizard, 1, false, [], false); + checkWizardNavigableSteps(wizard, 1, [0]); wizard.defaultStepIndex = 2; wizard.reset(); checkWizardState(wizard, 2, false, [], false); + checkWizardNavigableSteps(wizard, 2, [0, 1]); })); }); diff --git a/src/lib/util/test-utils.ts b/src/lib/util/test-utils.ts index dd7e879c..de575827 100644 --- a/src/lib/util/test-utils.ts +++ b/src/lib/util/test-utils.ts @@ -40,3 +40,28 @@ export function checkWizardState( expect(wizard.completed).toBe(wizardCompleted, `expected wizard ${wizardCompleted ? 'to be completed' : 'not to be completed'}`); } + + +/** + * Check which wizard steps are navigable using the navigation bar + * + * @param wizard Wizard component under test + * @param selectedStepIndex Expected selected step index + * @param navigableStepIndexes Array of step indexes expected to be navigable using the navigation bar + */ +export function checkWizardNavigableSteps( + wizard: WizardComponent, + selectedStepIndex: number, + navigableStepIndexes: number[], +): void { + expect(wizard.currentStepIndex).toBe(selectedStepIndex, `expected current step index to be ${selectedStepIndex}`); + + wizard.wizardSteps.forEach((step, index) => { + // Only the selected step should be selected + expect(step.selected).toBe(index === selectedStepIndex, `expected only step ${index} to be selected`); + + // Check navigable step indexes + expect(wizard.isNavigable(index)).toBe(navigableStepIndexes.includes(index), + `expected step ${index} ${navigableStepIndexes.includes(index) ? 'to be navigable' : 'not to be navigable'}`); + }); +}