diff --git a/README.md b/README.md index 12e7c6a0..e4ec8186 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,10 @@ Possible `` parameters: `ng2-archwizard` contains two ways to define a wizard step. One of these two ways is by using the `` component. +#### \[stepId\] +A wizard step can have its own unique id. +This id can then be used to navigate to the step. + #### \[stepTitle\] A wizard step needs to contain a title, which is shown in the navigation bar of the wizard. To set the title of a step, add the `stepTitle` input attribute, with the choosen step title, to the definition of your wizard step. @@ -206,6 +210,7 @@ Possible `` parameters: | Parameter name | Possible Values | Default Value | | ----------------------------- | ---------------------------------------------------------------------------------------------------- | ------------- | +| [stepId] | `string` | null | | [stepTitle] | `string` | null | | [navigationSymbol] | `string` | '' | | [navigationSymbolFontFamily] | `string` | null | @@ -229,6 +234,7 @@ Possible `` parameters: | Parameter name | Possible Values | Default Value | | ----------------------------- | ---------------------------------------------------------------------------------------------------- | ------------- | +| [stepId] | `string` | null | | [stepTitle] | `string` | null | | [navigationSymbol] | `string` | '' | | [navigationSymbolFontFamily] | `string` | null | @@ -284,27 +290,48 @@ When attaching the `awSelectedStep` directive to an arbitrary wizard step, it wi which is shown directly after the wizard startup. ### \[awGoToStep\] -`ng2-archwizard` has three directives, that allow moving between steps. +`ng2-archwizard` has three directives, which allow moving between steps. These directives are the `awPreviousStep`, `asNextStep` and `awGoToStep` directives. -The `awGoToStep` directive needs to receive an argument, that tells the wizard to which step it should change, -when the element with the `awGoToStep` directive has been clicked. -This argument has to be the zero-based index of the destination step: - -```html - -``` -In the previous example the button moves the user automatically to the third step, after the user pressed onto it. -This makes it possible to directly jump to all already completed steps and to the first not completed optional or default (not optional) next step, -which will set the current as completed and makes it possible to jump over steps defined as optional steps. +The `awGoToStep` directive needs to receive an input, which tells the wizard, to which step it should navigate, +when the element with the `awGoToStep` directive has been clicked. -Alternatively to an absolute step index, it's also possible to set the destination wizard step as an offset to the source step: -```html - -``` -In this example a click on the "Go to the third Step" button will move the user to the next step compared to the step the button belongs to. -If the button is for example part of the second step, a click on it will move the user to the third step. -When using offsets it's important to use `[]` around the `awGoToStep` directive to tell angular that the argument is to be interpreted as javascript. +This input accepts different arguments: + +- a destination **step index**: + One possible argument for the input is a destination step index. + A destination step index is always zero-based, i.e. the index of the first step inside the wizard + is always zero. + + To pass a destination step index to an `awGoToStep` directive, + you need to pass the following json object to the directive: + + ```html + + ``` +- a destination **step id**: + Another possible argument for the input is a the unique step id of the destination step. + This step id can be set for all wizard steps through their input `[stepId]`. + + To pass a unique destination step id to an `awGoToStep` directive, + you need to pass the following json object to the directive: + + ```html + + ``` +- a **step offset** between the current step and the destination step: + Alternatively to an absolute step index or an unique step id, + it's also possible to set the destination wizard step as an offset to the source step: + + ```html + + ``` + +In all above examples a click on the "Go to the third Step" button will move +the user to the next step (the third step) compared to the step the button belongs to (the second step). +If the button is part of the second step, a click on it will move the user to the third step. + +In all above cases it's important to use `[]` around the `awGoToStep` directive to tell angular that the argument is to be interpreted as javascript. In addition to a static value you can also pass a local variable from your component typescript class, that contains to which step a click on the element should change the current step of the wizard. @@ -329,7 +356,7 @@ Possible parameters: | Parameter name | Possible Values | Default Value | | ----------------- | ----------------------------------------------------------------- | ------------- | -| [goToStep] | `WizardStep | StepOffset | number | string` | null | +| [goToStep] | `WizardStep | StepOffset | StepIndex | StepId` | null | | (preFinalize) | `function(): void` | null | | (postFinalize) | `function(): void` | null | | (finalize) | `function(): void` | null | @@ -399,6 +426,7 @@ Possible `awWizardStep` parameters: | Parameter name | Possible Values | Default Value | | ----------------------------- | ---------------------------------------------------------------------------------------------------- | ------------- | +| [stepId] | `string` | null | | [stepTitle] | `string` | null | | [navigationSymbol] | `string` | '' | | [navigationSymbolFontFamily] | `string` | null | @@ -424,10 +452,11 @@ that contains the wizard completion step. ``` #### Parameter overview -Possible `wizardCompletionStep` parameters: +Possible `awWizardCompletionStep` parameters: | Parameter name | Possible Values | Default Value | | ----------------------------- | ---------------------------------------------------------------------------------------------------- | ------------- | +| [stepId] | `string` | null | | [stepTitle] | `string` | null | | [navigationSymbol] | `string` | '' | | [navigationSymbolFontFamily] | `string` | null | diff --git a/src/directives/go-to-step.directive.spec.ts b/src/directives/go-to-step.directive.spec.ts index fab98960..07cf88f1 100644 --- a/src/directives/go-to-step.directive.spec.ts +++ b/src/directives/go-to-step.directive.spec.ts @@ -3,7 +3,7 @@ */ import {GoToStepDirective} from './go-to-step.directive'; import {Component} from '@angular/core'; -import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {async, ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {ArchwizardModule} from '../archwizard.module'; import {WizardState} from '../navigation/wizard-state.model'; @@ -15,13 +15,13 @@ import {NavigationMode} from '../navigation/navigation-mode.interface'; Step 1 - - + + Step 2 - + @@ -79,168 +79,12 @@ describe('GoToStepDirective', () => { expect(wizardTestFixture.debugElement.queryAll(By.directive(GoToStepDirective)).length).toBe(9); }); - it('should move to step correctly', fakeAsync(() => { - const firstStepGoToButton = wizardTestFixture.debugElement.query( - By.css('aw-wizard-step[stepTitle="Steptitle 1"] > button:nth-child(2)')).nativeElement; - const secondStepGoToButton = wizardTestFixture.debugElement.query( - By.css('aw-wizard-step[stepTitle="Steptitle 2"] > button')).nativeElement; - - const wizardSteps = wizardState.wizardSteps; - - expect(wizardState.currentStepIndex).toBe(0); - expect(wizardSteps[0].selected).toBe(true); - expect(wizardSteps[1].selected).toBe(false); - expect(wizardSteps[2].selected).toBe(false); - - // click button - firstStepGoToButton.click(); - tick(); - wizardTestFixture.detectChanges(); - - expect(wizardState.currentStepIndex).toBe(1); - expect(wizardSteps[0].selected).toBe(false); - expect(wizardSteps[1].selected).toBe(true); - expect(wizardSteps[2].selected).toBe(false); - - // click button - secondStepGoToButton.click(); - tick(); - wizardTestFixture.detectChanges(); - - expect(wizardState.currentStepIndex).toBe(2); - expect(wizardSteps[0].selected).toBe(false); - expect(wizardSteps[1].selected).toBe(false); - expect(wizardSteps[2].selected).toBe(true); - })); - - it('should jump over an optional step correctly', fakeAsync(() => { - const firstStepGoToButton = wizardTestFixture.debugElement.query( - By.css('aw-wizard-step[stepTitle="Steptitle 1"] > button:nth-child(3)')).nativeElement; - const thirdStepGoToButton = wizardTestFixture.debugElement.query( - By.css('aw-wizard-step[stepTitle="Steptitle 3"] > button')).nativeElement; - - const wizardSteps = wizardState.wizardSteps; - - expect(wizardState.currentStepIndex).toBe(0); - expect(wizardSteps[0].selected).toBe(true); - expect(wizardSteps[1].selected).toBe(false); - expect(wizardSteps[2].selected).toBe(false); - - // click button - firstStepGoToButton.click(); - tick(); - wizardTestFixture.detectChanges(); - - expect(wizardState.currentStepIndex).toBe(2); - expect(wizardSteps[0].selected).toBe(false); - expect(wizardSteps[1].selected).toBe(false); - expect(wizardSteps[2].selected).toBe(true); - - // click button - thirdStepGoToButton.click(); - tick(); - wizardTestFixture.detectChanges(); - - expect(wizardState.currentStepIndex).toBe(0); - expect(wizardSteps[0].selected).toBe(true); - expect(wizardSteps[1].selected).toBe(false); - expect(wizardSteps[2].selected).toBe(false); - })); - - it('should stay at current step correctly', fakeAsync(() => { - const firstStepGoToButton = wizardTestFixture.debugElement.query( - By.css('aw-wizard-step[stepTitle="Steptitle 1"] > button:nth-child(1)')).nativeElement; - - const wizardSteps = wizardState.wizardSteps; - - expect(wizardState.currentStepIndex).toBe(0); - expect(wizardSteps[0].selected).toBe(true); - expect(wizardSteps[1].selected).toBe(false); - expect(wizardSteps[2].selected).toBe(false); - - // click button - firstStepGoToButton.click(); - tick(); - wizardTestFixture.detectChanges(); - - expect(wizardState.currentStepIndex).toBe(0); - expect(wizardSteps[0].selected).toBe(true); - expect(wizardSteps[1].selected).toBe(false); - expect(wizardSteps[2].selected).toBe(false); - })); - - it('should finalize step correctly', fakeAsync(() => { - const firstStepGoToButton = wizardTestFixture.debugElement.query( - By.css('aw-wizard-step[stepTitle="Steptitle 1"] > button:nth-child(3)')).nativeElement; - const thirdStepGoToButton = wizardTestFixture.debugElement.query( - By.css('aw-wizard-step[stepTitle="Steptitle 3"] > button')).nativeElement; - - expect(wizardState.currentStepIndex).toBe(0); - expect(wizardTest.eventLog).toEqual([]); - - // click button - firstStepGoToButton.click(); - tick(); - wizardTestFixture.detectChanges(); - - expect(wizardState.currentStepIndex).toBe(2); - expect(wizardTest.eventLog).toEqual(['finalize 1']); - - // click button - thirdStepGoToButton.click(); - tick(); - wizardTestFixture.detectChanges(); - - expect(wizardState.currentStepIndex).toBe(0); - expect(wizardTest.eventLog).toEqual(['finalize 1', 'finalize 3']); - })); - it('should throw an error when using an invalid targetStep value', fakeAsync(() => { const invalidGoToAttribute = wizardTestFixture.debugElement .query(By.css('aw-wizard-step[stepTitle="Steptitle 2"]')) .queryAll(By.directive(GoToStepDirective))[1].injector.get(GoToStepDirective) as GoToStepDirective; expect(() => invalidGoToAttribute.destinationStep) - .toThrow(new Error(`Input 'targetStep' is neither a WizardStep, StepOffset, number or string`)); - })); - - it('should return correct destination step for correct targetStep values', fakeAsync(() => { - const firstGoToAttribute = wizardTestFixture.debugElement - .query(By.css('aw-wizard-navigation-bar')) - .queryAll(By.directive(GoToStepDirective))[0].injector.get(GoToStepDirective) as GoToStepDirective; - - const secondGoToAttribute = wizardTestFixture.debugElement - .query(By.css('aw-wizard-step[stepTitle="Steptitle 1"]')) - .queryAll(By.directive(GoToStepDirective))[1].injector.get(GoToStepDirective) as GoToStepDirective; - - const thirdGoToAttribute = wizardTestFixture.debugElement - .query(By.css('aw-wizard-step[stepTitle="Steptitle 2"]')) - .queryAll(By.directive(GoToStepDirective))[0].injector.get(GoToStepDirective) as GoToStepDirective; - - const fourthGoToAttribute = wizardTestFixture.debugElement - .query(By.css('aw-wizard-step[stepTitle="Steptitle 3"]')) - .queryAll(By.directive(GoToStepDirective))[0].injector.get(GoToStepDirective) as GoToStepDirective; - - expect(firstGoToAttribute.destinationStep).toBe(0); - expect(secondGoToAttribute.destinationStep).toBe(1); - expect(thirdGoToAttribute.destinationStep).toBe(2); - expect(fourthGoToAttribute.destinationStep).toBe(0); - })); - - it('should not leave current step if it the destination step can not be entered', fakeAsync(() => { - expect(wizardState.currentStepIndex).toBe(0); - - wizardTest.canExit = false; - wizardTestFixture.detectChanges(); - - const secondGoToAttribute = wizardTestFixture.debugElement - .query(By.css('aw-wizard-navigation-bar')) - .queryAll(By.directive(GoToStepDirective))[1].nativeElement; - - secondGoToAttribute.click(); - tick(); - wizardTestFixture.detectChanges(); - - expect(wizardState.currentStepIndex).toBe(0); + .toThrow(new Error(`Input 'targetStep' is neither a WizardStep, StepOffset, StepIndex or StepId`)); })); }); diff --git a/src/directives/go-to-step.directive.ts b/src/directives/go-to-step.directive.ts index 79d30f82..24e22195 100644 --- a/src/directives/go-to-step.directive.ts +++ b/src/directives/go-to-step.directive.ts @@ -4,10 +4,11 @@ import {Directive, EventEmitter, HostListener, Input, Optional, Output} from '@angular/core'; import {isStepOffset, StepOffset} from '../util/step-offset.interface'; -import {isNumber, isString} from 'util'; import {WizardStep} from '../util/wizard-step.interface'; import {WizardState} from '../navigation/wizard-state.model'; import {NavigationMode} from '../navigation/navigation-mode.interface'; +import {isStepId, StepId} from '../util/step-id.interface'; +import {isStepIndex, StepIndex} from '../util/step-index.interface'; /** * The `awGoToStep` directive can be used to navigate to a given step. @@ -18,7 +19,13 @@ import {NavigationMode} from '../navigation/navigation-mode.interface'; * With absolute step index: * * ```html - * + * + * ``` + * + * With unique step id: + * + * ```html + * * ``` * * With a wizard step object: @@ -27,7 +34,7 @@ import {NavigationMode} from '../navigation/navigation-mode.interface'; * * ``` * - * With an offset to the defining step + * With an offset to the defining step: * * ```html * @@ -75,9 +82,9 @@ export class GoToStepDirective { * a [[StepOffset]] between the current step and the `wizardStep`, in which this directive has been used, * or a step index as a number or string */ - // tslint:disable-next-line:no-input-rename + // tslint:disable-next-line:no-input-rename @Input('awGoToStep') - public targetStep: WizardStep | StepOffset | number | string; + public targetStep: WizardStep | StepOffset | StepIndex | StepId; /** * The navigation mode @@ -92,7 +99,8 @@ export class GoToStepDirective { * @param wizardState The wizard state * @param wizardStep The wizard step, which contains this [[GoToStepDirective]] */ - constructor(private wizardState: WizardState, @Optional() private wizardStep: WizardStep) { } + constructor(private wizardState: WizardState, @Optional() private wizardStep: WizardStep) { + } /** * Returns the destination step of this directive as an absolute step index inside the wizard @@ -103,16 +111,16 @@ export class GoToStepDirective { get destinationStep(): number { let destinationStep: number; - if (isNumber(this.targetStep)) { - destinationStep = this.targetStep as number; - } else if (isString(this.targetStep)) { - destinationStep = parseInt(this.targetStep as string, 10); + if (isStepIndex(this.targetStep)) { + destinationStep = this.targetStep.stepIndex; + } else if (isStepId(this.targetStep)) { + destinationStep = this.wizardState.getIndexOfStepWithId(this.targetStep.stepId); } else if (isStepOffset(this.targetStep) && this.wizardStep !== null) { destinationStep = this.wizardState.getIndexOfStep(this.wizardStep) + this.targetStep.stepOffset; } else if (this.targetStep instanceof WizardStep) { destinationStep = this.wizardState.getIndexOfStep(this.targetStep); } else { - throw new Error(`Input 'targetStep' is neither a WizardStep, StepOffset, number or string`); + throw new Error(`Input 'targetStep' is neither a WizardStep, StepOffset, StepIndex or StepId`); } return destinationStep; @@ -122,7 +130,8 @@ export class GoToStepDirective { * Listener method for `click` events on the component with this directive. * After this method is called the wizard will try to transition to the `destinationStep` */ - @HostListener('click', ['$event']) onClick(event: Event): void { + @HostListener('click', ['$event']) + onClick(event: Event): void { this.navigationMode.goToStep(this.destinationStep, this.preFinalize, this.postFinalize); } } diff --git a/src/navigation/wizard-state.model.ts b/src/navigation/wizard-state.model.ts index b55d2c66..41e0a850 100644 --- a/src/navigation/wizard-state.model.ts +++ b/src/navigation/wizard-state.model.ts @@ -175,7 +175,18 @@ export class WizardState { } /** - * Find the index of the given [[WizardStep]] `step`. + * Finds the index of the step with the given `stepId`. + * If no step with the given `stepId` exists, `-1` is returned + * + * @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 { + return this.wizardSteps.findIndex(step => step.stepId === stepId); + } + + /** + * Finds the index of the given [[WizardStep]] `step`. * If the given [[WizardStep]] is not contained inside this wizard, `-1` is returned * * @param step The given [[WizardStep]] diff --git a/src/util/index.ts b/src/util/index.ts index 8224a721..14d682b4 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,4 +1,6 @@ export {MovingDirection} from './moving-direction.enum'; +export * from './step-id.interface'; +export * from './step-index.interface'; export * from './step-offset.interface'; export {WizardCompletionStep} from './wizard-completion-step.interface'; export {WizardStep} from './wizard-step.interface'; diff --git a/src/util/step-id.interface.spec.ts b/src/util/step-id.interface.spec.ts new file mode 100644 index 00000000..0742aa42 --- /dev/null +++ b/src/util/step-id.interface.spec.ts @@ -0,0 +1,180 @@ +/** + * Created by marc on 09.01.17. + */ +import {Component} from '@angular/core'; +import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {ArchwizardModule} from '../archwizard.module'; +import {WizardState} from '../navigation/wizard-state.model'; +import {NavigationMode} from '../navigation/navigation-mode.interface'; +import {GoToStepDirective} from '../directives/go-to-step.directive'; + +@Component({ + selector: 'aw-test-wizard', + template: ` + + + Step 1 + + + + Step 2 + + + + Step 3 + + + + ` +}) +class WizardTestComponent { + public canExit = true; + + public eventLog: Array = []; + + finalizeStep(stepIndex: number): void { + this.eventLog.push(`finalize ${stepIndex}`); + } +} + +describe('StepId', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + let wizardState: WizardState; + let navigationMode: NavigationMode; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [ArchwizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTestFixture.detectChanges(); + + wizardTest = wizardTestFixture.componentInstance; + wizardState = wizardTestFixture.debugElement.query(By.css('aw-wizard')).injector.get(WizardState); + navigationMode = wizardState.navigationMode; + }); + + it('should create an instance', () => { + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-navigation-bar')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(3); + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-step[stepTitle="Steptitle 1"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(1); + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-step[stepTitle="Steptitle 2"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(1); + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-step[stepTitle="Steptitle 3"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(1); + + expect(wizardTestFixture.debugElement.queryAll(By.directive(GoToStepDirective)).length).toBe(6); + }); + + it('should move to an earlier step correctly', fakeAsync(() => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('aw-wizard-step[stepTitle="Steptitle 3"] > button')).nativeElement; + + const wizardSteps = wizardState.wizardSteps; + + navigationMode.goToStep(2); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(2); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(true); + + // click button + firstStepGoToButton.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(0); + expect(wizardSteps[0].selected).toBe(true); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(false); + })); + + it('should move to a later step correctly', fakeAsync(() => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('aw-wizard-step[stepTitle="Steptitle 1"] > button')).nativeElement; + + const wizardSteps = wizardState.wizardSteps; + + expect(wizardState.currentStepIndex).toBe(0); + expect(wizardSteps[0].selected).toBe(true); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(false); + + // click button + firstStepGoToButton.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(2); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(true); + })); + + it('should stay at current step correctly', fakeAsync(() => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('aw-wizard-step[stepTitle="Steptitle 2"] > button')).nativeElement; + + const wizardSteps = wizardState.wizardSteps; + + navigationMode.goToStep(1); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(1); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(true); + expect(wizardSteps[2].selected).toBe(false); + + // click button + firstStepGoToButton.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(1); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(true); + expect(wizardSteps[2].selected).toBe(false); + })); + + it('should return correct destination step for correct targetStep values', fakeAsync(() => { + const firstGoToAttribute = wizardTestFixture.debugElement + .query(By.css('aw-wizard-step[stepTitle="Steptitle 1"]')) + .query(By.directive(GoToStepDirective)).injector.get(GoToStepDirective) as GoToStepDirective; + + const secondGoToAttribute = wizardTestFixture.debugElement + .query(By.css('aw-wizard-step[stepTitle="Steptitle 3"]')) + .query(By.directive(GoToStepDirective)).injector.get(GoToStepDirective) as GoToStepDirective; + + expect(firstGoToAttribute.destinationStep).toBe(2); + expect(secondGoToAttribute.destinationStep).toBe(0); + })); + + it('should not leave current step if the destination step can not be entered', fakeAsync(() => { + expect(wizardState.currentStepIndex).toBe(0); + + wizardTest.canExit = false; + wizardTestFixture.detectChanges(); + + const secondGoToAttribute = wizardTestFixture.debugElement + .query(By.css('aw-wizard-navigation-bar')) + .query(By.directive(GoToStepDirective)).nativeElement; + + secondGoToAttribute.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(0); + })); +}); diff --git a/src/util/step-id.interface.ts b/src/util/step-id.interface.ts new file mode 100644 index 00000000..b08ad993 --- /dev/null +++ b/src/util/step-id.interface.ts @@ -0,0 +1,23 @@ +import {WizardStep} from './wizard-step.interface'; + +/** + * An unique identifier of a wizard step + * + * @author Marc Arndt + */ +export interface StepId { + /** + * The id of the destination step + */ + stepId: string +} + +/** + * Checks whether the given `value` implements the interface [[StepId]]. + * + * @param value The value to be checked + * @returns True if the given value implements [[StepId]] and false otherwise + */ +export function isStepId(value: any): value is StepId { + return value.hasOwnProperty('stepId') && !(value instanceof WizardStep); +} diff --git a/src/util/step-index.interface.spec.ts b/src/util/step-index.interface.spec.ts new file mode 100644 index 00000000..bd936592 --- /dev/null +++ b/src/util/step-index.interface.spec.ts @@ -0,0 +1,180 @@ +/** + * Created by marc on 09.01.17. + */ +import {Component} from '@angular/core'; +import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {ArchwizardModule} from '../archwizard.module'; +import {WizardState} from '../navigation/wizard-state.model'; +import {NavigationMode} from '../navigation/navigation-mode.interface'; +import {GoToStepDirective} from '../directives/go-to-step.directive'; + +@Component({ + selector: 'aw-test-wizard', + template: ` + + + Step 1 + + + + Step 2 + + + + Step 3 + + + + ` +}) +class WizardTestComponent { + public canExit = true; + + public eventLog: Array = []; + + finalizeStep(stepIndex: number): void { + this.eventLog.push(`finalize ${stepIndex}`); + } +} + +describe('StepIndex', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + let wizardState: WizardState; + let navigationMode: NavigationMode; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [ArchwizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTestFixture.detectChanges(); + + wizardTest = wizardTestFixture.componentInstance; + wizardState = wizardTestFixture.debugElement.query(By.css('aw-wizard')).injector.get(WizardState); + navigationMode = wizardState.navigationMode; + }); + + it('should create an instance', () => { + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-navigation-bar')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(3); + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-step[stepTitle="Steptitle 1"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(1); + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-step[stepTitle="Steptitle 2"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(1); + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-step[stepTitle="Steptitle 3"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(1); + + expect(wizardTestFixture.debugElement.queryAll(By.directive(GoToStepDirective)).length).toBe(6); + }); + + it('should move to an earlier step correctly', fakeAsync(() => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('aw-wizard-step[stepTitle="Steptitle 3"] > button')).nativeElement; + + const wizardSteps = wizardState.wizardSteps; + + navigationMode.goToStep(2); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(2); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(true); + + // click button + firstStepGoToButton.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(0); + expect(wizardSteps[0].selected).toBe(true); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(false); + })); + + it('should move to a later step correctly', fakeAsync(() => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('aw-wizard-step[stepTitle="Steptitle 1"] > button')).nativeElement; + + const wizardSteps = wizardState.wizardSteps; + + expect(wizardState.currentStepIndex).toBe(0); + expect(wizardSteps[0].selected).toBe(true); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(false); + + // click button + firstStepGoToButton.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(2); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(true); + })); + + it('should stay at current step correctly', fakeAsync(() => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('aw-wizard-step[stepTitle="Steptitle 2"] > button')).nativeElement; + + const wizardSteps = wizardState.wizardSteps; + + navigationMode.goToStep(1); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(1); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(true); + expect(wizardSteps[2].selected).toBe(false); + + // click button + firstStepGoToButton.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(1); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(true); + expect(wizardSteps[2].selected).toBe(false); + })); + + it('should return correct destination step for correct targetStep values', fakeAsync(() => { + const firstGoToAttribute = wizardTestFixture.debugElement + .query(By.css('aw-wizard-step[stepTitle="Steptitle 1"]')) + .query(By.directive(GoToStepDirective)).injector.get(GoToStepDirective) as GoToStepDirective; + + const secondGoToAttribute = wizardTestFixture.debugElement + .query(By.css('aw-wizard-step[stepTitle="Steptitle 3"]')) + .query(By.directive(GoToStepDirective)).injector.get(GoToStepDirective) as GoToStepDirective; + + expect(firstGoToAttribute.destinationStep).toBe(2); + expect(secondGoToAttribute.destinationStep).toBe(0); + })); + + it('should not leave current step if the destination step can not be entered', fakeAsync(() => { + expect(wizardState.currentStepIndex).toBe(0); + + wizardTest.canExit = false; + wizardTestFixture.detectChanges(); + + const secondGoToAttribute = wizardTestFixture.debugElement + .query(By.css('aw-wizard-navigation-bar')) + .query(By.directive(GoToStepDirective)).nativeElement; + + secondGoToAttribute.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(0); + })); +}); diff --git a/src/util/step-index.interface.ts b/src/util/step-index.interface.ts new file mode 100644 index 00000000..50311c37 --- /dev/null +++ b/src/util/step-index.interface.ts @@ -0,0 +1,23 @@ +/** + * An index of a wizard step. + * This index is the index of the step inside the wizard. + * The index is always zero based, i.e. the step with index 0 is the first step of the wizard + * + * @author Marc Arndt + */ +export interface StepIndex { + /** + * The index of the destination step + */ + stepIndex: number +} + +/** + * Checks whether the given `value` implements the interface [[StepIndex]]. + * + * @param value The value to be checked + * @returns True if the given value implements [[StepIndex]] and false otherwise + */ +export function isStepIndex(value: any): value is StepIndex { + return value.hasOwnProperty('stepIndex'); +} diff --git a/src/util/step-offset.interface.spec.ts b/src/util/step-offset.interface.spec.ts new file mode 100644 index 00000000..f8ef2e68 --- /dev/null +++ b/src/util/step-offset.interface.spec.ts @@ -0,0 +1,180 @@ +/** + * Created by marc on 09.01.17. + */ +import {Component} from '@angular/core'; +import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {ArchwizardModule} from '../archwizard.module'; +import {WizardState} from '../navigation/wizard-state.model'; +import {NavigationMode} from '../navigation/navigation-mode.interface'; +import {GoToStepDirective} from '../directives/go-to-step.directive'; + +@Component({ + selector: 'aw-test-wizard', + template: ` + + + Step 1 + + + + Step 2 + + + + Step 3 + + + + ` +}) +class WizardTestComponent { + public canExit = true; + + public eventLog: Array = []; + + finalizeStep(stepIndex: number): void { + this.eventLog.push(`finalize ${stepIndex}`); + } +} + +describe('StepOffset', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + let wizardState: WizardState; + let navigationMode: NavigationMode; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [ArchwizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTestFixture.detectChanges(); + + wizardTest = wizardTestFixture.componentInstance; + wizardState = wizardTestFixture.debugElement.query(By.css('aw-wizard')).injector.get(WizardState); + navigationMode = wizardState.navigationMode; + }); + + it('should create an instance', () => { + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-navigation-bar')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(3); + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-step[stepTitle="Steptitle 1"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(1); + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-step[stepTitle="Steptitle 2"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(1); + expect(wizardTestFixture.debugElement.query(By.css('aw-wizard-step[stepTitle="Steptitle 3"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(1); + + expect(wizardTestFixture.debugElement.queryAll(By.directive(GoToStepDirective)).length).toBe(6); + }); + + it('should move to an earlier step correctly', fakeAsync(() => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('aw-wizard-step[stepTitle="Steptitle 3"] > button')).nativeElement; + + const wizardSteps = wizardState.wizardSteps; + + navigationMode.goToStep(2); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(2); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(true); + + // click button + firstStepGoToButton.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(0); + expect(wizardSteps[0].selected).toBe(true); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(false); + })); + + it('should move to a later step correctly', fakeAsync(() => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('aw-wizard-step[stepTitle="Steptitle 1"] > button')).nativeElement; + + const wizardSteps = wizardState.wizardSteps; + + expect(wizardState.currentStepIndex).toBe(0); + expect(wizardSteps[0].selected).toBe(true); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(false); + + // click button + firstStepGoToButton.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(2); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(false); + expect(wizardSteps[2].selected).toBe(true); + })); + + it('should stay at current step correctly', fakeAsync(() => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('aw-wizard-step[stepTitle="Steptitle 2"] > button')).nativeElement; + + const wizardSteps = wizardState.wizardSteps; + + navigationMode.goToStep(1); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(1); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(true); + expect(wizardSteps[2].selected).toBe(false); + + // click button + firstStepGoToButton.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(1); + expect(wizardSteps[0].selected).toBe(false); + expect(wizardSteps[1].selected).toBe(true); + expect(wizardSteps[2].selected).toBe(false); + })); + + it('should return correct destination step for correct targetStep values', fakeAsync(() => { + const firstGoToAttribute = wizardTestFixture.debugElement + .query(By.css('aw-wizard-step[stepTitle="Steptitle 1"]')) + .query(By.directive(GoToStepDirective)).injector.get(GoToStepDirective) as GoToStepDirective; + + const secondGoToAttribute = wizardTestFixture.debugElement + .query(By.css('aw-wizard-step[stepTitle="Steptitle 3"]')) + .query(By.directive(GoToStepDirective)).injector.get(GoToStepDirective) as GoToStepDirective; + + expect(firstGoToAttribute.destinationStep).toBe(2); + expect(secondGoToAttribute.destinationStep).toBe(0); + })); + + it('should not leave current step if the destination step can not be entered', fakeAsync(() => { + expect(wizardState.currentStepIndex).toBe(0); + + wizardTest.canExit = false; + wizardTestFixture.detectChanges(); + + const secondGoToAttribute = wizardTestFixture.debugElement + .query(By.css('aw-wizard-navigation-bar')) + .query(By.directive(GoToStepDirective)).nativeElement; + + secondGoToAttribute.click(); + tick(); + wizardTestFixture.detectChanges(); + + expect(wizardState.currentStepIndex).toBe(0); + })); +}); diff --git a/src/util/wizard-step.interface.ts b/src/util/wizard-step.interface.ts index 751ed83e..4d012ccd 100644 --- a/src/util/wizard-step.interface.ts +++ b/src/util/wizard-step.interface.ts @@ -17,6 +17,12 @@ export abstract class WizardStep { @ContentChild(WizardStepTitleDirective) public stepTitleTemplate: WizardStepTitleDirective; + /** + * A step id, unique to the step + */ + @Input() + public stepId: string; + /** * A step title property, which contains the visible header title of the step. * This title is only shown inside the navigation bar, if `stepTitleTemplate` is not defined or null.