Skip to content

Commit

Permalink
Make defaultStepIndex, disableNavigationBar and the navigation mode i…
Browse files Browse the repository at this point in the history
…nputs updatable and allow the insertion/removal of wizard steps (#95)

- make defaultStepIndex updatable
- make disableNavigationBar updatable
- make the navigation mode changable
- enable the removal of arbitrary wizard steps (except the current step) from the DOM at runtime
- add tests
- add a short explanation about the removal and insertion of wizard steps after the wizard initialization
  • Loading branch information
madoar committed Jan 28, 2018
1 parent fff3849 commit 760dd83
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 55 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,29 @@ For additional information about how to write your own navigation bar please tak
https://github.com/madoar/ng2-archwizard/blob/master/src/components/wizard-navigation-bar.component.horizontal.less and
https://github.com/madoar/ng2-archwizard/blob/master/src/components/wizard-navigation-bar.component.vertical.less.

### Working with dynamically inserted and removed steps
In some cases it may be required to remove or insert one or multiple steps after the wizard initialization,
for example after a user does some interaction with the wizard, it may be required to add or remove a later step.
In such situations the wizard supports the removal and insertion of steps in the DOM.

If you require to the dynamic removal or insertion of steps, please be aware that the angular component containing the wizard
needs to trigger a `detectChanges()` call inside the `afterViewInit` lifecycle phase.
This call can be triggered by adding the following lines to the component class:
```typescript
constructor(private _changeDetectionRef: ChangeDetectorRef) {}

ngAfterViewInit(): void {
// Force another change detection in order to fix an occuring ExpressionChangedAfterItHasBeenCheckedError
this._changeDetectionRef.detectChanges();
}
```

If an earlier step, compared to the current step, has been removed or inserted,
the wizard will adjust the index of the current step to make the changed state valid again.

Please be also sure to not remove the step, the wizard is currently displaying, because otherwise the wizard will be inside an
invalid state, which may lead to strange and unexpected behavior.

## Example
You can find an basic example project using `ng2-archwizard` [here](https://madoar.github.io/ng2-archwizard-demo).
The sources for the example can be found in the [ng2-archwizard-demo](https://github.com/madoar/ng2-archwizard-demo) repository.
Expand Down
173 changes: 147 additions & 26 deletions src/components/wizard.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component, ViewChild} from '@angular/core';
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {AfterViewInit, ChangeDetectorRef, Component, ViewChild} from '@angular/core';
import {WizardComponent} from './wizard.component';
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 {StrictNavigationMode} from '../navigation/strict-navigation-mode';
import {FreeNavigationMode} from '../navigation/free-navigation-mode';

@Component({
selector: 'aw-test-wizard',
template: `
<aw-wizard>
<aw-wizard-step stepTitle='Steptitle 1'>Step 1</aw-wizard-step>
<aw-wizard [navigationMode]="navigationMode" [disableNavigationBar]="disableNavigationBar" [defaultStepIndex]="defaultStepIndex">
<aw-wizard-step stepTitle='Steptitle 1' *ngIf="showStep1">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-step stepTitle='Steptitle 3' *ngIf="showStep3">Step 3</aw-wizard-step>
</aw-wizard>
`
})
class WizardTestComponent {
class WizardTestComponent implements AfterViewInit {
public navigationMode = 'strict';

public disableNavigationBar = false;

public defaultStepIndex = 0;

public showStep1 = true;
public showStep3 = true;

@ViewChild(WizardComponent)
public wizard: WizardComponent;

constructor(private _changeDetectionRef: ChangeDetectorRef) {
}

ngAfterViewInit(): void {
// Force another change detection in order to fix an occuring ExpressionChangedAfterItHasBeenCheckedError
this._changeDetectionRef.detectChanges();
}
}

describe('WizardComponent', () => {
Expand Down Expand Up @@ -67,10 +86,12 @@ describe('WizardComponent', () => {
expect(wizardTestFixture.debugElement.query(By.css('aw-wizard > :first-child')).name).toBe('aw-wizard-navigation-bar');
expect(wizardTestFixture.debugElement.query(By.css('aw-wizard > :last-child')).name).toBe('div');

expect(navBar.classes).toEqual({ 'horizontal': true, 'vertical': false, 'small': true,
'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false });
expect(wizard.classes).toEqual({ 'horizontal': true, 'vertical': false });
expect(wizardStepsDiv.classes).toEqual({ 'wizard-steps': true, 'horizontal': true, 'vertical': false });
expect(navBar.classes).toEqual({
'horizontal': true, 'vertical': false, 'small': true,
'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false
});
expect(wizard.classes).toEqual({'horizontal': true, 'vertical': false});
expect(wizardStepsDiv.classes).toEqual({'wizard-steps': true, 'horizontal': true, 'vertical': false});
});

it('should contain navigation bar at the correct position in top navBarLocation mode', () => {
Expand All @@ -87,10 +108,12 @@ describe('WizardComponent', () => {
expect(wizardTestFixture.debugElement.query(By.css('aw-wizard > :first-child')).name).toBe('aw-wizard-navigation-bar');
expect(wizardTestFixture.debugElement.query(By.css('aw-wizard > :last-child')).name).toBe('div');

expect(navBar.classes).toEqual({ 'horizontal': true, 'vertical': false, 'small': true,
'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false });
expect(wizard.classes).toEqual({ 'horizontal': true, 'vertical': false });
expect(wizardStepsDiv.classes).toEqual({ 'wizard-steps': true, 'horizontal': true, 'vertical': false });
expect(navBar.classes).toEqual({
'horizontal': true, 'vertical': false, 'small': true,
'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false
});
expect(wizard.classes).toEqual({'horizontal': true, 'vertical': false});
expect(wizardStepsDiv.classes).toEqual({'wizard-steps': true, 'horizontal': true, 'vertical': false});
});

it('should contain navigation bar at the correct position in left navBarLocation mode', () => {
Expand All @@ -107,10 +130,12 @@ describe('WizardComponent', () => {
expect(wizardTestFixture.debugElement.query(By.css('aw-wizard > :first-child')).name).toBe('aw-wizard-navigation-bar');
expect(wizardTestFixture.debugElement.query(By.css('aw-wizard > :last-child')).name).toBe('div');

expect(navBar.classes).toEqual({ 'horizontal': false, 'vertical': true, 'small': true,
'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false });
expect(wizard.classes).toEqual({ 'horizontal': false, 'vertical': true });
expect(wizardStepsDiv.classes).toEqual({ 'wizard-steps': true, 'horizontal': false, 'vertical': true });
expect(navBar.classes).toEqual({
'horizontal': false, 'vertical': true, 'small': true,
'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false
});
expect(wizard.classes).toEqual({'horizontal': false, 'vertical': true});
expect(wizardStepsDiv.classes).toEqual({'wizard-steps': true, 'horizontal': false, 'vertical': true});
});

it('should contain navigation bar at the correct position in bottom navBarLocation mode', () => {
Expand All @@ -127,10 +152,12 @@ describe('WizardComponent', () => {
expect(wizardTestFixture.debugElement.query(By.css('aw-wizard > :first-child')).name).toBe('div');
expect(wizardTestFixture.debugElement.query(By.css('aw-wizard > :last-child')).name).toBe('aw-wizard-navigation-bar');

expect(navBar.classes).toEqual({ 'horizontal': true, 'vertical': false, 'small': true,
'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false });
expect(wizard.classes).toEqual({ 'horizontal': true, 'vertical': false });
expect(wizardStepsDiv.classes).toEqual({ 'wizard-steps': true, 'horizontal': true, 'vertical': false });
expect(navBar.classes).toEqual({
'horizontal': true, 'vertical': false, 'small': true,
'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false
});
expect(wizard.classes).toEqual({'horizontal': true, 'vertical': false});
expect(wizardStepsDiv.classes).toEqual({'wizard-steps': true, 'horizontal': true, 'vertical': false});
});

it('should contain navigation bar at the correct position in right navBarLocation mode', () => {
Expand All @@ -147,9 +174,103 @@ describe('WizardComponent', () => {
expect(wizardTestFixture.debugElement.query(By.css('aw-wizard > :first-child')).name).toBe('div');
expect(wizardTestFixture.debugElement.query(By.css('aw-wizard > :last-child')).name).toBe('aw-wizard-navigation-bar');

expect(navBar.classes).toEqual({ 'horizontal': false, 'vertical': true, 'small': true,
'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false });
expect(wizard.classes).toEqual({ 'horizontal': false, 'vertical': true });
expect(wizardStepsDiv.classes).toEqual({ 'wizard-steps': true, 'horizontal': false, 'vertical': true });
expect(navBar.classes).toEqual({
'horizontal': false, 'vertical': true, 'small': true,
'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false
});
expect(wizard.classes).toEqual({'horizontal': false, 'vertical': true});
expect(wizardStepsDiv.classes).toEqual({'wizard-steps': true, 'horizontal': false, 'vertical': true});
});

it('should change the navigation mode correctly during runtime', () => {
expect(wizardTest.wizard.navigation instanceof StrictNavigationMode).toBe(true);

wizardTest.navigationMode = 'free';
wizardTestFixture.detectChanges();

expect(wizardTest.wizard.navigation instanceof FreeNavigationMode).toBe(true);
});

it('should change disableNavigationBar correctly during runtime', () => {
expect(wizardState.disableNavigationBar).toBe(false);

wizardTest.disableNavigationBar = true;
wizardTestFixture.detectChanges();

expect(wizardState.disableNavigationBar).toBe(true);
});

it('should change defaultStepIndex correctly during runtime', () => {
expect(wizardState.defaultStepIndex).toBe(0);

wizardTest.defaultStepIndex = 1;
wizardTestFixture.detectChanges();

expect(wizardState.defaultStepIndex).toBe(1);
});

it('should react on a previous step removal and insertion correctly', fakeAsync(() => {
navigationMode.goToStep(1);
tick();
wizardTestFixture.detectChanges();

expect(wizardState.currentStepIndex).toBe(1);
expect(wizardState.wizardSteps.length).toBe(3);

wizardTest.showStep1 = false;
wizardTestFixture.detectChanges();

expect(wizardState.currentStepIndex).toBe(0);
expect(wizardState.wizardSteps.length).toBe(2);

wizardTest.showStep1 = true;
wizardTestFixture.detectChanges();

expect(wizardState.currentStepIndex).toBe(1);
expect(wizardState.wizardSteps.length).toBe(3);
}));

it('should react on a later step removal and insertion correctly', fakeAsync(() => {
navigationMode.goToStep(1);
tick();
wizardTestFixture.detectChanges();

expect(wizardState.currentStepIndex).toBe(1);
expect(wizardState.wizardSteps.length).toBe(3);

wizardTest.showStep3 = false;
wizardTestFixture.detectChanges();

expect(wizardState.currentStepIndex).toBe(1);
expect(wizardState.wizardSteps.length).toBe(2);

wizardTest.showStep3 = true;
wizardTestFixture.detectChanges();

expect(wizardState.currentStepIndex).toBe(1);
expect(wizardState.wizardSteps.length).toBe(3);
}));

it('should react on a combined removal and insertion of previous and later steps correctly', fakeAsync(() => {
navigationMode.goToStep(1);
tick();
wizardTestFixture.detectChanges();

expect(wizardState.currentStepIndex).toBe(1);
expect(wizardState.wizardSteps.length).toBe(3);

wizardTest.showStep1 = false;
wizardTest.showStep3 = false;
wizardTestFixture.detectChanges();

expect(wizardState.currentStepIndex).toBe(0);
expect(wizardState.wizardSteps.length).toBe(1);

wizardTest.showStep1 = true;
wizardTest.showStep3 = true;
wizardTestFixture.detectChanges();

expect(wizardState.currentStepIndex).toBe(1);
expect(wizardState.wizardSteps.length).toBe(3);
}));
});
56 changes: 53 additions & 3 deletions src/components/wizard.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import {AfterContentInit, Component, ContentChildren, HostBinding, Input, QueryList, ViewEncapsulation} from '@angular/core';
import {
AfterContentInit,
Component,
ContentChildren,
HostBinding,
Input,
OnChanges,
QueryList,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import {WizardStep} from '../util/wizard-step.interface';
import {WizardState} from '../navigation/wizard-state.model';
import {NavigationMode} from '../navigation/navigation-mode.interface';
Expand Down Expand Up @@ -45,7 +55,7 @@ import {NavigationMode} from '../navigation/navigation-mode.interface';
encapsulation: ViewEncapsulation.None,
providers: [WizardState]
})
export class WizardComponent implements AfterContentInit {
export class WizardComponent implements OnChanges, AfterContentInit {
/**
* A QueryList containing all [[WizardStep]]s inside this wizard
*/
Expand Down Expand Up @@ -123,15 +133,55 @@ export class WizardComponent implements AfterContentInit {

/**
* Constructor
*
* @param model The model for this wizard component
*/
constructor(public model: WizardState) {
}

/**
* Updates the model after certain input values have changed
*
* @param changes The detected changes
*/
ngOnChanges(changes: SimpleChanges) {
for (const propName of Object.keys(changes)) {
let change = changes[propName];

if (!change.firstChange) {
switch (propName) {
case 'defaultStepIndex':
this.model.defaultStepIndex = parseInt(change.currentValue, 10);
break;
case 'disableNavigationBar':
this.model.disableNavigationBar = change.currentValue;
break;
case 'navigationMode':
this.model.updateNavigationMode(change.currentValue);
break;
/* istanbul ignore next */
default:
}
}
}
}

/**
* Initialization work
*/
ngAfterContentInit(): void {
this.model.initialize(this.wizardSteps, this.navigationMode, this.defaultStepIndex, this.disableNavigationBar);
// 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());
});

// initialize the model
this.model.disableNavigationBar = this.disableNavigationBar;
this.model.defaultStepIndex = this.defaultStepIndex;
this.model.updateWizardSteps(this.wizardSteps.toArray());
this.model.updateNavigationMode(this.navigationMode);

// finally reset the whole wizard state
this.navigation.reset();
}
}

0 comments on commit 760dd83

Please sign in to comment.