Skip to content

Commit

Permalink
Support [navigateForward]="visited" (#228)
Browse files Browse the repository at this point in the history
* Enhance existing tests with checks for which steps are navigable (`checkWizardNavigableState`)
* Implement navigateForward="visited", update README and tests
* Rename helper function checkWizardNavigableState → checkWizardNavigableSteps
  • Loading branch information
earshinov authored and madoar committed Aug 11, 2019
1 parent 4bb54a6 commit 70b638d
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 9 deletions.
15 changes: 12 additions & 3 deletions README.md
Expand Up @@ -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 `<aw-wizard>` 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
<aw-wizard [awNavigationMode] navigateBackward="allow" navigateForward="allow">...</aw-wizard>
```

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
Expand Down Expand Up @@ -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`.
Expand Down
4 changes: 2 additions & 2 deletions src/lib/directives/navigation-mode.directive.ts
Expand Up @@ -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) { }

Expand All @@ -95,4 +96,3 @@ export class NavigationModeDirective implements OnChanges {
}

}

7 changes: 5 additions & 2 deletions src/lib/navigation/configurable-navigation-mode.ts
Expand Up @@ -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"`
*/
Expand All @@ -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';
Expand Down Expand Up @@ -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)
Expand All @@ -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}`);
}
Expand Down
197 changes: 197 additions & 0 deletions 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: `
<aw-wizard [awNavigationMode] navigateForward="visited">
<aw-wizard-step stepTitle='Steptitle 1'>
Step 1
</aw-wizard-step>
<aw-wizard-step stepTitle='Steptitle 2'>
Step 2
</aw-wizard-step>
<aw-wizard-step stepTitle='Steptitle 3'>
Step 3
</aw-wizard-step>
</aw-wizard>
`
})
class WizardTestComponent {
@ViewChild(WizardComponent)
public wizard: WizardComponent;
}

describe('Wizard navigation with navigateForward=visited', () => {
let wizardTestFixture: ComponentFixture<WizardTestComponent>;

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]);
}));
});

0 comments on commit 70b638d

Please sign in to comment.