diff --git a/.travis.yml b/.travis.yml index 4c815642c0..c15b3920d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: node_js node_js: - "6.9" before_script: - - npm install -g --silent @angular/cli@1.1.3 + - npm install -g --silent @angular/cli@1.2.6 - npm install -g --silent yarn script: - cd server && yarn install && yarn run gulp build-all && tsc && cd ../AzureFunctions.AngularClient && yarn install && ng build -prod \ No newline at end of file diff --git a/AzureFunctions.AngularClient/.vscode/settings.json b/AzureFunctions.AngularClient/.vscode/settings.json index 75b21dc464..ac6f4dacc7 100644 --- a/AzureFunctions.AngularClient/.vscode/settings.json +++ b/AzureFunctions.AngularClient/.vscode/settings.json @@ -1,2 +1,6 @@ // Place your settings in this file to overwrite default and user settings. -{} \ No newline at end of file +{ + "[typescript]": { + "editor.formatOnSave": true + } +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/package.json b/AzureFunctions.AngularClient/package.json index 5483be0c9d..e4c192a17a 100644 --- a/AzureFunctions.AngularClient/package.json +++ b/AzureFunctions.AngularClient/package.json @@ -1,67 +1,70 @@ { - "name": "azure-functions-client", - "version": "0.0.0", - "license": "MIT", - "angular-cli": {}, - "scripts": { - "ng": "ng", - "watch": "ng b -w", - "start": "ng serve", - "build": "ng build", - "test": "ng test", - "lint": "ng lint", - "e2e": "ng e2e", - "bundle-report": "webpack-bundle-analyzer ../azurefunctions/ng-full/stats.json" - }, - "private": true, - "dependencies": { - "@angular/common": "^4.0.0", - "@angular/compiler": "^4.0.0", - "@angular/core": "^4.0.0", - "@angular/forms": "^4.0.0", - "@angular/http": "^4.0.0", - "@angular/platform-browser": "^4.0.0", - "@angular/platform-browser-dynamic": "^4.0.0", - "@angular/router": "^4.0.0", - "@ngx-translate/core": "^6.0.1", - "angular2-uuid": "^1.1.1", - "azure-mobile-apps-client": "^2.0.1", - "bootstrap": "^3.3.7", - "core-js": "^2.4.1", - "font-awesome": "^4.7.0", - "jsonschema": "^1.1.1", - "marked": "^0.3.9", - "moment": "^2.17.0", - "monaco-editor": "^0.10.0", - "ng-sidebar": "^6.0.1", - "ng2-cookies": "^1.0.3", - "ng2-file-upload": "~1.2.1", - "ng2-popover": "^0.0.14", - "node-sass": "^4.7.2", - "rxjs": "^5.1.0", - "swagger-editor": "git+/~https://github.com/azure/swagger-editor.git#ff974a50cc7c756f0a96a66f1e92f1286324c549", - "ts-helpers": "^1.1.1", - "zone.js": "^0.8.4" - }, - "devDependencies": { - "@angular/cli": "^1.1.3", - "@angular/compiler-cli": "^4.0.0", - "@types/jasmine": "2.5.38", - "@types/jsonschema": "^1.1.1", - "@types/node": "~6.0.60", - "codelyzer": "~2.0.0", - "jasmine-core": "~2.5.2", - "jasmine-spec-reporter": "~3.2.0", - "karma": "~1.4.1", - "karma-chrome-launcher": "~2.0.0", - "karma-cli": "~1.0.1", - "karma-coverage-istanbul-reporter": "^0.2.0", - "karma-jasmine": "~1.1.0", - "karma-jasmine-html-reporter": "^0.2.2", - "protractor": "~5.1.0", - "ts-node": "~2.0.0", - "tslint": "~4.5.0", - "typescript": "^2.4.1", - "webpack-bundle-analyzer": "^2.9.0" - } + "name": "azure-functions-client", + "version": "0.0.0", + "license": "MIT", + "angular-cli": {}, + "scripts": { + "ng": "ng", + "watch": "ng b -w", + "start": "ng serve", + "build": "ng build", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e", + "bundle-report": "webpack-bundle-analyzer ../azurefunctions/ng-full/stats.json" + }, + "private": true, + "dependencies": { + "@angular/common": "^4.0.0", + "@angular/compiler": "^4.0.0", + "@angular/core": "^4.0.0", + "@angular/forms": "^4.0.0", + "@angular/http": "^4.0.0", + "@angular/platform-browser": "^4.0.0", + "@angular/platform-browser-dynamic": "^4.0.0", + "@angular/router": "^4.0.0", + "@ngx-translate/core": "^6.0.1", + "angular2-uuid": "^1.1.1", + "azure-mobile-apps-client": "^2.0.1", + "bootstrap": "^3.3.7", + "core-js": "^2.4.1", + "font-awesome": "^4.7.0", + "jsonschema": "^1.1.1", + "marked": "^0.3.9", + "moment": "^2.17.0", + "monaco-editor": "^0.10.0", + "ng-sidebar": "^6.0.1", + "ng2-cookies": "^1.0.3", + "ng2-file-upload": "~1.2.1", + "ng2-popover": "^0.0.14", + "node-sass": "^4.7.2", + "rxjs": "^5.1.0", + "swagger-editor": "git+/~https://github.com/azure/swagger-editor.git#ff974a50cc7c756f0a96a66f1e92f1286324c549", + "ts-helpers": "^1.1.1", + "zone.js": "^0.8.4", + "ng-sidebar": "^6.0.1", + "lodash": "4.17.4" + }, + "devDependencies": { + "@angular/cli": "^1.1.3", + "@angular/compiler-cli": "^4.0.0", + "@types/jasmine": "2.5.38", + "@types/jsonschema": "^1.1.1", + "@types/node": "~6.0.60", + "@types/lodash": "^4.14.80", + "codelyzer": "~2.0.0", + "jasmine-core": "~2.5.2", + "jasmine-spec-reporter": "~3.2.0", + "karma": "~1.4.1", + "karma-chrome-launcher": "~2.0.0", + "karma-cli": "~1.0.1", + "karma-coverage-istanbul-reporter": "^0.2.0", + "karma-jasmine": "~1.1.0", + "karma-jasmine-html-reporter": "^0.2.2", + "protractor": "~5.1.0", + "ts-node": "~2.0.0", + "tslint": "~4.5.0", + "typescript": "^2.4.1", + "webpack-bundle-analyzer": "^2.9.0" + } } diff --git a/AzureFunctions.AngularClient/src/app/app.module.ts b/AzureFunctions.AngularClient/src/app/app.module.ts index b26da7e893..c483257752 100644 --- a/AzureFunctions.AngularClient/src/app/app.module.ts +++ b/AzureFunctions.AngularClient/src/app/app.module.ts @@ -17,6 +17,7 @@ import 'rxjs/add/operator/distinctUntilChanged'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/first'; +import 'rxjs/add/observable/forkJoin'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/merge'; import 'rxjs/add/operator/mergeMap'; diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.css b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.css new file mode 100755 index 0000000000..7fc4514270 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.css @@ -0,0 +1,4 @@ +:host { + height: auto; + width: 100%; +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.html b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.html new file mode 100755 index 0000000000..6dbc743063 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.html @@ -0,0 +1 @@ + diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.spec.ts new file mode 100755 index 0000000000..796bfb69b9 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.spec.ts @@ -0,0 +1,139 @@ +/** + * Created by marc on 20.05.17. + */ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {WizardCompletionStepComponent} from './wizard-completion-step.component'; +import {ViewChild, Component} from '@angular/core'; +import {WizardComponent} from './wizard.component'; +import {MovingDirection} from '../util/moving-direction.enum'; +import {By} from '@angular/platform-browser'; +import {WizardModule} from '../wizard.module'; + +@Component({ + selector: 'test-wizard', + template: ` + + Step 1 + Step 2 + Step 3 + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; + + public isValid: any = true; + + public eventLog: Array = new Array(); + + enterInto(direction: MovingDirection, destination: number): void { + this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); + } + + exitFrom(direction: MovingDirection, source: number): void { + this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); + } +} + +describe('WizardCompletionStepComponent', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTestFixture.debugElement.queryAll(By.css('wizard-step')).length).toBe(2); + expect(wizardTestFixture.debugElement.queryAll(By.css('wizard-completion-step')).length).toBe(1); + }); + + it('should have correct step title', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTest.wizard.getStepAtIndex(0).title).toBe('Steptitle 1'); + expect(wizardTest.wizard.getStepAtIndex(1).title).toBe('Steptitle 2'); + expect(wizardTest.wizard.getStepAtIndex(2).title).toBe('Completion steptitle 3'); + }); + + it('should enter first step after initialisation', () => { + expect(wizardTest.eventLog).toEqual(['enter Forwards 1']); + }); + + it('should enter completion step after first step', () => { + expect(wizardTest.wizard.currentStepIndex).toBe(0); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2']); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', + 'exit Forwards 2', 'enter Forwards 3']); + }); + + it('should enter completion step after jumping over second optional step', () => { + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3']); + }); + + it('should set the wizard as completed after entering the completion step', () => { + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.completed).toBe(true); + }); + + it('should be unable to leave the completion step', () => { + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.canGoToStep(0)).toBe(false); + expect(wizardTest.wizard.canGoToStep(1)).toBe(false); + }); + + + it('should not be able to leave the completion step in any direction', () => { + wizardTest.isValid = false; + + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.wizard.currentStep.canExit).toBe(false); + }); + + it('should not leave the completion step if it can\'t be exited', () => { + wizardTest.isValid = false; + + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3', 'enter Stay 3']); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.ts new file mode 100755 index 0000000000..eccb027386 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-completion-step.component.ts @@ -0,0 +1,145 @@ +/** + * Created by marc on 20.05.17. + */ + +import {Component, ContentChild, EventEmitter, forwardRef, HostBinding, Inject, Input, Output} from '@angular/core'; +import {MovingDirection} from '../util/moving-direction.enum'; +import {WizardComponent} from './wizard.component'; +import {WizardStep} from '../util/wizard-step.interface'; +import {WizardStepTitleDirective} from '../directives/wizard-step-title.directive'; +import {WizardCompletionStep} from '../util/wizard-completion-step.inferface'; + +/** + * The `wizard-completion-step` component can be used to define a completion/success step at the end of your wizard + * After a `wizard-completion-step` has been entered, it has the characteristic that the user is blocked from + * leaving it again to a previous step. + * In addition entering a `wizard-completion-step` automatically sets the `wizard` amd all steps inside the `wizard` + * as completed. + * + * ### Syntax + * + * ```html + * + * ... + * + * ``` + * + * ### Example + * + * ```html + * + * ... + * + * ``` + * + * With a navigation symbol from the `font-awesome` font: + * + * ```html + * + * ... + * + * ``` + * + * @author Marc Arndt + */ +@Component({ + selector: 'wizard-completion-step', + templateUrl: 'wizard-completion-step.component.html', + styleUrls: ['wizard-completion-step.component.css'], + providers: [ + { provide: WizardStep, useExisting: forwardRef(() => WizardCompletionStepComponent) }, + { provide: WizardCompletionStep, useExisting: forwardRef(() => WizardCompletionStepComponent) } + ] +}) +export class WizardCompletionStepComponent extends WizardCompletionStep { + /** + * @inheritDoc + */ + @ContentChild(WizardStepTitleDirective) + public titleTemplate: WizardStepTitleDirective; + + /** + * @inheritDoc + */ + @Input() + public title: string; + + /** + * @inheritDoc + */ + @Input() + public navigationSymbol = ''; + + /** + * @inheritDoc + */ + @Input() + public navigationSymbolFontFamily: string; + + /** + * @inheritDoc + */ + @Output() + public stepEnter = new EventEmitter(); + + /** + * @inheritDoc + */ + public stepExit = new EventEmitter(); + + /** + * @inheritDoc + */ + @HostBinding('hidden') + public get hidden(): boolean { + return !this.selected; + } + + /** + * @inheritDoc + */ + public completed: false; + + /** + * @inheritDoc + */ + public selected = false; + + /** + * @inheritDoc + */ + public optional = false; + + /** + * @inheritDoc + */ + public canExit: ((direction: MovingDirection) => boolean) | boolean = false; + + /** + * Constructor + * @param wizard The [[WizardComponent]], this completion step is contained inside + */ + /* istanbul ignore next */ + constructor(@Inject(forwardRef(() => WizardComponent)) private wizard: WizardComponent) { + super(); + } + + /** + * @inheritDoc + */ + enter(direction: MovingDirection): void { + this.wizard.completed = true; + this.stepEnter.emit(direction); + } + + /** + * @inheritDoc + */ + exit(direction: MovingDirection): void { + this.wizard.completed = false; + this.stepExit.emit(direction); + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.html b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.html new file mode 100755 index 0000000000..a697ab343a --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.html @@ -0,0 +1,21 @@ + diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.scss b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.scss new file mode 100644 index 0000000000..0ce60f32dd --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.scss @@ -0,0 +1,181 @@ +@import '../../../../sass/main'; +/* + color definitions + */ +$wz-color-default: #8492a0; +$wz-color-current: #0456de; +$text-color: white; + +$wz-color-done: #009e1a; +$wz-color-optional: #8492a0; +$wz-color-editing: #0456de; + +$text-height: 14px; + +$bubble-diameter: 30px; + +:host { + &.NavSymbols { + ul.steps-indicator { + padding: 0px 0 10px 0; + + li { + &:not(:last-child):before { + background-color: $wz-color-default; + content: ''; + position: absolute; + height: 1px; + width: calc(100% - #{$bubble-diameter}); + top: -$bubble-diameter/2; + left: calc(50% + #{$bubble-diameter/2}); + } + + &:after { + position: absolute; + top: -$bubble-diameter; + left: calc(50% - #{$bubble-diameter/2}); + width: $bubble-diameter; + height: $bubble-diameter; + text-align: center; + vertical-align: middle; + line-height: $bubble-diameter; + transition: 0.25s; + border-radius: 100%; + background-color: $wz-color-default; + color: $text-color; + content: attr(step-symbol); + } + } + + // default steps shouldn't change when hovered, because they aren't clickable + li.default a:hover { + color: $wz-color-current; + } + + li.current:after { + background-color: $wz-color-current; + color: $text-color; + box-shadow: 0px 0px 0px 8px transparentize($wz-color-current, 0.5); + } + + li.done:after { + background-color: $wz-color-done; + color: $text-color; + content: url(/ng-full/image/success.svg); + } + + li.optional:after { + background-color: $wz-color-optional; + color: $text-color; + } + + li.editing:after { + background-color: $wz-color-editing; + color: $text-color; + box-shadow: 0px 0px 0px 8px transparentize($wz-color-current, 0.5); + } + } + } + + ul.steps-indicator { + display: flex; + flex-direction: row; + justify-content: center; + + right: 0; + bottom: 0; + left: 0; + margin: 60px auto 0px auto; + width: 50%; + list-style: none; + + @mixin steps($number-of-components) { + &:before { + left: 100% / $number-of-components / 2; + right: 100% / $number-of-components / 2; + } + + li { + width: 100% / $number-of-components; + } + } + + &.steps-2 { + @include steps(2); + } + + &.steps-3 { + @include steps(3); + } + + &.steps-4 { + @include steps(4); + } + + &.steps-5 { + @include steps(5); + } + + &.steps-6 { + @include steps(6); + } + + &.steps-7 { + @include steps(7); + } + + &.steps-8 { + @include steps(8); + } + + &.steps-9 { + @include steps(9); + } + + &.steps-10 { + @include steps(10); + } + + /* --- http://www.paulirish.com/2012/box-sizing-border-box-ftw/ ---- */ + * { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + + li { + position: relative; + margin: 0; + padding: 10px 0 0 0; + + div { + display: flex; + flex-direction: column; + align-items: center; + + a { + color: black; + line-height: 0.9em; + font-size: 0.9em; + text-decoration: none; + text-transform: uppercase; + text-align: center; + font-weight: bold; + transition: 0.25s; + cursor: pointer; + + &:hover { + color: darken($primary-color, 20%); + } + } + } + } + + li.default, + li.current, + li.optional, + li.editing { + pointer-events: none; + } + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.spec.ts new file mode 100755 index 0000000000..22a8b09ec3 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.spec.ts @@ -0,0 +1,410 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {ViewChild, Component} from '@angular/core'; +import {WizardNavigationBarComponent} from './wizard-navigation-bar.component'; +import {WizardComponent} from './wizard.component'; +import {By} from '@angular/platform-browser'; +import {WizardModule} from '../wizard.module'; + +@Component({ + selector: 'test-wizard', + template: ` + + Step 1 + Step 2 + Step 3 + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; +} + +describe('WizardNavigationBarComponent', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar'))).toBeTruthy(); + }); + + it('should create only one navigation bar', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTestFixture.debugElement.queryAll(By.css('wizard-navigation-bar')).length).toBe(1); + }); + + it('should show the initial step correctly', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + const allLi = navBar.queryAll(By.css('li')); + + const currentLi = navBar.queryAll(By.css('li.current')); + const doneLi = navBar.queryAll(By.css('li.done')); + const editingLi = navBar.queryAll(By.css('li.editing')); + const optionalLi = navBar.queryAll(By.css('li.optional')); + const defaultLi = navBar.queryAll(By.css('li.default')); + + // the first step is the current step + expect(currentLi.length).toBe(1); + expect(currentLi[0]).toBe(allLi[0]); + + // no step is currently marked as done + expect(doneLi.length).toBe(0); + + // no step is marked as editing + expect(editingLi.length).toBe(0); + + // only the second step is marked as optional + expect(optionalLi.length).toBe(1); + expect(optionalLi[0]).toBe(allLi[1]); + + // the second and third step is marked as default (neither done or current) + expect(defaultLi.length).toBe(1); + expect(defaultLi[0]).toBe(allLi[2]); + }); + + it('should show the second step correctly', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + // go to second step + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + const allLi = navBar.queryAll(By.css('li')); + + const currentLi = navBar.queryAll(By.css('li.current')); + const doneLi = navBar.queryAll(By.css('li.done')); + const editingLi = navBar.queryAll(By.css('li.editing')); + const optionalLi = navBar.queryAll(By.css('li.optional')); + const defaultLi = navBar.queryAll(By.css('li.default')); + + // the second step is the current step + expect(currentLi.length).toBe(1); + expect(currentLi[0]).toBe(allLi[1]); + + // the first step should be marked as done + expect(doneLi.length).toBe(1); + expect(doneLi[0]).toBe(allLi[0]); + + // no step is marked as editing + expect(editingLi.length).toBe(0); + + // no step is marked as optional, because the optional step is the current step + expect(optionalLi.length).toBe(0); + + // only the third step is marked as default (neither done or current) + expect(defaultLi.length).toBe(1); + expect(defaultLi[0]).toBe(allLi[2]); + }); + + it('should show the third step correctly', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + // go to second step + wizardTest.wizard.goToNextStep(); + // go to third step + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + const allLi = navBar.queryAll(By.css('li')); + + const currentLi = navBar.queryAll(By.css('li.current')); + const doneLi = navBar.queryAll(By.css('li.done')); + const editingLi = navBar.queryAll(By.css('li.editing')); + const optionalLi = navBar.queryAll(By.css('li.optional')); + const defaultLi = navBar.queryAll(By.css('li.default')); + + // the third step is the current step + expect(currentLi.length).toBe(1); + expect(currentLi[0]).toBe(allLi[2]); + + // the first and second step should be marked as done + expect(doneLi.length).toBe(2); + expect(doneLi[0]).toBe(allLi[0]); + expect(doneLi[1]).toBe(allLi[1]); + + // no step is marked as editing + expect(editingLi.length).toBe(0); + + // no step is marked as optional, because the optional step is a "done" step + expect(optionalLi.length).toBe(0); + + // no step is marked as default (neither done, current or optional) + expect(defaultLi.length).toBe(0); + }); + + it('should show the third step correctly, after jump from first to third step', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + // go to third step and jump over the optional second step + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + const allLi = navBar.queryAll(By.css('li')); + + const currentLi = navBar.queryAll(By.css('li.current')); + const doneLi = navBar.queryAll(By.css('li.done')); + const editingLi = navBar.queryAll(By.css('li.editing')); + const optionalLi = navBar.queryAll(By.css('li.optional')); + const defaultLi = navBar.queryAll(By.css('li.default')); + + // the third step is the current step + expect(currentLi.length).toBe(1); + expect(currentLi[0]).toBe(allLi[2]); + + // the first step should be marked as done + expect(doneLi.length).toBe(1); + expect(doneLi[0]).toBe(allLi[0]); + + // no step is marked as editing + expect(editingLi.length).toBe(0); + + // the second step is marked as optional, because we jumped over it + expect(optionalLi.length).toBe(1); + expect(optionalLi[0]).toBe(allLi[1]); + + // the second step is marked as default (neither done nor current) + expect(defaultLi.length).toBe(0); + }); + + it('should show the first step correctly, after going back from the second step to the first step', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + // go to second step + wizardTest.wizard.goToNextStep(); + // go back to first step + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + const allLi = navBar.queryAll(By.css('li')); + + const currentLi = navBar.queryAll(By.css('li.current')); + const doneLi = navBar.queryAll(By.css('li.done')); + const editingLi = navBar.queryAll(By.css('li.editing')); + const optionalLi = navBar.queryAll(By.css('li.optional')); + const defaultLi = navBar.queryAll(By.css('li.default')); + + // no step is the current step + expect(currentLi.length).toBe(0); + + // no step should be marked as done + expect(doneLi.length).toBe(0); + + // the first step is marked as editing + expect(editingLi.length).toBe(1); + expect(editingLi[0]).toBe(allLi[0]); + + // the second step is marked as optional + expect(optionalLi.length).toBe(1); + expect(optionalLi[0]).toBe(allLi[1]); + + // the second and third step is marked as default (neither done or current) + expect(defaultLi.length).toBe(1); + expect(defaultLi[0]).toBe(allLi[2]); + }); + + it('should show the first step correctly, after first jumping from the first to the third step ' + + 'and then back from the third step to the first step', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + // go to third step, by jumping over the optional step + wizardTest.wizard.goToStep(2); + // go back to first step + wizardTest.wizard.goToStep(0); + wizardTestFixture.detectChanges(); + + const allLi = navBar.queryAll(By.css('li')); + + const currentLi = navBar.queryAll(By.css('li.current')); + const doneLi = navBar.queryAll(By.css('li.done')); + const editingLi = navBar.queryAll(By.css('li.editing')); + const optionalLi = navBar.queryAll(By.css('li.optional')); + const defaultLi = navBar.queryAll(By.css('li.default')); + + // no step is the current step + expect(currentLi.length).toBe(0); + + // no step should be marked as done + expect(doneLi.length).toBe(0); + + // the first step is marked as editing + expect(editingLi.length).toBe(1); + expect(editingLi[0]).toBe(allLi[0]); + + // the second step is marked as optional + expect(optionalLi.length).toBe(1); + expect(optionalLi[0]).toBe(allLi[1]); + + // the second and third step is marked as default (neither done or current) + expect(defaultLi.length).toBe(1); + expect(defaultLi[0]).toBe(allLi[2]); + }); + + it('should show the second step correctly, after first jumping from the first to the third step ' + + 'and then back from the third step to the second step', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + // go to third step, by jumping over the optional step + wizardTest.wizard.goToStep(2); + // go back to second step + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + const allLi = navBar.queryAll(By.css('li')); + + const currentLi = navBar.queryAll(By.css('li.current')); + const doneLi = navBar.queryAll(By.css('li.done')); + const editingLi = navBar.queryAll(By.css('li.editing')); + const optionalLi = navBar.queryAll(By.css('li.optional')); + const defaultLi = navBar.queryAll(By.css('li.default')); + + // the second step is the current step + expect(currentLi.length).toBe(1); + expect(currentLi[0]).toBe(allLi[1]); + + // the first step should be marked as done + expect(doneLi.length).toBe(1); + expect(doneLi[0]).toBe(allLi[0]); + + // no step is marked as editing + expect(editingLi.length).toBe(0); + + // no step is marked as optional, because the optional step is the current step + expect(optionalLi.length).toBe(0); + + // the third step is marked as default (neither done or current) + expect(defaultLi.length).toBe(1); + expect(defaultLi[0]).toBe(allLi[2]); + }); + + it('should move back to the first step from the second step, after clicking on the corresponding link', () => { + const goToFirstStepLink = wizardTestFixture.debugElement.query(By.css('li:nth-child(1) a')).nativeElement; + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + + // go to the second step + wizardTest.wizard.goToNextStep(); + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + // go back to the first step + goToFirstStepLink.click(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + }); + + it('should move back to the first step from the third step, after clicking on the corresponding link', () => { + const goToFirstStepLink = wizardTestFixture.debugElement.query(By.css('li:nth-child(1) a')).nativeElement; + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + + // go to the second step + wizardTest.wizard.goToStep(2); + expect(wizardTest.wizard.currentStepIndex).toBe(2); + + // go back to the first step + goToFirstStepLink.click(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + }); + + it('should move back to the second step from the third step, after clicking on the corresponding link', () => { + const goToSecondStepLink = wizardTestFixture.debugElement.query(By.css('li:nth-child(2) a')).nativeElement; + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + + // go to the second step + wizardTest.wizard.goToStep(2); + expect(wizardTest.wizard.currentStepIndex).toBe(2); + + // go back to the first step + goToSecondStepLink.click(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + }); + + it('should not move to the second step from the first step, after clicking on the corresponding link', () => { + const goToFirstStepLink = wizardTestFixture.debugElement.query(By.css('li:nth-child(1)')); + const goToSecondStepLink = wizardTestFixture.debugElement.query(By.css('li:nth-child(2)')); + const goToThirdStepLink = wizardTestFixture.debugElement.query(By.css('li:nth-child(3)')); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + // links contain a class that is not clickable (contains "pointer-events: none;") + expect(goToFirstStepLink.classes.hasOwnProperty('current')).toBeTruthy('First step label is clickable'); + expect(goToSecondStepLink.classes.hasOwnProperty('default')).toBeTruthy('Second step label is clickable'); + expect(goToThirdStepLink.classes.hasOwnProperty('default')).toBeTruthy('Third step label is clickable'); + }); + + it('should use the \"small\" layout when no navigation bar layout is specified', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('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 }); + }); + + it('should use the \"small\" layout when it is specified', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + wizardTest.wizard.navBarLayout = 'small'; + wizardTestFixture.detectChanges(); + + expect(navBar.classes).toEqual({ 'horizontal': true, 'vertical': false, 'small': true, + 'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false }); + }); + + it('should use the \"large-filled\" layout when it is specified', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + wizardTest.wizard.navBarLayout = 'large-filled'; + wizardTestFixture.detectChanges(); + + expect(navBar.classes).toEqual({ 'horizontal': true, 'vertical': false, 'small': false, + 'large-filled': true, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': false }); + }); + + it('should use the \"large-empty\" layout when it is specified', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + wizardTest.wizard.navBarLayout = 'large-empty'; + wizardTestFixture.detectChanges(); + + expect(navBar.classes).toEqual({ 'horizontal': true, 'vertical': false, 'small': false, + 'large-filled': false, 'large-filled-symbols': false, 'large-empty': true, 'large-empty-symbols': false }); + }); + + it('should use the \"large-filled-symbols\" layout when it is specified', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + wizardTest.wizard.navBarLayout = 'large-filled-symbols'; + wizardTestFixture.detectChanges(); + + expect(navBar.classes).toEqual({ 'horizontal': true, 'vertical': false, 'small': false, + 'large-filled': false, 'large-filled-symbols': true, 'large-empty': false, 'large-empty-symbols': false }); + }); + + it('should use the \"large-empty-symbols\" layout when it is specified', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + + wizardTest.wizard.navBarLayout = 'large-empty-symbols'; + wizardTestFixture.detectChanges(); + + expect(navBar.classes).toEqual({ 'horizontal': true, 'vertical': false, 'small': false, + 'large-filled': false, 'large-filled-symbols': false, 'large-empty': false, 'large-empty-symbols': true }); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.ts new file mode 100755 index 0000000000..6ff5421f4b --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-navigation-bar.component.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { WizardComponent } from './wizard.component'; +import { WizardStep } from '../util/wizard-step.interface'; + +/** + * The `wizard-navigation-bar` component contains the navigation bar inside a [[WizardComponent]]. + * To correctly display the navigation bar, it's required to set the right css classes for the navigation bar, + * otherwise it will look like a normal `ul` component. + * + * ### Syntax + * + * ```html + * + * ``` + * + * @author Marc Arndt + */ +@Component({ + selector: 'wizard-navigation-bar', + templateUrl: 'wizard-navigation-bar.component.html', + styleUrls: ['wizard-navigation-bar.component.scss'] +}) +export class WizardNavigationBarComponent { + /** + * Constructor + * + * @param wizard The wizard, which includes this navigation bar + */ + constructor(private wizard: WizardComponent) {} + + /** + * Returns all [[WizardStep]]s contained in the wizard + * + * @returns {Array} An array containing all [[WizardStep]]s + */ + get wizardSteps(): Array { + return this.wizard.wizardSteps.toArray(); + } + + /** + * Returns the number of wizard steps, that need to be displaced in the navigation bar + * + * @returns {number} The number of wizard steps to be displayed + */ + get numberOfWizardSteps(): number { + return this.wizard.wizardSteps.length; + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.css b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.css new file mode 100755 index 0000000000..7fc4514270 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.css @@ -0,0 +1,4 @@ +:host { + height: auto; + width: 100%; +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.html b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.html new file mode 100755 index 0000000000..6dbc743063 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.html @@ -0,0 +1 @@ + diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.spec.ts new file mode 100755 index 0000000000..6cea34e402 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.spec.ts @@ -0,0 +1,258 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {WizardStepComponent} from './wizard-step.component'; +import {ViewChild, Component} from '@angular/core'; +import {WizardComponent} from './wizard.component'; +import {MovingDirection} from '../util/moving-direction.enum'; +import {By} from '@angular/platform-browser'; +import {WizardModule} from '../wizard.module'; + +@Component({ + selector: 'test-wizard', + template: ` + + Step 1 + Step 2 + Step 3 + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; + + public isValid: any = true; + + public eventLog: Array = new Array(); + + enterInto(direction: MovingDirection, destination: number): void { + this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); + } + + exitFrom(direction: MovingDirection, source: number): void { + this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); + } +} + +describe('WizardStepComponent', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTestFixture.debugElement.queryAll(By.css('wizard-step')).length).toBe(3); + }); + + it('should have correct step title', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTest.wizard.getStepAtIndex(0).title).toBe('Steptitle 1'); + expect(wizardTest.wizard.getStepAtIndex(1).title).toBe('Steptitle 2'); + expect(wizardTest.wizard.getStepAtIndex(2).title).toBe('Steptitle 3'); + }); + + it('should enter first step after initialisation', () => { + expect(wizardTest.eventLog).toEqual(['enter Forwards 1']); + }); + + it('should enter second step after first step', () => { + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2']); + }); + + it('should enter first step after exiting second step', () => { + wizardTest.wizard.goToNextStep(); + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Backwards 2', 'enter Backwards 1']); + }); + + it('should enter third step after jumping over second optional step', () => { + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3']); + }); + + it('should enter first step after jumping over second optional step two times', () => { + wizardTest.wizard.goToStep(2); + wizardTest.wizard.goToStep(0); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3', 'exit Backwards 3', 'enter Backwards 1']); + }); + + it('should enter second step after jumping over second optional step and the going back once', () => { + wizardTest.wizard.goToStep(2); + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3', 'exit Backwards 3', 'enter Backwards 2']); + }); + + it('should stay at first step correctly', () => { + wizardTest.wizard.goToStep(0); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Stay 1', 'enter Stay 1']); + }); + + it('should not be able to leave the second step in any direction', () => { + wizardTest.isValid = false; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.wizard.currentStep.canExit).toBe(false); + }); + + it('should not be able to leave the second step in forwards direction', () => { + wizardTest.isValid = direction => direction !== MovingDirection.Forwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Forwards)).toBe(false); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Backwards)).toBe(true); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Stay)).toBe(true); + }); + + it('should not be able to leave the second step in backwards direction', () => { + wizardTest.isValid = direction => direction !== MovingDirection.Backwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Forwards)).toBe(true); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Backwards)).toBe(false); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Stay)).toBe(true); + }); + + it('should throw error when method "canExit" is malformed', () => { + wizardTest.isValid = 'String'; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(() => wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Forwards)) + .toThrow(new Error(`Input value 'String' is neither a boolean nor a function`)); + }); + + it('should not leave the second step in forward direction if it can\'t be exited', () => { + wizardTest.isValid = false; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Stay 2', 'enter Stay 2']); + }); + + it('should not leave the second step in backward direction if it can\'t be exited', () => { + wizardTest.isValid = false; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Stay 2', 'enter Stay 2']); + }); + + it('should not leave the second step in forward direction if it can\'t be exited in this direction', () => { + wizardTest.isValid = direction => direction === MovingDirection.Backwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Stay 2', 'enter Stay 2']); + }); + + it('should not leave the second step in backward direction if it can\'t be exited in this direction', () => { + wizardTest.isValid = direction => direction === MovingDirection.Forwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Stay 2', 'enter Stay 2']); + }); + + it('should leave the second step in forward direction if it can be exited in this direction', () => { + wizardTest.isValid = direction => direction === MovingDirection.Forwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Forwards 2', 'enter Forwards 3']); + }); + + it('should leave the second step in backward direction if it can be exited in this direction', () => { + wizardTest.isValid = direction => direction === MovingDirection.Backwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Backwards 2', 'enter Backwards 1']); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.ts new file mode 100755 index 0000000000..cdba22e60b --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard-step.component.ts @@ -0,0 +1,148 @@ +import {Component, ContentChild, EventEmitter, forwardRef, HostBinding, Input, Output} from '@angular/core'; +import {MovingDirection} from '../util/moving-direction.enum'; +import {WizardStep} from '../util/wizard-step.interface'; +import {WizardStepTitleDirective} from '../directives/wizard-step-title.directive'; + +/** + * The `wizard-step` component is used to define a normal step inside a wizard. + * + * ### Syntax + * + * With `title` input: + * + * ```html + * + * ... + * + * ``` + * + * With `wizardStepTitle` directive: + * + * ```html + * + * + * step title + * + * ... + * + * ``` + * + * ### Example + * + * With `title` input: + * + * ```html + * + * ... + * + * ``` + * + * With `wizardStepTitle` directive: + * + * ```html + * + * + * Address information + * + * + * ``` + * + * @author Marc Arndt + */ +@Component({ + selector: 'wizard-step', + templateUrl: 'wizard-step.component.html', + styleUrls: ['wizard-step.component.css'], + providers: [ + { provide: WizardStep, useExisting: forwardRef(() => WizardStepComponent) } + ] +}) +export class WizardStepComponent extends WizardStep { + /** + * @inheritDoc + */ + @ContentChild(WizardStepTitleDirective) + public titleTemplate: WizardStepTitleDirective; + + /** + * @inheritDoc + */ + @Input() + public title: string; + + /** + * @inheritDoc + */ + @Input() + public navigationSymbol = ''; + + /** + * @inheritDoc + */ + @Input() + public navigationSymbolFontFamily: string; + + /** + * @inheritDoc + */ + @Input() + public canExit: ((direction: MovingDirection) => boolean) | boolean = true; + + /** + * @inheritDoc + */ + @Output() + public stepEnter = new EventEmitter(); + + /** + * @inheritDoc + */ + @Output() + public stepExit = new EventEmitter(); + + /** + * @inheritDoc + */ + @HostBinding('hidden') + public get hidden(): boolean { + return !this.selected; + } + + /** + * @inheritDoc + */ + public completed = false; + + /** + * @inheritDoc + */ + public selected = false; + + /** + * @inheritDoc + */ + public optional = false; + + /** + * Constructor + */ + constructor() { + super(); + } + + /** + * @inheritDoc + */ + enter(direction: MovingDirection): void { + this.stepEnter.emit(direction); + } + + /** + * @inheritDoc + */ + exit(direction: MovingDirection): void { + this.stepExit.emit(direction); + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.html b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.html new file mode 100755 index 0000000000..6506a3659f --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.html @@ -0,0 +1,6 @@ + + + +
+ +
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.scss b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.scss new file mode 100644 index 0000000000..129f8d606f --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.scss @@ -0,0 +1,19 @@ +:host { + display: flex; + justify-content: flex-start; + + &.vertical { + flex-direction: row; + } + + &.horizontal { + flex-direction: column; + } + + .wizard-steps { + top: 0; + display: flex; + width: 100%; + flex-direction: row; + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.spec.ts new file mode 100755 index 0000000000..f3cd615ab4 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.spec.ts @@ -0,0 +1,374 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {QueryList, Component, ViewChild} from '@angular/core'; +import {WizardComponent} from './wizard.component'; +import {By} from '@angular/platform-browser'; +import {WizardStep} from '../util/wizard-step.interface'; +import {WizardModule} from '../wizard.module'; + +@Component({ + selector: 'test-wizard', + template: ` + + Step 1 + Step 2 + Step 3 + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; +} + +function checkWizardSteps(steps: QueryList, selectedStepIndex: number) { + steps.forEach((step, index, array) => { + // Only the selected step should be selected + if (index === selectedStepIndex) { + expect(step.selected).toBe(true, `the selected wizard step index ${index} is not selected`); + } else { + expect(step.selected).toBe(false, `the not selected wizard step index ${index} is selected`); + } + + // All steps before the selected step need to be completed + if (index < selectedStepIndex) { + expect(step.completed).toBe(true, + `the wizard step ${index} is not completed while the currently selected step index is ${selectedStepIndex}`); + } else if (index > selectedStepIndex) { + expect(step.completed).toBe(false, + `the wizard step ${index} is completed while the currently selected step index is ${selectedStepIndex}`); + } + }); +} + +describe('WizardComponent', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTest.wizard).toBeTruthy(); + expect(wizardTestFixture.debugElement.query(By.css('wizard'))).toBeTruthy(); + }); + + it('should contain navigation bar at the correct position in default navBarLocation mode', () => { + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + const wizard = wizardTestFixture.debugElement.query(By.css('wizard')); + const wizardStepsDiv = wizardTestFixture.debugElement.query(By.css('div.wizard-steps')); + + // check default: the navbar should be at the top of the wizard if no navBarLocation was set + expect(navBar).toBeTruthy(); + expect(wizardTestFixture.debugElement.query(By.css('wizard')).children.length).toBe(2); + expect(wizardTestFixture.debugElement.query(By.css('wizard > :first-child')).name).toBe('wizard-navigation-bar'); + expect(wizardTestFixture.debugElement.query(By.css('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 }); + }); + + it('should contain navigation bar at the correct position in top navBarLocation mode', () => { + wizardTest.wizard.navBarLocation = 'top'; + wizardTestFixture.detectChanges(); + + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + const wizard = wizardTestFixture.debugElement.query(By.css('wizard')); + const wizardStepsDiv = wizardTestFixture.debugElement.query(By.css('div.wizard-steps')); + + // check default: the navbar should be at the top of the wizard if no navBarLocation was set + expect(navBar).toBeTruthy(); + expect(wizardTestFixture.debugElement.query(By.css('wizard')).children.length).toBe(2); + expect(wizardTestFixture.debugElement.query(By.css('wizard > :first-child')).name).toBe('wizard-navigation-bar'); + expect(wizardTestFixture.debugElement.query(By.css('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 }); + }); + + it('should contain navigation bar at the correct position in left navBarLocation mode', () => { + wizardTest.wizard.navBarLocation = 'left'; + wizardTestFixture.detectChanges(); + + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + const wizard = wizardTestFixture.debugElement.query(By.css('wizard')); + const wizardStepsDiv = wizardTestFixture.debugElement.query(By.css('div.wizard-steps')); + + // check default: the navbar should be at the top of the wizard if no navBarLocation was set + expect(navBar).toBeTruthy(); + expect(wizardTestFixture.debugElement.query(By.css('wizard')).children.length).toBe(2); + expect(wizardTestFixture.debugElement.query(By.css('wizard > :first-child')).name).toBe('wizard-navigation-bar'); + expect(wizardTestFixture.debugElement.query(By.css('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 }); + }); + + it('should contain navigation bar at the correct position in bottom navBarLocation mode', () => { + wizardTest.wizard.navBarLocation = 'bottom'; + wizardTestFixture.detectChanges(); + + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + const wizard = wizardTestFixture.debugElement.query(By.css('wizard')); + const wizardStepsDiv = wizardTestFixture.debugElement.query(By.css('div.wizard-steps')); + + // check default: the navbar should be at the top of the wizard if no navBarLocation was set + expect(navBar).toBeTruthy(); + expect(wizardTestFixture.debugElement.query(By.css('wizard')).children.length).toBe(2); + expect(wizardTestFixture.debugElement.query(By.css('wizard > :first-child')).name).toBe('div'); + expect(wizardTestFixture.debugElement.query(By.css('wizard > :last-child')).name).toBe('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 }); + }); + + it('should contain navigation bar at the correct position in right navBarLocation mode', () => { + wizardTest.wizard.navBarLocation = 'right'; + wizardTestFixture.detectChanges(); + + const navBar = wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')); + const wizard = wizardTestFixture.debugElement.query(By.css('wizard')); + const wizardStepsDiv = wizardTestFixture.debugElement.query(By.css('div.wizard-steps')); + + // check default: the navbar should be at the top of the wizard if no navBarLocation was set + expect(navBar).toBeTruthy(); + expect(wizardTestFixture.debugElement.query(By.css('wizard')).children.length).toBe(2); + expect(wizardTestFixture.debugElement.query(By.css('wizard > :first-child')).name).toBe('div'); + expect(wizardTestFixture.debugElement.query(By.css('wizard > :last-child')).name).toBe('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 }); + }); + + it('should have steps', () => { + expect(wizardTest.wizard.wizardSteps.length).toBe(3); + }); + + it('should start at first step', () => { + expect(wizardTest.wizard.currentStepIndex).toBe(0); + expect(wizardTest.wizard.currentStep.title).toBe('Steptitle 1'); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 0); + }); + + it('should return correct step at index', () => { + expect(() => wizardTest.wizard.getStepAtIndex(-1)) + .toThrow(new Error(`Expected a known step, but got stepIndex: -1.`)); + + expect(wizardTest.wizard.getStepAtIndex(0).title).toBe('Steptitle 1'); + expect(wizardTest.wizard.getStepAtIndex(1).title).toBe('Steptitle 2'); + expect(wizardTest.wizard.getStepAtIndex(2).title).toBe('Steptitle 3'); + + // Check that the first wizard step is the only selected one + checkWizardSteps(wizardTest.wizard.wizardSteps, 0); + + expect(() => wizardTest.wizard.getStepAtIndex(3)) + .toThrow(new Error(`Expected a known step, but got stepIndex: 3.`)); + }); + + it('should return correct index at step', () => { + expect(wizardTest.wizard.getIndexOfStep(wizardTest.wizard.getStepAtIndex(0))).toBe(0); + expect(wizardTest.wizard.getIndexOfStep(wizardTest.wizard.getStepAtIndex(1))).toBe(1); + expect(wizardTest.wizard.getIndexOfStep(wizardTest.wizard.getStepAtIndex(2))).toBe(2); + }); + + it('should return correct can go to step', () => { + expect(wizardTest.wizard.canGoToStep(-1)).toBe(false); + expect(wizardTest.wizard.canGoToStep(0)).toBe(true); + expect(wizardTest.wizard.canGoToStep(1)).toBe(true); + expect(wizardTest.wizard.canGoToStep(2)).toBe(false); + expect(wizardTest.wizard.canGoToStep(3)).toBe(false); + }); + + it('should return correct can go to next step', () => { + expect(wizardTest.wizard.canGoToNextStep()).toBe(true); + + wizardTest.wizard.goToNextStep(); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 1); + expect(wizardTest.wizard.canGoToNextStep()).toBe(true); + + wizardTest.wizard.goToNextStep(); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 2); + expect(wizardTest.wizard.canGoToNextStep()).toBe(false); + + // should do nothing + wizardTest.wizard.goToNextStep(); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 2); + }); + + it('should return correct can go to previous step', () => { + expect(wizardTest.wizard.canGoToPreviousStep()).toBe(false); + + // should do nothing + wizardTest.wizard.goToPreviousStep(); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 0); + + wizardTest.wizard.goToNextStep(); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 1); + expect(wizardTest.wizard.canGoToPreviousStep()).toBe(true); + }); + + it('should go to step', () => { + checkWizardSteps(wizardTest.wizard.wizardSteps, 0); + + wizardTest.wizard.goToStep(1); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.wizard.currentStep).toBe(wizardTest.wizard.getStepAtIndex(1)); + expect(wizardTest.wizard.currentStep.completed).toBe(false); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 1); + + wizardTest.wizard.goToStep(2); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.wizard.currentStep).toBe(wizardTest.wizard.getStepAtIndex(2)); + expect(wizardTest.wizard.currentStep.completed).toBe(false); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 2); + + wizardTest.wizard.goToStep(0); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + expect(wizardTest.wizard.currentStep).toBe(wizardTest.wizard.getStepAtIndex(0)); + expect(wizardTest.wizard.currentStep.completed).toBe(true); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 0); + + wizardTest.wizard.goToStep(1); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.wizard.currentStep).toBe(wizardTest.wizard.getStepAtIndex(1)); + expect(wizardTest.wizard.currentStep.completed).toBe(false); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 1); + + wizardTest.wizard.goToStep(2); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.wizard.currentStep).toBe(wizardTest.wizard.getStepAtIndex(2)); + expect(wizardTest.wizard.currentStep.completed).toBe(false); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 2); + + wizardTest.wizard.goToStep(1); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.wizard.currentStep).toBe(wizardTest.wizard.getStepAtIndex(1)); + expect(wizardTest.wizard.currentStep.completed).toBe(true); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 1); + }); + + it('should go to next step', () => { + wizardTest.wizard.goToNextStep(); + + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.wizard.currentStep.title).toBe('Steptitle 2'); + expect(wizardTest.wizard.currentStep.completed).toBe(false); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 1); + }); + + it('should go to previous step', () => { + expect(wizardTest.wizard.getStepAtIndex(0).completed).toBe(false); + checkWizardSteps(wizardTest.wizard.wizardSteps, 0); + + wizardTest.wizard.goToStep(1); + + expect(wizardTest.wizard.getStepAtIndex(0).completed).toBe(true); + checkWizardSteps(wizardTest.wizard.wizardSteps, 1); + + wizardTest.wizard.goToPreviousStep(); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + expect(wizardTest.wizard.currentStep).toBe(wizardTest.wizard.getStepAtIndex(0)); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 0); + }); + + it('should have next step', () => { + checkWizardSteps(wizardTest.wizard.wizardSteps, 0); + expect(wizardTest.wizard.hasNextStep()).toBe(true); + + wizardTest.wizard.goToNextStep(); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 1); + expect(wizardTest.wizard.hasNextStep()).toBe(true); + + wizardTest.wizard.goToNextStep(); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 2); + expect(wizardTest.wizard.hasNextStep()).toBe(false); + }); + + it('should have previous step', () => { + checkWizardSteps(wizardTest.wizard.wizardSteps, 0); + expect(wizardTest.wizard.hasPreviousStep()).toBe(false); + + wizardTest.wizard.goToNextStep(); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 1); + expect(wizardTest.wizard.hasPreviousStep()).toBe(true); + + wizardTest.wizard.goToNextStep(); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 2); + expect(wizardTest.wizard.hasPreviousStep()).toBe(true); + }); + + it('should be last step', () => { + checkWizardSteps(wizardTest.wizard.wizardSteps, 0); + expect(wizardTest.wizard.isLastStep()).toBe(false); + + wizardTest.wizard.goToNextStep(); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 1); + expect(wizardTest.wizard.isLastStep()).toBe(false); + + wizardTest.wizard.goToNextStep(); + + checkWizardSteps(wizardTest.wizard.wizardSteps, 2); + expect(wizardTest.wizard.isLastStep()).toBe(true); + }); + + it('should reset the wizard correctly', () => { + wizardTest.wizard.goToNextStep(); + wizardTest.wizard.goToNextStep(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + checkWizardSteps(wizardTest.wizard.wizardSteps, 2); + + wizardTest.wizard.reset(); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + checkWizardSteps(wizardTest.wizard.wizardSteps, 0); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.ts new file mode 100755 index 0000000000..acaef323c7 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/components/wizard.component.ts @@ -0,0 +1,349 @@ +import { AfterContentInit, Component, ContentChildren, HostBinding, Input, QueryList } from '@angular/core'; +import { MovingDirection } from '../util/moving-direction.enum'; +import { WizardStep } from '../util/wizard-step.interface'; +import { isBoolean } from 'util'; + +/** + * The `wizard` component defines the root component of a wizard. + * Through the setting of input parameters for the `wizard` component it's possible to change the location and size + * of its navigation bar. + * + * ### Syntax + * ```html + * + * ... + * + * ``` + * + * ### Example + * + * Without completion step: + * + * ```html + * + * ... + * ... + * + * ``` + * + * With completion step: + * + * ```html + * + * ... + * ... + * ... + * + * ``` + * + * @author Marc Arndt + */ +@Component({ + selector: 'wizard', + templateUrl: 'wizard.component.html', + styleUrls: ['wizard.component.scss'] +}) +export class WizardComponent implements AfterContentInit { + /** + * A QueryList containing all WizardSteps in this Wizard + */ + @ContentChildren(WizardStep) public wizardSteps: QueryList; + + /** + * The location of the navigation bar inside the wizard. + * This location can be either top, bottom, left or right + * + * @type {string} + */ + @Input() public navBarLocation = 'top'; + + /** + * Returns true if this wizard uses a horizontal orientation. + * The wizard uses a horizontal orientation, iff the navigation bar is shown at the top or bottom of this wizard + * + * @returns {boolean} True if this wizard uses a horizontal orientation + */ + @HostBinding('class.horizontal') + public get horizontalOrientation(): boolean { + return this.navBarLocation === 'top' || this.navBarLocation === 'bottom'; + } + + /** + * Returns true if this wizard uses a vertical orientation. + * The wizard uses a vertical orientation, iff the navigation bar is shown at the left or right of this wizard + * + * @returns {boolean} True if this wizard uses a vertical orientation + */ + @HostBinding('class.vertical') + public get verticalOrientation(): boolean { + return this.navBarLocation === 'left' || this.navBarLocation === 'right'; + } + + /** + * The index of the currently visible and selected step inside the wizardSteps QueryList. + * If this wizard contains no steps, currentStepIndex is -1 + */ + public currentStepIndex = -1; + + /** + * The WizardStep object belonging to the currently visible and selected step. + * The currentStep is always the currently selected wizard step. + * The currentStep can be either completed, if it was visited earlier, + * or not completed, if it is visited for the first time or its state is currently out of date. + * + * If this wizard contains no steps, currentStep is null + */ + public currentStep: WizardStep; + + /** + * If this wizard has been completed, `completed` will be true + */ + public completed: boolean; + + /** + * Constructor + */ + constructor() {} + + /** + * Initialization work + */ + ngAfterContentInit(): void { + this.reset(); + } + + /** + * Checks if a given index `stepIndex` is inside the range of possible wizard steps inside this wizard + * + * @param stepIndex The to be checked index of a step inside this wizard + * @returns {boolean} True if the given `stepIndex` is contained inside this wizard, false otherwise + */ + hasStep(stepIndex: number): boolean { + return this.wizardSteps.length > 0 && 0 <= stepIndex && stepIndex < this.wizardSteps.length; + } + + /** + * Checks if this wizard has a previous step, compared to the current step + * + * @returns {boolean} True if this wizard has a previous step before the current step + */ + hasPreviousStep(): boolean { + return this.hasStep(this.currentStepIndex - 1); + } + + /** + * Checks if this wizard has a next step, compared to the current step + * + * @returns {boolean} True if this wizard has a next step after the current step + */ + hasNextStep(): boolean { + return this.hasStep(this.currentStepIndex + 1); + } + + /** + * Checks if this wizard is currently inside its last step + * + * @returns {boolean} True if the wizard is currently inside its last step + */ + isLastStep(): boolean { + return this.wizardSteps.length > 0 && this.currentStepIndex === this.wizardSteps.length - 1; + } + + /** + * Finds the [[WizardStep]] at the given index `stepIndex`. + * If no [[WizardStep]] exists at the given index an Error is thrown + * + * @param stepIndex The given index + * @returns {undefined|WizardStep} 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 { + if (!this.hasStep(stepIndex)) { + throw new Error(`Expected a known step, but got stepIndex: ${stepIndex}.`); + } + + return this.wizardSteps.find((item, index, array) => index === stepIndex); + } + + /** + * Find 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]] + * @returns {number} The found index of `step` or `-1` if the step is not included in the wizard + */ + getIndexOfStep(step: WizardStep): number { + let stepIndex: number = -1; + + this.wizardSteps.forEach((item, index, array) => { + if (item === step) { + stepIndex = index; + } + }); + + return stepIndex; + } + + /** + * Calculates the correct [[MovingDirection]] value for a given `destinationStep` compared to the `currentStepIndex`. + * + * @param destinationStep The given destination step + * @returns {MovingDirection} The calculated [[MovingDirection]] + */ + getMovingDirection(destinationStep: number): MovingDirection { + let movingDirection: MovingDirection; + + if (destinationStep > this.currentStepIndex) { + movingDirection = MovingDirection.Forwards; + } else if (destinationStep < this.currentStepIndex) { + movingDirection = MovingDirection.Backwards; + } else { + movingDirection = MovingDirection.Stay; + } + + return movingDirection; + } + + /** + * Tries to transition the wizard to the previous step from the `currentStep` + */ + goToPreviousStep(): void { + if (this.hasPreviousStep()) { + this.goToStep(this.currentStepIndex - 1); + } + } + + /** + * Tries to transition the wizard to the next step from the `currentStep` + */ + goToNextStep(): void { + if (this.hasNextStep()) { + this.goToStep(this.currentStepIndex + 1); + } + } + + /** + * Checks if it's possible to transition to the previous step from the `currentStep` + * + * @returns {boolean} True if it's possible to transition to the previous step, false otherwise + */ + canGoToPreviousStep(): boolean { + const previousStepIndex = this.currentStepIndex - 1; + + return this.hasStep(previousStepIndex) && this.canGoToStep(previousStepIndex); + } + + /** + * Checks if it's possible to transition to the next step from the `currentStep` + * + * @returns {boolean} True if it's possible to transition to the next step, false otherwise + */ + canGoToNextStep(): boolean { + const nextStepIndex = this.currentStepIndex + 1; + + return this.hasStep(nextStepIndex) && this.canGoToStep(nextStepIndex); + } + + /** + * Checks if it's possible to transition to the step with the given `stepIndex` from the `currentStep`. + * + * @param stepIndex The to be checked step index + * @returns {boolean} True if it's possible to transition to the given `stepIndex` + */ + canGoToStep(stepIndex: number): boolean { + let result: boolean = this.canExitStep(this.currentStep, this.getMovingDirection(stepIndex)) && this.hasStep(stepIndex); + + this.wizardSteps.forEach((wizardStep, index, array) => { + if (index < stepIndex && index !== this.currentStepIndex) { + // all steps before the next step, that aren't the current step, must be completed or optional + result = result && (wizardStep.completed || wizardStep.optional); + } + }); + + return result; + } + + /** + * Tries to transition to the given `destinationStepIndex`. + * This will only fail, if the `currentStep` can't be left + * + * @param destinationStepIndex The index of the destination step + */ + goToStep(destinationStepIndex: number): void { + const destinationStep: WizardStep = this.getStepAtIndex(destinationStepIndex); + + // In which direction is a step transition done? + const movingDirection: MovingDirection = this.getMovingDirection(destinationStepIndex); + + if (this.canExitStep(this.currentStep, movingDirection)) { + // is it possible to leave the current step in the given direction? + this.wizardSteps.forEach((wizardStep, index, array) => { + if (index === this.currentStepIndex) { + // finish processing old step + wizardStep.completed = true; + } + + if (this.currentStepIndex > destinationStepIndex && index > destinationStepIndex) { + // if the next step is before the current step set all steps in between to incomplete + wizardStep.completed = false; + } + }); + + // leave current step + this.currentStep.exit(movingDirection); + this.currentStep.selected = false; + + // go to next step + this.currentStepIndex = destinationStepIndex; + this.currentStep = destinationStep; + this.currentStep.enter(movingDirection); + this.currentStep.selected = true; + } else { + // if the current step can't be left, reenter the current step + this.currentStep.exit(MovingDirection.Stay); + this.currentStep.enter(MovingDirection.Stay); + } + } + + /** + * 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 { + // reset the step internal state + this.wizardSteps.forEach((step, index) => { + step.completed = false; + step.selected = false; + }); + + // set the wizard to incomplete + this.completed = false; + + // set the first step as the current step + this.currentStepIndex = 0; + this.currentStep = this.getStepAtIndex(0); + this.currentStep.selected = true; + this.currentStep.enter(MovingDirection.Forwards); + } + + /** + * This method returns true, if the given step `wizardStep` can be exited and false otherwise. + * Because this method depends on the value `canExit`, it will throw an error, if `canExit` is neither a boolean + * nor a function. + * + * @param wizardStep The [[WizardStep]] to be checked + * @param direction The direction in which this step should be left + * @returns {any} True if the given step `wizardStep` can be exited in the given direction, false otherwise + * @throws An `Error` is thrown if `wizardStep.canExit` is neither a function nor a boolean + */ + public canExitStep(wizardStep: WizardStep, direction: MovingDirection): boolean { + if (isBoolean(wizardStep.canExit)) { + return wizardStep.canExit as boolean; + } else if (wizardStep.canExit instanceof Function) { + return wizardStep.canExit(direction); + } else { + throw new Error(`Input value '${wizardStep.canExit}' is neither a boolean nor a function`); + } + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/enable-back-links.directive.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/enable-back-links.directive.spec.ts new file mode 100755 index 0000000000..cc53be2dea --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/enable-back-links.directive.spec.ts @@ -0,0 +1,164 @@ +/** + * Created by marc on 30.06.17. + */ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {ViewChild, Component} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; +import {MovingDirection} from '../util/moving-direction.enum'; +import {By} from '@angular/platform-browser'; +import {WizardModule} from '../wizard.module'; + +@Component({ + selector: 'test-wizard', + template: ` + + + Step 1 + + + Step 2 + + + Step 3 + + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; + + public isValid: any = true; + + public eventLog: Array = new Array(); + + public completionStepExit: (direction: MovingDirection, source: number) => void = this.exitFrom; + + enterInto(direction: MovingDirection, destination: number): void { + this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); + } + + exitFrom(direction: MovingDirection, source: number): void { + this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); + } +} + +describe('EnableBackLinksDirective', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTestFixture.debugElement.queryAll(By.css('wizard-step')).length).toBe(2); + expect(wizardTestFixture.debugElement.queryAll(By.css('wizard-completion-step')).length).toBe(1); + }); + + it('should have correct step title', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTest.wizard.getStepAtIndex(0).title).toBe('Steptitle 1'); + expect(wizardTest.wizard.getStepAtIndex(1).title).toBe('Steptitle 2'); + expect(wizardTest.wizard.getStepAtIndex(2).title).toBe('Completion steptitle 3'); + }); + + it('should enter first step after initialisation', () => { + expect(wizardTest.eventLog).toEqual(['enter Forwards 1']); + }); + + it('should enter completion step after first step', () => { + expect(wizardTest.wizard.currentStepIndex).toBe(0); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2']); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', + 'exit Forwards 2', 'enter Forwards 3']); + }); + + it('should enter completion step after jumping over second optional step', () => { + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.completed).toBe(true); + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3']); + }); + + it('should be able to leave the completion step', () => { + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.canGoToStep(0)).toBe(true); + expect(wizardTest.wizard.canGoToStep(1)).toBe(true); + }); + + + it('should be able to leave the completion step in any direction', () => { + wizardTest.isValid = false; + + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.wizard.currentStep.canExit).toBe(true); + }); + + it('should leave the completion step', () => { + wizardTest.isValid = false; + + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3', 'exit Backwards 3', 'enter Backwards 2']); + }); + + it('should work with changed stepExit value', () => { + wizardTest.isValid = false; + wizardTest.completionStepExit = (direction: MovingDirection, source: number) => { + wizardTest.eventLog.push(`changed exit ${MovingDirection[direction]} ${source}`); + }; + + expect(wizardTest.wizard.completed).toBe(false); + + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.wizard.completed).toBe(true); + + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3', 'changed exit Backwards 3', 'enter Backwards 2']); + expect(wizardTest.wizard.completed).toBe(false); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/enable-back-links.directive.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/enable-back-links.directive.ts new file mode 100755 index 0000000000..5bd620679e --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/enable-back-links.directive.ts @@ -0,0 +1,53 @@ +import {Directive, EventEmitter, Host, OnInit, Output} from '@angular/core'; +import {MovingDirection} from '../util/moving-direction.enum'; +import {WizardCompletionStep} from '../util/wizard-completion-step.inferface'; + +/** + * The `enableBackLinks` directive can be used to allow the user to leave a [[WizardCompletionStep]] after is has been entered. + * + * ### Syntax + * + * ```html + * + * ... + * + * ``` + * + * ### Example + * + * ```html + * + * ... + * + * ``` + * + * @author Marc Arndt + */ +@Directive({ + selector: '[enableBackLinks]' +}) +export class EnableBackLinksDirective implements OnInit { + /** + * This EventEmitter is called when the step is exited. + * The bound method can be used to do cleanup work. + * + * @type {EventEmitter} + */ + @Output() + public stepExit = new EventEmitter(); + + /** + * Constructor + * + * @param completionStep The wizard completion step, which should be exitable + */ + constructor(@Host() private completionStep: WizardCompletionStep) { } + + /** + * Initialization work + */ + ngOnInit(): void { + this.completionStep.canExit = true; + this.completionStep.stepExit = this.stepExit; + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/go-to-step.directive.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/go-to-step.directive.spec.ts new file mode 100755 index 0000000000..5d33197efd --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/go-to-step.directive.spec.ts @@ -0,0 +1,234 @@ +/** + * Created by marc on 09.01.17. + */ +import {GoToStepDirective} from './go-to-step.directive'; +import {Component, ViewChild} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; +import {ComponentFixture, async, TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {WizardModule} from '../wizard.module'; + +@Component({ + selector: 'test-wizard', + template: ` + + + Step 1 + + + + + + Step 2 + + + + + Step 3 + + + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; + + public goToSecondStep = 1; + + public canExit = true; + + public eventLog: Array = new Array(); + + finalizeStep(stepIndex: number): void { + this.eventLog.push(`finalize ${stepIndex}`); + } +} + +describe('GoToStepDirective', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create an instance', () => { + expect(wizardTestFixture.debugElement.query(By.css('wizard-navigation-bar')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(3); + expect(wizardTestFixture.debugElement.query(By.css('wizard-step[title="Steptitle 1"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(3); + expect(wizardTestFixture.debugElement.query(By.css('wizard-step[title="Steptitle 2"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(2); + expect(wizardTestFixture.debugElement.query(By.css('wizard-step[title="Steptitle 3"]')) + .queryAll(By.directive(GoToStepDirective)).length).toBe(1); + + expect(wizardTestFixture.debugElement.queryAll(By.directive(GoToStepDirective)).length).toBe(9); + }); + + it('should move to step correctly', () => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 1"] > button:nth-child(2)')).nativeElement; + const secondStepGoToButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 2"] > button')).nativeElement; + + const wizardSteps = wizardTest.wizard.wizardSteps.toArray(); + + expect(wizardTest.wizard.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(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.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(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.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', () => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 1"] > button:nth-child(3)')).nativeElement; + const thirdStepGoToButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 3"] > button')).nativeElement; + + const wizardSteps = wizardTest.wizard.wizardSteps.toArray(); + + expect(wizardTest.wizard.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(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.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(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.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', () => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 1"] > button:nth-child(1)')).nativeElement; + + const wizardSteps = wizardTest.wizard.wizardSteps.toArray(); + + expect(wizardTest.wizard.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(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.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', () => { + const firstStepGoToButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 1"] > button:nth-child(3)')).nativeElement; + const thirdStepGoToButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 3"] > button')).nativeElement; + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + expect(wizardTest.eventLog).toEqual([]); + + // click button + firstStepGoToButton.click(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.eventLog).toEqual(['finalize 1']); + + // click button + thirdStepGoToButton.click(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + expect(wizardTest.eventLog).toEqual(['finalize 1', 'finalize 3']); + }); + + it('should throw an error when using an invalid goToStep value', () => { + const invalidGoToAttribute = wizardTestFixture.debugElement + .query(By.css('wizard-step[title="Steptitle 2"]')) + .queryAll(By.directive(GoToStepDirective))[1].injector.get(GoToStepDirective) as GoToStepDirective; + + expect(() => invalidGoToAttribute.destinationStep) + .toThrow(new Error(`Input 'goToStep' is neither a WizardStep, StepOffset, number or string`)); + }); + + it('should return correct destination step for correct goToStep values', () => { + const firstGoToAttribute = wizardTestFixture.debugElement + .query(By.css('wizard-navigation-bar')) + .queryAll(By.directive(GoToStepDirective))[0].injector.get(GoToStepDirective) as GoToStepDirective; + + const secondGoToAttribute = wizardTestFixture.debugElement + .query(By.css('wizard-step[title="Steptitle 1"]')) + .queryAll(By.directive(GoToStepDirective))[1].injector.get(GoToStepDirective) as GoToStepDirective; + + const thirdGoToAttribute = wizardTestFixture.debugElement + .query(By.css('wizard-step[title="Steptitle 2"]')) + .queryAll(By.directive(GoToStepDirective))[0].injector.get(GoToStepDirective) as GoToStepDirective; + + const fourthGoToAttribute = wizardTestFixture.debugElement + .query(By.css('wizard-step[title="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', () => { + expect(wizardTest.wizard.currentStepIndex).toBe(0); + + wizardTest.canExit = false; + wizardTestFixture.detectChanges(); + + const secondGoToAttribute = wizardTestFixture.debugElement + .query(By.css('wizard-navigation-bar')) + .queryAll(By.directive(GoToStepDirective))[1].nativeElement; + + secondGoToAttribute.click(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/go-to-step.directive.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/go-to-step.directive.ts new file mode 100755 index 0000000000..4cd5d0967f --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/go-to-step.directive.ts @@ -0,0 +1,102 @@ +/** + * Created by marc on 09.01.17. + */ + +import {Directive, Output, HostListener, EventEmitter, Input, Optional} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; +import {isStepOffset, StepOffset} from '../util/step-offset.interface'; +import {isNumber, isString} from 'util'; +import {WizardStep} from '../util/wizard-step.interface'; + +/** + * The `goToStep` directive can be used to navigate to a given step. + * This step can be defined in one of multiple formats + * + * ### Syntax + * + * With absolute step index: + * + * ```html + * + * ``` + * + * With a wizard step object: + * + * ```html + * + * ``` + * + * With an offset to the defining step + * + * ```html + * + * ``` + * + * @author Marc Arndt + */ +@Directive({ + selector: '[goToStep]' +}) +export class GoToStepDirective { + /** + * An EventEmitter to be called when this directive is used to exit the current step. + * This EventEmitter can be used to do cleanup work + * + * @type {EventEmitter} + */ + @Output() + public finalize = new EventEmitter(); + + /** + * The destination step, to which the wizard should navigate, after the component, having this directive has been activated. + * This destination step can be given either as a [[WizardStep]] containing the step directly, + * 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 + */ + @Input() + private goToStep: WizardStep | StepOffset | number | string; + + /** + * Constructor + * + * @param wizard The wizard, which contains this [[GoToStepDirective]] + * @param wizardStep The wizard step, which contains this [[GoToStepDirective]] + */ + constructor(private wizard: WizardComponent, @Optional() private wizardStep: WizardStep) { } + + /** + * Returns the destination step of this directive as an absolute step index inside the wizard + * + * @returns {number} The index of the destination step + * @throws If `goToStep` is of an unknown type an `Error` is thrown + */ + get destinationStep(): number { + let destinationStep: number; + + if (isNumber(this.goToStep)) { + destinationStep = this.goToStep as number; + } else if (isString(this.goToStep)) { + destinationStep = parseInt(this.goToStep as string, 10); + } else if (isStepOffset(this.goToStep) && this.wizardStep !== null) { + destinationStep = this.wizard.getIndexOfStep(this.wizardStep) + this.goToStep.stepOffset; + } else if (this.goToStep instanceof WizardStep) { + destinationStep = this.wizard.getIndexOfStep(this.goToStep); + } else { + throw new Error(`Input 'goToStep' is neither a WizardStep, StepOffset, number or string`); + } + + return destinationStep; + } + + /** + * 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(): void { + if (this.wizard.canGoToStep(this.destinationStep)) { + this.finalize.emit(); + + this.wizard.goToStep(this.destinationStep); + } + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/next-step.directive.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/next-step.directive.spec.ts new file mode 100755 index 0000000000..efdcca3fb4 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/next-step.directive.spec.ts @@ -0,0 +1,97 @@ +import { NextStepDirective } from './next-step.directive'; +import {ViewChild, Component} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; +import {ComponentFixture, async, TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {WizardModule} from '../wizard.module'; + +@Component({ + selector: 'test-wizard', + template: ` + + + Step 1 + + + + Step 2 + + + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; + + public eventLog: Array = new Array(); + + finalizeStep(stepIndex: number): void { + this.eventLog.push(`finalize ${stepIndex}`); + } +} + +describe('NextStepDirective', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create an instance', () => { + expect(wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 1"] > button[nextStep]'))).toBeTruthy(); + expect(wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 2"] > button[nextStep]'))).toBeTruthy(); + expect(wizardTestFixture.debugElement.queryAll( + By.css('wizard-step > button[nextStep]')).length).toBe(2); + }); + + it('should move correctly to the next step', () => { + const firstStepButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 1"] > button[nextStep]')).nativeElement; + const secondStepButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 2"] > button[nextStep]')).nativeElement; + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + + // go to second step + firstStepButton.click(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + // don't go to third step because it doesn't exist + secondStepButton.click(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + }); + + it('should move call finalize correctly when going the next step', () => { + const firstStepButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 1"] > button[nextStep]')).nativeElement; + const secondStepButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 2"] > button[nextStep]')).nativeElement; + + expect(wizardTest.eventLog).toEqual([]); + + // go to second step + firstStepButton.click(); + + expect(wizardTest.eventLog).toEqual(['finalize 1']); + + // don't go to third step because it doesn't exist + secondStepButton.click(); + + expect(wizardTest.eventLog).toEqual(['finalize 1']); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/next-step.directive.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/next-step.directive.ts new file mode 100755 index 0000000000..acda91c0ec --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/next-step.directive.ts @@ -0,0 +1,46 @@ +import {Directive, Output, HostListener, EventEmitter} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; + +/** + * The `nextStep` directive can be used to navigate to the next step. + * + * ### Syntax + * + * ```html + * + * ``` + * + * @author Marc Arndt + */ +@Directive({ + selector: '[nextStep]' +}) +export class NextStepDirective { + /** + * An EventEmitter to be called when this directive is used to exit the current step. + * This EventEmitter can be used to do cleanup work + * + * @type {EventEmitter} + */ + @Output() + public finalize = new EventEmitter(); + + /** + * Constructor + * + * @param wizard The [[WizardComponent]], this directive is used inside + */ + constructor(private wizard: WizardComponent) { } + + /** + * Listener method for `click` events on the component with this directive. + * After this method is called the wizard will try to transition to the next step + */ + @HostListener('click', ['$event']) onClick(): void { + if (this.wizard.canGoToNextStep()) { + this.finalize.emit(); + + this.wizard.goToNextStep(); + } + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/optional-step.directive.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/optional-step.directive.spec.ts new file mode 100755 index 0000000000..95c0f51d5b --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/optional-step.directive.spec.ts @@ -0,0 +1,56 @@ +import { OptionalStepDirective } from './optional-step.directive'; +import {Component, ViewChild} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; +import {ComponentFixture, TestBed, async} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {WizardModule} from '../wizard.module'; + +@Component({ + selector: 'test-wizard', + template: ` + + + Step 1 + + + Step 2 + + + Step 3 + + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; +} + +describe('OptionalStepDirective', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create an instance', () => { + expect(wizardTestFixture.debugElement.query(By.css('wizard-step[optionalStep]'))).toBeTruthy(); + expect(wizardTestFixture.debugElement.queryAll(By.css('wizard-step[optionalStep]')).length).toBe(1); + }); + + it('should set optional correctly', () => { + expect(wizardTest.wizard.getStepAtIndex(0).optional).toBe(false); + expect(wizardTest.wizard.getStepAtIndex(1).optional).toBe(true); + expect(wizardTest.wizard.getStepAtIndex(2).optional).toBe(false); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/optional-step.directive.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/optional-step.directive.ts new file mode 100755 index 0000000000..60f96f2562 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/optional-step.directive.ts @@ -0,0 +1,43 @@ +import {Directive, Host, OnInit} from '@angular/core'; +import {WizardStep} from '../util/wizard-step.interface'; + +/** + * The `optionalStep` directive can be used to define an optional `wizard-step`. + * An optional `wizard-step` is a [[WizardStep]] that doesn't need to be completed to transition to later wizard steps. + * + * ### Syntax + * + * ```html + * + * ... + * + * ``` + * + * ### Example + * + * ```html + * + * ... + * + * ``` + * + * @author Marc Arndt + */ +@Directive({ + selector: '[optionalStep]' +}) +export class OptionalStepDirective implements OnInit { + /** + * Constructor + * + * @param wizardStep The wizard step, which contains this [[OptionalStepDirective]] + */ + constructor(@Host() private wizardStep: WizardStep) { } + + /** + * Initialization work + */ + ngOnInit(): void { + this.wizardStep.optional = true; + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/previous-step.directive.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/previous-step.directive.spec.ts new file mode 100755 index 0000000000..78c9061c5d --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/previous-step.directive.spec.ts @@ -0,0 +1,78 @@ +import { PreviousStepDirective } from './previous-step.directive'; +import {ViewChild, Component} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; +import {TestBed, async, ComponentFixture} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {WizardModule} from '../wizard.module'; + +@Component({ + selector: 'test-wizard', + template: ` + + + Step 1 + + + + Step 2 + + + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; +} + +describe('PreviousStepDirective', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create an instance', () => { + expect(wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 1"] > button[previousStep]'))).toBeTruthy(); + expect(wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 2"] > button[previousStep]'))).toBeTruthy(); + expect(wizardTestFixture.debugElement.queryAll( + By.css('wizard-step > button[previousStep]')).length).toBe(2); + }); + + it('should move correctly to the previous step', () => { + const firstStepButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 1"] > button[previousStep]')).nativeElement; + const secondStepButton = wizardTestFixture.debugElement.query( + By.css('wizard-step[title="Steptitle 2"] > button[previousStep]')).nativeElement; + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + + // don't go to zero (-1) step, because it doesn't exist + firstStepButton.click(); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + + // move to second step to test the previousStep directive + wizardTest.wizard.goToStep(1); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + // go back to first step + secondStepButton.click(); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/previous-step.directive.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/previous-step.directive.ts new file mode 100755 index 0000000000..041a289b10 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/previous-step.directive.ts @@ -0,0 +1,36 @@ +import {Directive, HostListener} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; + +/** + * The `previousStep` directive can be used to navigate to the previous step. + * Compared to the [[NextStepDirective]] it's important to note, that this directive doesn't contain a `finalize` output method. + * + * ### Syntax + * + * ```html + * + * ``` + * + * @author Marc Arndt + */ +@Directive({ + selector: '[previousStep]' +}) +export class PreviousStepDirective { + /** + * Constructor + * + * @param wizard The [[WizardComponent]], this directive is used inside + */ + constructor(private wizard: WizardComponent) { } + + /** + * Listener method for `click` events on the component with this directive. + * After this method is called the wizard will try to transition to the previous step + */ + @HostListener('click', ['$event']) onClick(): void { + if (this.wizard.canGoToPreviousStep()) { + this.wizard.goToPreviousStep(); + } + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/step-number.directive.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/step-number.directive.spec.ts new file mode 100644 index 0000000000..19ad1a5f6c --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/step-number.directive.spec.ts @@ -0,0 +1,8 @@ +import { StepNumberDirective } from './step-number.directive'; + +describe('StepNumberDirective', () => { + it('should create an instance', () => { + const directive = new StepNumberDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/step-number.directive.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/step-number.directive.ts new file mode 100644 index 0000000000..2c8d314a79 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/step-number.directive.ts @@ -0,0 +1,21 @@ +import { Directive, Host, AfterContentInit } from '@angular/core'; +import { WizardComponent } from 'app/controls/form-wizard/components/wizard.component'; + +@Directive({ + selector: '[StepNumber]' +}) +export class StepNumberDirective implements AfterContentInit { + constructor(@Host() private wizard: WizardComponent) {} + ngAfterContentInit(): void { + this.numberSteps(); + this.wizard.wizardSteps.changes.subscribe(() => { + this.numberSteps(); + }); + } + + numberSteps() { + this.wizard.wizardSteps.forEach((x, i) => { + x.navigationSymbol = `${i + 1}`; + }); + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-completion-step.directive.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-completion-step.directive.spec.ts new file mode 100755 index 0000000000..8b9d408fa3 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-completion-step.directive.spec.ts @@ -0,0 +1,145 @@ +/** + * Created by marc on 20.05.17. + */ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {ViewChild, Component} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; +import {MovingDirection} from '../util/moving-direction.enum'; +import {By} from '@angular/platform-browser'; +import {WizardModule} from '../wizard.module'; +import {WizardCompletionStepDirective} from './wizard-completion-step.directive'; + +@Component({ + selector: 'test-wizard', + template: ` + + + Step 1 + + + Step 2 + +
+ Step 3 +
+
+ ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; + + public isValid: any = true; + + public eventLog: Array = new Array(); + + enterInto(direction: MovingDirection, destination: number): void { + this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); + } + + exitFrom(direction: MovingDirection, source: number): void { + this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); + } +} + +describe('WizardCompletionStepDirective', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTestFixture.debugElement.queryAll(By.css('wizard-step')).length).toBe(2); + expect(wizardTestFixture.debugElement.queryAll(By.directive(WizardCompletionStepDirective)).length).toBe(1); + }); + + it('should have correct step title', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTest.wizard.getStepAtIndex(0).title).toBe('Steptitle 1'); + expect(wizardTest.wizard.getStepAtIndex(1).title).toBe('Steptitle 2'); + expect(wizardTest.wizard.getStepAtIndex(2).title).toBe('Completion steptitle 3'); + }); + + it('should enter first step after initialisation', () => { + expect(wizardTest.eventLog).toEqual(['enter Forwards 1']); + }); + + it('should enter completion step after first step', () => { + expect(wizardTest.wizard.currentStepIndex).toBe(0); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2']); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', + 'exit Forwards 2', 'enter Forwards 3']); + }); + + it('should enter completion step after jumping over second optional step', () => { + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3']); + }); + + it('should set the wizard as completed after entering the completion step', () => { + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.completed).toBe(true); + }); + + it('should be unable to leave the completion step', () => { + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.canGoToStep(0)).toBe(false); + expect(wizardTest.wizard.canGoToStep(1)).toBe(false); + }); + + + it('should not be able to leave the completion step in any direction', () => { + wizardTest.isValid = false; + + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.wizard.currentStep.canExit).toBe(false); + }); + + it('should not leave the completion step if it can\'t be exited', () => { + wizardTest.isValid = false; + + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3', 'enter Stay 3']); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-completion-step.directive.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-completion-step.directive.ts new file mode 100755 index 0000000000..83073414b0 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-completion-step.directive.ts @@ -0,0 +1,139 @@ +import {ContentChild, Directive, EventEmitter, forwardRef, HostBinding, Inject, Input, Output} from '@angular/core'; +import {MovingDirection} from '../util/moving-direction.enum'; +import {WizardComponent} from '../components/wizard.component'; +import {WizardStep} from '../util/wizard-step.interface'; +import {WizardStepTitleDirective} from './wizard-step-title.directive'; +import {WizardCompletionStep} from '../util/wizard-completion-step.inferface'; + +/** + * The `wizardCompletionStep` directive can be used to define a completion/success step at the end of your wizard + * After a [[WizardCompletionStep]] has been entered, it has the characteristic that the user is blocked from + * leaving it again to a previous step. + * In addition entering a [[WizardCompletionStep]] automatically sets the `wizard` amd all steps inside the `wizard` + * as completed. + * + * ### Syntax + * + * ```html + *
+ * ... + *
+ * ``` + * + * ### Example + * + * ```html + *
+ * ... + *
+ * ``` + * + * With a navigation symbol from the `font-awesome` font: + * + * ```html + *
+ * ... + *
+ * ``` + * + * @author Marc Arndt + */ +@Directive({ + selector: '[wizardCompletionStep]', + providers: [ + { provide: WizardStep, useExisting: forwardRef(() => WizardCompletionStepDirective) }, + { provide: WizardCompletionStep, useExisting: forwardRef(() => WizardCompletionStepDirective) } + ] +}) +export class WizardCompletionStepDirective extends WizardCompletionStep { + /** + * @inheritDoc + */ + @ContentChild(WizardStepTitleDirective) + public titleTemplate: WizardStepTitleDirective; + + /** + * @inheritDoc + */ + @Input() + public title: string; + + /** + * @inheritDoc + */ + @Input() + public navigationSymbol = ''; + + /** + * @inheritDoc + */ + @Input() + public navigationSymbolFontFamily: string; + + /** + * @inheritDoc + */ + @Output() + public stepEnter = new EventEmitter(); + + /** + * @inheritDoc + */ + public stepExit = new EventEmitter(); + + /** + * @inheritDoc + */ + @HostBinding('hidden') + public get hidden(): boolean { + return !this.selected; + } + + /** + * @inheritDoc + */ + public completed: false; + + /** + * @inheritDoc + */ + public selected = false; + + /** + * @inheritDoc + */ + public optional = false; + + /** + * @inheritDoc + */ + public canExit: ((direction: MovingDirection) => boolean) | boolean = false; + + /** + * Constructor + * @param wizard The [[WizardComponent]], this completion step is contained inside + */ + /* istanbul ignore next */ + constructor(@Inject(forwardRef(() => WizardComponent)) private wizard: WizardComponent) { + super(); + } + + /** + * @inheritDoc + */ + enter(direction: MovingDirection): void { + this.wizard.completed = true; + this.stepEnter.emit(direction); + } + + /** + * @inheritDoc + */ + exit(direction: MovingDirection): void { + this.wizard.completed = false; + this.stepExit.emit(direction); + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step-title.directive.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step-title.directive.spec.ts new file mode 100755 index 0000000000..49e76d1978 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step-title.directive.spec.ts @@ -0,0 +1,60 @@ +/** + * Created by marc on 02.06.17. + */ +import {ViewChild, Component} from '@angular/core'; +import {TestBed, async, ComponentFixture} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +import {WizardComponent} from '../components/wizard.component'; +import {WizardModule} from '../wizard.module'; + + +@Component({ + selector: 'test-wizard', + template: ` + + + + Steptitle 1 + + Step 1 + + + + Steptitle 2 + + Step 2 + + + ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; +} + +describe('PreviousStepDirective', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create an instance', () => { + let navigationLinks = wizardTestFixture.debugElement.queryAll(By.css('wizard-navigation-bar ul li a')); + + expect(navigationLinks.length).toBe(2); + expect(navigationLinks[0].nativeElement.innerText).toBe('STEPTITLE 1'); + expect(navigationLinks[1].nativeElement.innerText).toBe('STEPTITLE 2'); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step-title.directive.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step-title.directive.ts new file mode 100755 index 0000000000..1e0b9cae04 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step-title.directive.ts @@ -0,0 +1,31 @@ +/** + * Created by marc on 01.06.17. + */ +import {Directive, TemplateRef} from '@angular/core'; + +/** + * The `wizardStepTitle` directive can be used as an alternative to the `title` input of a [[WizardStep]] + * to define the content of a step title inside the navigation bar. + * This title can be freely created and can contain more than only plain text + * + * ### Syntax + * + * ```html + * + * ... + * + * ``` + * + * @author Marc Arndt + */ +@Directive({ + selector: 'ng-template[wizardStepTitle]' +}) +export class WizardStepTitleDirective { + /** + * Constructor + * + * @param templateRef A reference to the content of the `ng-template` that contains this [[WizardStepTitleDirective]] + */ + constructor(public templateRef: TemplateRef) { } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step.directive.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step.directive.spec.ts new file mode 100755 index 0000000000..314c4a0bb7 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step.directive.spec.ts @@ -0,0 +1,283 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {ViewChild, Component, Host, EventEmitter, forwardRef} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; +import {MovingDirection} from '../util/moving-direction.enum'; +import {By} from '@angular/platform-browser'; +import {WizardModule} from '../wizard.module'; +import {WizardStepDirective} from './wizard-step.directive'; + +@Component({ + selector: 'test-wizard', + template: ` + +
+ Step 1 +
+ + Step 2 + +
+ Step 3 +
+
+ ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; + + @ViewChild(forwardRef(() => WizardStepTestComponent)) + public wizardStepTestComponent; + + public eventLog: Array = new Array(); + + enterInto(direction: MovingDirection, destination: number): void { + this.eventLog.push(`enter ${MovingDirection[direction]} ${destination}`); + } + + exitFrom(direction: MovingDirection, source: number): void { + this.eventLog.push(`exit ${MovingDirection[direction]} ${source}`); + } +} + +@Component({ + selector: 'test-wizard-step', + template: ` + Step 2 + ` +}) +class WizardStepTestComponent { + public set isValid(valid: any) { + this.wizardStep.canExit = valid; + } + + constructor(@Host() private wizardStep: WizardStepDirective, wizard: WizardTestComponent) { + wizardStep.title = 'Steptitle 2'; + wizardStep.stepEnter.emit = direction => wizard.enterInto(direction, 2); + wizardStep.stepExit.emit = direction => wizard.exitFrom(direction, 2); + } +} + + +describe('WizardStepDirective', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent, WizardStepTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTestFixture.debugElement.queryAll(By.directive(WizardStepDirective)).length).toBe(3); + }); + + it('should have correct step title', () => { + expect(wizardTest).toBeTruthy(); + expect(wizardTest.wizard.getStepAtIndex(0).title).toBe('Steptitle 1'); + expect(wizardTest.wizard.getStepAtIndex(1).title).toBe('Steptitle 2'); + expect(wizardTest.wizard.getStepAtIndex(2).title).toBe('Steptitle 3'); + }); + + it('should enter first step after initialisation', () => { + expect(wizardTest.eventLog).toEqual(['enter Forwards 1']); + }); + + it('should enter second step after first step', () => { + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2']); + }); + + it('should enter first step after exiting second step', () => { + wizardTest.wizard.goToNextStep(); + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Backwards 2', 'enter Backwards 1']); + }); + + it('should enter third step after jumping over second optional step', () => { + wizardTest.wizard.goToStep(2); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3']); + }); + + it('should enter first step after jumping over second optional step two times', () => { + wizardTest.wizard.goToStep(2); + wizardTest.wizard.goToStep(0); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3', 'exit Backwards 3', 'enter Backwards 1']); + }); + + it('should enter second step after jumping over second optional step and the going back once', () => { + wizardTest.wizard.goToStep(2); + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 3', 'exit Backwards 3', 'enter Backwards 2']); + }); + + it('should stay at first step correctly', () => { + wizardTest.wizard.goToStep(0); + wizardTestFixture.detectChanges(); + + expect(wizardTest.eventLog).toEqual(['enter Forwards 1', 'exit Stay 1', 'enter Stay 1']); + }); + + it('should not be able to leave the second step in any direction', () => { + wizardTest.wizardStepTestComponent.isValid = false; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.wizard.currentStep.canExit).toBe(false); + }); + + it('should not be able to leave the second step in forwards direction', () => { + wizardTest.wizardStepTestComponent.isValid = direction => direction !== MovingDirection.Forwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Forwards)).toBe(false); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Backwards)).toBe(true); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Stay)).toBe(true); + }); + + it('should not be able to leave the second step in backwards direction', () => { + wizardTest.wizardStepTestComponent.isValid = direction => direction !== MovingDirection.Backwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Forwards)).toBe(true); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Backwards)).toBe(false); + expect(wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Stay)).toBe(true); + }); + + it('should throw error when method "canExit" is malformed', () => { + wizardTest.wizardStepTestComponent.isValid = 'String'; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(() => wizardTest.wizard.canExitStep(wizardTest.wizard.currentStep, MovingDirection.Forwards)) + .toThrow(new Error(`Input value 'String' is neither a boolean nor a function`)); + }); + + it('should not leave the second step in forward direction if it can\'t be exited', () => { + wizardTest.wizardStepTestComponent.isValid = false; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Stay 2', 'enter Stay 2']); + }); + + it('should not leave the second step in backward direction if it can\'t be exited', () => { + wizardTest.wizardStepTestComponent.isValid = false; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Stay 2', 'enter Stay 2']); + }); + + it('should not leave the second step in forward direction if it can\'t be exited in this direction', () => { + wizardTest.wizardStepTestComponent.isValid = direction => direction === MovingDirection.Backwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Stay 2', 'enter Stay 2']); + }); + + it('should not leave the second step in backward direction if it can\'t be exited in this direction', () => { + wizardTest.wizardStepTestComponent.isValid = direction => direction === MovingDirection.Forwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Stay 2', 'enter Stay 2']); + }); + + it('should leave the second step in forward direction if it can be exited in this direction', () => { + wizardTest.wizardStepTestComponent.isValid = direction => direction === MovingDirection.Forwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(2); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Forwards 2', 'enter Forwards 3']); + }); + + it('should leave the second step in backward direction if it can be exited in this direction', () => { + wizardTest.wizardStepTestComponent.isValid = direction => direction === MovingDirection.Backwards; + + wizardTest.wizard.goToNextStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(1); + + wizardTest.wizard.goToPreviousStep(); + wizardTestFixture.detectChanges(); + + expect(wizardTest.wizard.currentStepIndex).toBe(0); + expect(wizardTest.eventLog) + .toEqual(['enter Forwards 1', 'exit Forwards 1', 'enter Forwards 2', 'exit Backwards 2', 'enter Backwards 1']); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step.directive.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step.directive.ts new file mode 100755 index 0000000000..e57e720f00 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/directives/wizard-step.directive.ts @@ -0,0 +1,146 @@ +import {ContentChild, Directive, EventEmitter, forwardRef, HostBinding, Input, Output} from '@angular/core'; +import {MovingDirection} from '../util/moving-direction.enum'; +import {WizardStep} from '../util/wizard-step.interface'; +import {WizardStepTitleDirective} from './wizard-step-title.directive'; + +/** + * The `wizardStep` directive can be used to define a normal step inside a wizard. + * + * ### Syntax + * + * With `title` input: + * + * ```html + *
+ * ... + *
+ * ``` + * + * With `wizardStepTitle` directive: + * + * ```html + *
+ * + * step title + * + * ... + *
+ * ``` + * + * ### Example + * + * With `title` input: + * + * ```html + *
+ * ... + *
+ * ``` + * + * With `wizardStepTitle` directive: + * + * ```html + *
+ * + * Address information + * + *
+ * ``` + * + * @author Marc Arndt + */ +@Directive({ + selector: '[wizardStep]', + providers: [ + { provide: WizardStep, useExisting: forwardRef(() => WizardStepDirective) } + ] +}) +export class WizardStepDirective extends WizardStep { + /** + * @inheritDoc + */ + @ContentChild(WizardStepTitleDirective) + public titleTemplate: WizardStepTitleDirective; + + /** + * @inheritDoc + */ + @Input() + public title: string; + + /** + * @inheritDoc + */ + @Input() + public navigationSymbol = ''; + + /** + * @inheritDoc + */ + @Input() + public navigationSymbolFontFamily: string; + + /** + * @inheritDoc + */ + @Input() + public canExit: ((direction: MovingDirection) => boolean) | boolean = true; + + /** + * @inheritDoc + */ + @Output() + public stepEnter = new EventEmitter(); + + /** + * @inheritDoc + */ + @Output() + public stepExit = new EventEmitter(); + + /** + * @inheritDoc + */ + @HostBinding('hidden') + public get hidden(): boolean { + return !this.selected; + } + + /** + * @inheritDoc + */ + public completed = false; + + /** + * @inheritDoc + */ + public selected = false; + + /** + * @inheritDoc + */ + public optional = false; + + /** + * Constructor + */ + constructor() { + super(); + } + + /** + * @inheritDoc + */ + enter(direction: MovingDirection): void { + this.stepEnter.emit(direction); + } + + /** + * @inheritDoc + */ + exit(direction: MovingDirection): void { + this.stepExit.emit(direction); + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/moving-direction.enum.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/moving-direction.enum.ts new file mode 100755 index 0000000000..6308cf1235 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/moving-direction.enum.ts @@ -0,0 +1,25 @@ +/** + * The direction in which a step transition was made + * + * @author Marc Arndt + */ + +/** + * This enum contains the different possible moving directions in which a wizard can be traversed + * + * @author Marc Arndt + */ +export enum MovingDirection { + /** + * A forward step transition + */ + Forwards, + /** + * A backward step transition + */ + Backwards, + /** + * No step transition was done + */ + Stay +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/step-offset.interface.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/step-offset.interface.ts new file mode 100755 index 0000000000..b8545a183a --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/step-offset.interface.ts @@ -0,0 +1,24 @@ +/** + * An offset between two steps. + * This offset can be either positive or negative. + * A positive offset means, that the offset step is after the other step, while a negative offset means, + * that the offset step is ahead of the other step. + * + * @author Marc Arndt + */ +export interface StepOffset { + /** + * The offset to the destination step + */ + stepOffset: number +} + +/** + * Checks wether the given `value` implements the interface [[StepOffset]]. + * + * @param value The value to be checked + * @returns {boolean} True if the given value implements [[StepOffset]] and false otherwise + */ +export function isStepOffset(value: any): value is StepOffset { + return value.hasOwnProperty('stepOffset'); +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/wizard-completion-step.inferface.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/wizard-completion-step.inferface.ts new file mode 100755 index 0000000000..70696b6508 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/wizard-completion-step.inferface.ts @@ -0,0 +1,15 @@ +import {WizardStep} from './wizard-step.interface'; + +/** + * Basic functionality every wizard completion step needs to provide + * + * @author Marc Arndt + */ +export abstract class WizardCompletionStep extends WizardStep { + /** + * Constructor + */ + constructor() { + super(); + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/wizard-step.interface.spec.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/wizard-step.interface.spec.ts new file mode 100755 index 0000000000..295ea0b458 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/wizard-step.interface.spec.ts @@ -0,0 +1,106 @@ +/** + * Created by marc on 29.06.17. + */ +import {Component, ViewChild} from '@angular/core'; +import {WizardComponent} from '../components/wizard.component'; +import {ComponentFixture, async, TestBed} from '@angular/core/testing'; +import {WizardStepComponent} from '../components/wizard-step.component'; +import {WizardCompletionStepComponent} from '../components/wizard-completion-step.component'; +import {WizardModule} from '../wizard.module'; +import {WizardStep} from './wizard-step.interface'; +import {WizardCompletionStepDirective} from '../directives/wizard-completion-step.directive'; +import {WizardStepDirective} from '../directives/wizard-step.directive'; + +@Component({ + selector: 'test-wizard', + template: ` + + + Step 1 + + + Step 2 + + + + Steptitle 3 + + Step 3 + +
+ Step 4 +
+ + Step 5 + +
+ Step 6 +
+
+ ` +}) +class WizardTestComponent { + @ViewChild(WizardComponent) + public wizard: WizardComponent; + + @ViewChild('step1') + public step1: WizardStepComponent; + + @ViewChild('step2') + public step2: WizardStepComponent; + + @ViewChild('step3') + public step3: WizardStepComponent; + + @ViewChild('step4', {read: WizardStepDirective}) + public step4: WizardStepDirective; + + @ViewChild('step5') + public step5: WizardCompletionStepComponent; + + @ViewChild('step6', {read: WizardCompletionStepDirective}) + public step6: WizardCompletionStepDirective; +} + +describe('WizardStep', () => { + let wizardTest: WizardTestComponent; + let wizardTestFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [WizardTestComponent], + imports: [WizardModule] + }).compileComponents(); + })); + + beforeEach(() => { + wizardTestFixture = TestBed.createComponent(WizardTestComponent); + wizardTest = wizardTestFixture.componentInstance; + wizardTestFixture.detectChanges(); + }); + + it('should create an instance', () => { + expect(wizardTest.step1).toBeDefined(); + expect(wizardTest.step2).toBeDefined(); + expect(wizardTest.step3).toBeDefined(); + expect(wizardTest.step4).toBeDefined(); + expect(wizardTest.step5).toBeDefined(); + expect(wizardTest.step6).toBeDefined(); + + expect(wizardTest.wizard.wizardSteps.length).toBe(6); + }); + + it('should be a WizardStep', () => { + expect(wizardTest.step1 instanceof WizardStep).toBe(true, 'Step 1 couldn\'t be identified as a WizardStep'); + expect(wizardTest.step2 instanceof WizardStep).toBe(true, 'Step 2 couldn\'t be identified as a WizardStep'); + expect(wizardTest.step3 instanceof WizardStep).toBe(true, 'Step 3 couldn\'t be identified as a WizardStep'); + expect(wizardTest.step4 instanceof WizardStep).toBe(true, 'Step 4 couldn\'t be identified as a WizardStep'); + expect(wizardTest.step5 instanceof WizardStep).toBe(true, 'Step 5 couldn\'t be identified as a WizardStep'); + expect(wizardTest.step6 instanceof WizardStep).toBe(true, 'Step 6 couldn\'t be identified as a WizardStep'); + }); + + it('should not be a WizardStep', () => { + expect({stepOffset: 1} instanceof WizardStep).toBe(false); + expect({title: 'Test title'} instanceof WizardStep).toBe(false); + }); +}); diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/wizard-step.interface.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/wizard-step.interface.ts new file mode 100755 index 0000000000..137343c00f --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/util/wizard-step.interface.ts @@ -0,0 +1,93 @@ +import {MovingDirection} from './moving-direction.enum'; +import {WizardStepTitleDirective} from '../directives/wizard-step-title.directive'; +import {EventEmitter} from '@angular/core'; + +/** + * Basic functionality every type of wizard step needs to provide + * + * @author Marc Arndt + */ +export abstract class WizardStep { + /** + * A title property, which contains the title of the step. + * This title is then shown inside the navigation bar. + * Compared to `title` this property can contain any html content and not only plain text + */ + titleTemplate: WizardStepTitleDirective; + + /** + * A title property, which contains the title of the step. + * This title is only shown inside the navigation bar, if `titleTemplate` is not defined or null. + */ + title: string; + + /** + * A symbol property, which contains an optional symbol for the step inside the navigation bar. + * If no navigation symbol is specified, an empty string should be used + */ + navigationSymbol: string; + + /** + * The font family belonging to the `navigationSymbol`. + * If no font family is specified, null should be used + */ + navigationSymbolFontFamily: string; + + /** + * A boolean describing if the wizard step has been completed + */ + completed: boolean; + + /** + * A boolean describing if the wizard step is currently selected + */ + selected: boolean; + + /** + * A boolean describing if the wizard step is an optional step + */ + optional: boolean; + + /** + * A function, taking a [[MovingDirection]], or boolean returning true, if the step can be exited and false otherwise. + */ + canExit: ((direction: MovingDirection) => boolean) | boolean; + + /** + * This EventEmitter is called when the step is entered. + * The bound method should be used to do initialization work. + * + * @type {EventEmitter} + */ + public stepEnter: EventEmitter; + + /** + * This EventEmitter is called when the step is exited. + * The bound method can be used to do cleanup work. + * + * @type {EventEmitter} + */ + public stepExit: EventEmitter; + + /** + * Returns if this wizard step should be visible to the user. + * If the step should be visible to the user false is returned, otherwise true + * + * @returns {boolean} + */ + abstract get hidden(): boolean; + + /** + * A function called when the step is entered + * + * @param direction The direction in which the step is entered + */ + abstract enter(direction: MovingDirection): void; + + /** + * A function called when the step is exited + * + * @param direction The direction in which the step is exited + */ + abstract exit(direction: MovingDirection): void; +} diff --git a/AzureFunctions.AngularClient/src/app/controls/form-wizard/wizard.module.ts b/AzureFunctions.AngularClient/src/app/controls/form-wizard/wizard.module.ts new file mode 100755 index 0000000000..f6d7fa58c5 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/form-wizard/wizard.module.ts @@ -0,0 +1,85 @@ +import { CommonModule } from '@angular/common'; +import { NgModule, ModuleWithProviders } from '@angular/core'; + +import { WizardComponent } from './components/wizard.component'; +import { WizardNavigationBarComponent } from './components/wizard-navigation-bar.component'; +import { WizardStepComponent } from './components/wizard-step.component'; +import { WizardCompletionStepComponent } from './components/wizard-completion-step.component'; + +import { NextStepDirective } from './directives/next-step.directive'; +import { PreviousStepDirective } from './directives/previous-step.directive'; +import { OptionalStepDirective } from './directives/optional-step.directive'; +import { GoToStepDirective } from './directives/go-to-step.directive'; +import { WizardStepTitleDirective } from './directives/wizard-step-title.directive'; +import { EnableBackLinksDirective } from './directives/enable-back-links.directive'; +import { WizardStepDirective } from './directives/wizard-step.directive'; +import { WizardCompletionStepDirective } from './directives/wizard-completion-step.directive'; +import { StepNumberDirective } from './directives/step-number.directive'; + +/** + * The module defining all the content inside `ng2-archwizard` + * + * +MIT License + +Copyright (c) 2016 madoar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +* + * @author Marc Arndt + */ +@NgModule({ + declarations: [ + WizardComponent, + WizardStepComponent, + WizardNavigationBarComponent, + WizardCompletionStepComponent, + GoToStepDirective, + NextStepDirective, + PreviousStepDirective, + OptionalStepDirective, + WizardStepTitleDirective, + EnableBackLinksDirective, + WizardStepDirective, + WizardCompletionStepDirective, + StepNumberDirective + ], + imports: [CommonModule], + exports: [ + WizardComponent, + WizardStepComponent, + WizardNavigationBarComponent, + WizardCompletionStepComponent, + GoToStepDirective, + NextStepDirective, + PreviousStepDirective, + OptionalStepDirective, + WizardStepTitleDirective, + EnableBackLinksDirective, + WizardStepDirective, + WizardCompletionStepDirective, + StepNumberDirective + ] +}) +export class WizardModule { + /* istanbul ignore next */ + static forRoot(): ModuleWithProviders { + return { ngModule: WizardModule, providers: [] }; + } +} diff --git a/AzureFunctions.AngularClient/src/app/controls/tbl/tbl.component.ts b/AzureFunctions.AngularClient/src/app/controls/tbl/tbl.component.ts index a9f9d1aede..078f5e7459 100644 --- a/AzureFunctions.AngularClient/src/app/controls/tbl/tbl.component.ts +++ b/AzureFunctions.AngularClient/src/app/controls/tbl/tbl.component.ts @@ -26,7 +26,6 @@ export class TblComponent implements OnInit, OnChanges, AfterContentChecked { @Input() name: string | null; @Input() tblClass = 'tbl'; @Input() items: TableItem[]; - // groupColName will be what col items are sorted by within individual groups // if no grouping is done in the table it is null @Input() groupColName: string | null; @@ -353,7 +352,7 @@ export class TblComponent implements OnInit, OnChanges, AfterContentChecked { return this._origItems; } - groupItems(name: string) { + groupItems(name: string, sortDir: 'asc' | 'desc' = 'asc') { if (!this.groupColName) { throw Error('No group name was specified for this table component'); @@ -362,7 +361,7 @@ export class TblComponent implements OnInit, OnChanges, AfterContentChecked { this._resetRovingTabindex(); this.groupedBy = name; - + const sortMult = sortDir === 'asc' ? 1 : -1; if (name === 'none') { this.items = this.items.filter(item => item.type !== 'group'); } else { @@ -379,9 +378,9 @@ export class TblComponent implements OnInit, OnChanges, AfterContentChecked { aCol = typeof aCol === 'string' ? aCol : aCol.toString(); bCol = typeof bCol === 'string' ? bCol : bCol.toString(); - return bCol.localeCompare(aCol); + return bCol.localeCompare(aCol) * sortMult; }); - + // determine uniqueGroup values const uniqueDictGroups = {}; newItems.forEach(item => { @@ -405,7 +404,8 @@ export class TblComponent implements OnInit, OnChanges, AfterContentChecked { // reverse newItems to be all groups, sorted, followed by all rows, sorted then push onto items in correct order this.items = []; newItems.reverse(); - uniqueGroups.sort().forEach(group => { + + uniqueGroups.sort((a, b) => a.localeCompare(b) * sortMult).forEach(group => { newItems.forEach(item => { if (item.type === 'group' && item[this.groupColName] === group) { this.items.push(item); diff --git a/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.html b/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.html index ab2f62cd96..8f275f27c8 100644 --- a/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.html +++ b/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.html @@ -1,12 +1,11 @@ -