diff --git a/.travis.yml b/.travis.yml index dbcbd7a9f..6d85e41e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,7 @@ language: node_js +node_js: + - '6' + cache: directories: - node_modules @@ -6,9 +9,6 @@ cache: notifications: email: false -node_js: - - '6' - env: global: - ENCRYPTION_LABEL: "a8c5e363dc84" @@ -24,9 +24,10 @@ install: true script: - sh -x ./node_modules/patternfly-eng-release/scripts/_build.sh -x -before_script: - - npm prune after_success: + - if [[ -s package.json ]]; then cp package.json dist; fi + - if [[ -s npm-shrinkwrap.json ]]; then cp package.json dist; fi + - npm prune - npm run semantic-release - npm run publish-travis diff --git a/index.ts b/index.ts index 9437a50a7..d45012bd1 100644 --- a/index.ts +++ b/index.ts @@ -61,3 +61,15 @@ export { ToolbarConfig } from './src/app/toolbar/toolbar-config'; export { ToolbarComponent } from './src/app/toolbar/toolbar.component'; export { ToolbarModule } from './src/app/toolbar/toolbar.module'; export { ToolbarView } from './src/app/toolbar/toolbar-view'; + +// Wizard +export { WizardBase } from './src/app/wizard/wizard-base'; +export { WizardComponent } from './src/app/wizard/wizard.component'; +export { WizardConfig } from './src/app/wizard/wizard-config'; +export { WizardEvent } from './src/app/wizard/wizard-event'; +export { WizardModule } from './src/app/wizard/wizard.module'; +export { WizardReviewComponent } from './src/app/wizard/wizard-review.component'; +export { WizardStep } from './src/app/wizard/wizard-step'; +export { WizardStepComponent } from './src/app/wizard/wizard-step.component'; +export { WizardStepConfig } from './src/app/wizard/wizard-step-config'; +export { WizardSubstepComponent } from './src/app/wizard/wizard-substep.component'; diff --git a/package.json b/package.json index 6e1b808c8..46877ee44 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "start:demo": "webpack-dev-server --config config/webpack.demo.js --progress --host 0.0.0.0 --port 8001 --profile --watch --content-base dist-demo", "test": "karma start", "transpile": "gulp transpile", - "semantic-release": "semantic-release pre && cp package.json dist && npm publish dist/ && semantic-release post", + "semantic-release": "semantic-release pre && npm publish dist/ && semantic-release post", "semantic-release-build": "npm run build" }, "license": "Apache-2.0", @@ -67,10 +67,10 @@ "@angular/router": "^4.0.1", "core-js": "2.4.1", "moment": "2.17.1", - "ngx-bootstrap": "1.7.1", - "patternfly": "^3.24.0", + "ngx-bootstrap": "1.8.0", + "patternfly": "^3.26.1", "rxjs": "5.0.1", - "zone.js": "0.7.7" + "zone.js": "0.8.4" }, "devDependencies": { "@types/jasmine": "2.5.43", @@ -133,12 +133,13 @@ "mocha": "3.2.0", "ng-router-loader": "2.1.0", "npm-run-all": "4.0.2", + "nsp": "2.7.0", "null-loader": "0.1.1", "opt-cli": "1.5.1", "optimize-js-plugin": "0.0.4", "parse5": "2.2.3", "patternfly-eng-publish": "0.0.4", - "patternfly-eng-release": "~3.25.1", + "patternfly-eng-release": "^3.26.3", "phantomjs-prebuilt": "2.1.14", "postcss": "6.0.6", "postcss-loader": "1.3.3", diff --git a/patternfly-ng.module.ts b/patternfly-ng.module.ts index 4ce5d811e..5b31c3dcc 100644 --- a/patternfly-ng.module.ts +++ b/patternfly-ng.module.ts @@ -11,13 +11,12 @@ import { SampleModule } from './src/app/sample/sample.module'; import { SearchHighlightModule } from './src/app/search-highlight/search-highlight.module'; import { SortModule } from './src/app/sort/sort.module'; import { ToolbarModule } from './src/app/toolbar/toolbar.module'; +import { WizardModule } from './src/app/wizard/wizard.module'; @NgModule({ imports: [ FormsModule ], - declarations: [ - ], exports: [ ActionModule, EmptyStateModule, @@ -28,7 +27,8 @@ import { ToolbarModule } from './src/app/toolbar/toolbar.module'; SampleModule, SearchHighlightModule, SortModule, - ToolbarModule + ToolbarModule, + WizardModule ] }) export class PatternFlyNgModule { diff --git a/src/app/filter/filter-fields.component.less b/src/app/filter/filter-fields.component.less index 19c2c8bac..e8cbe6f4f 100644 --- a/src/app/filter/filter-fields.component.less +++ b/src/app/filter/filter-fields.component.less @@ -7,7 +7,6 @@ padding-left: 0; width: 275px; } - .btn-default { font-size: 12px; } .typeahead-input-container { position: relative; padding-right: 0; @@ -26,7 +25,6 @@ background-color: @color-pf-white; background-image: none; color: @color-pf-black-500; - font-size: 12px; font-style: italic; font-weight: 400; } diff --git a/src/app/wizard/examples/wizard-basic-example.component.html b/src/app/wizard/examples/wizard-basic-example.component.html new file mode 100644 index 000000000..20522ae20 --- /dev/null +++ b/src/app/wizard/examples/wizard-basic-example.component.html @@ -0,0 +1,161 @@ + + + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ + + + + + + +
+
+
+

Deployment in progress

+

Lorem ipsum dolor sit amet, porta at suspendisse ac, ut wisi vivamus, lorem sociosqu eget nunc amet.

+
+
+
+

Deployment was successful

+

Lorem ipsum dolor sit amet, porta at suspendisse ac, ut wisi vivamus, lorem sociosqu eget nunc amet.

+ +
+
+
+
+
+ + +
+
+ Name: + {{data.name}} +
+
+ Description: + {{data.description}} +
+
+
+ + +
+
+ Lorem: + {{data.lorem}} +
+
+ Ipsum: + {{data.ipsum}} +
+
+
+ + +
+
+ Aliquam: + {{data.aliquam}} +
+
+ Fermentum: + {{data.fermentum}} +
+
+
+ + +
+
+ Consectetur: + {{data.consectetur}} +
+
+ Adipiscing: + {{data.adipiscing}} +
+
+
diff --git a/src/app/wizard/examples/wizard-basic-example.component.ts b/src/app/wizard/examples/wizard-basic-example.component.ts new file mode 100644 index 000000000..0ded8dabb --- /dev/null +++ b/src/app/wizard/examples/wizard-basic-example.component.ts @@ -0,0 +1,168 @@ +import { + Component, + Host, + OnInit, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; + +import { WizardConfig } from '../wizard-config'; +import { WizardComponent } from '../wizard.component'; +import { WizardExampleComponent } from './wizard-example.component'; +import { WizardEvent } from '../wizard-event'; +import { WizardStepConfig } from '../wizard-step-config'; + +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'wizard-basic-example', + templateUrl: './wizard-basic-example.component.html' +}) +export class WizardBasicExampleComponent implements OnInit { + @ViewChild('wizard') wizard: WizardComponent; + + data: any = {}; + deployComplete: boolean = true; + + // Wizard Step 1 + step1Config: WizardStepConfig; + step1aConfig: WizardStepConfig; + step1bConfig: WizardStepConfig; + + // Wizard Step 2 + step2Config: WizardStepConfig; + step2aConfig: WizardStepConfig; + step2bConfig: WizardStepConfig; + + // Wizard Step 3 + step3Config: WizardStepConfig; + step3aConfig: WizardStepConfig; + step3bConfig: WizardStepConfig; + + // Wizard + wizardConfig: WizardConfig; + wizardExample: WizardExampleComponent; + + constructor(@Host() wizardExample: WizardExampleComponent) { + this.wizardExample = wizardExample; + } + + ngOnInit(): void { + // Step 1 + this.step1Config = { + id: 'step1', + priority: 0, + title: 'First Step' + } as WizardStepConfig; + this.step1aConfig = { + id: 'step1a', + expandReviewDetails: true, + nextEnabled: false, + priority: 0, + title: 'Details' + } as WizardStepConfig; + this.step1bConfig = { + id: 'step1b', + expandReviewDetails: true, + priority: 1, + title: 'Settings' + } as WizardStepConfig; + + // Step 2 + this.step2Config = { + id: 'step2', + priority: 0, + title: 'Second Step' + } as WizardStepConfig; + this.step2aConfig = { + id: 'step2a', + expandReviewDetails: true, + priority: 0, + title: 'Details' + } as WizardStepConfig; + this.step2bConfig = { + id: 'step2b', + expandReviewDetails: true, + priority: 1, + title: 'Settings' + } as WizardStepConfig; + + // Step 3 + this.step3Config = { + id: 'step3', + priority: 2, + title: 'Review' + } as WizardStepConfig; + this.step3aConfig = { + id: 'step3a', + priority: 0, + title: 'Summary' + } as WizardStepConfig; + this.step3bConfig = { + id: 'step3b', + priority: 1, + title: 'Deploy' + } as WizardStepConfig; + + // Wizard + this.wizardConfig = { + title: 'Wizard Title', + sidebarStyleClass: 'example-wizard-sidebar', + stepStyleClass: 'example-wizard-step' + } as WizardConfig; + + this.setNavAway(false); + } + + // Methods + + nextClicked($event: WizardEvent): void { + if ($event.step.config.id === 'step3b') { + this.wizardExample.closeModal($event); + } + } + + startDeploy(): void { + this.deployComplete = false; + this.wizardConfig.done = true; + + // Simulate a delay + setTimeout(() => { + this.deployComplete = true; + }, 2500); + } + + stepChanged($event: WizardEvent) { + if ($event.step.config.id === 'step1a') { + this.setNavAway(false); + } else if ($event.step.config.id === 'step3a') { + this.wizardConfig.nextTitle = 'Deploy'; + } else if ($event.step.config.id === 'step3b') { + this.wizardConfig.nextTitle = 'Close'; + } else { + this.wizardConfig.nextTitle = 'Next >'; + } + } + + updateName(): void { + this.step1aConfig.nextEnabled = (this.data.name !== undefined && this.data.name.length > 0); + this.setNavAway(this.step1aConfig.nextEnabled); + } + + // Private + + private setNavAway(allow: boolean) { + this.step1aConfig.allowNavAway = allow; + + this.step1Config.allowClickNav = allow; + this.step1aConfig.allowClickNav = allow; + this.step1bConfig.allowClickNav = allow; + + this.step2Config.allowClickNav = allow; + this.step2aConfig.allowClickNav = allow; + this.step2bConfig.allowClickNav = allow; + + this.step3Config.allowClickNav = allow; + this.step3aConfig.allowClickNav = allow; + this.step3bConfig.allowClickNav = allow; + } +} diff --git a/src/app/wizard/examples/wizard-embed-example.component.html b/src/app/wizard/examples/wizard-embed-example.component.html new file mode 100644 index 000000000..20522ae20 --- /dev/null +++ b/src/app/wizard/examples/wizard-embed-example.component.html @@ -0,0 +1,161 @@ + + + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ + + + + + + +
+
+
+

Deployment in progress

+

Lorem ipsum dolor sit amet, porta at suspendisse ac, ut wisi vivamus, lorem sociosqu eget nunc amet.

+
+
+
+

Deployment was successful

+

Lorem ipsum dolor sit amet, porta at suspendisse ac, ut wisi vivamus, lorem sociosqu eget nunc amet.

+ +
+
+
+
+
+ + +
+
+ Name: + {{data.name}} +
+
+ Description: + {{data.description}} +
+
+
+ + +
+
+ Lorem: + {{data.lorem}} +
+
+ Ipsum: + {{data.ipsum}} +
+
+
+ + +
+
+ Aliquam: + {{data.aliquam}} +
+
+ Fermentum: + {{data.fermentum}} +
+
+
+ + +
+
+ Consectetur: + {{data.consectetur}} +
+
+ Adipiscing: + {{data.adipiscing}} +
+
+
diff --git a/src/app/wizard/examples/wizard-embed-example.component.ts b/src/app/wizard/examples/wizard-embed-example.component.ts new file mode 100644 index 000000000..8c5f7dcf4 --- /dev/null +++ b/src/app/wizard/examples/wizard-embed-example.component.ts @@ -0,0 +1,172 @@ +import { + Component, + Host, + OnInit, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; + +import { WizardConfig } from '../wizard-config'; +import { WizardComponent } from '../wizard.component'; +import { WizardExampleComponent } from './wizard-example.component'; +import { WizardEvent } from '../wizard-event'; +import { WizardStepConfig } from '../wizard-step-config'; + +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'wizard-embed-example', + templateUrl: './wizard-embed-example.component.html' +}) +export class WizardEmbedExampleComponent implements OnInit { + @ViewChild('wizard') wizard: WizardComponent; + + data: any = {}; + deployComplete: boolean = true; + + // Wizard Step 1 + step1Config: WizardStepConfig; + step1aConfig: WizardStepConfig; + step1bConfig: WizardStepConfig; + + // Wizard Step 2 + step2Config: WizardStepConfig; + step2aConfig: WizardStepConfig; + step2bConfig: WizardStepConfig; + + // Wizard Step 3 + step3Config: WizardStepConfig; + step3aConfig: WizardStepConfig; + step3bConfig: WizardStepConfig; + + // Wizard + wizardConfig: WizardConfig; + wizardExample: WizardExampleComponent; + + constructor(@Host() wizardExample: WizardExampleComponent) { + this.wizardExample = wizardExample; + } + + ngOnInit(): void { + // Step 1 + this.step1Config = { + id: 'step1', + priority: 0, + title: 'First Step' + } as WizardStepConfig; + this.step1aConfig = { + id: 'step1a', + expandReviewDetails: true, + nextEnabled: false, + priority: 0, + title: 'Details' + } as WizardStepConfig; + this.step1bConfig = { + id: 'step1b', + expandReviewDetails: true, + priority: 1, + title: 'Settings' + } as WizardStepConfig; + + // Step 2 + this.step2Config = { + id: 'step2', + priority: 0, + title: 'Second Step' + } as WizardStepConfig; + this.step2aConfig = { + id: 'step2a', + expandReviewDetails: true, + priority: 0, + title: 'Details' + } as WizardStepConfig; + this.step2bConfig = { + id: 'step2b', + expandReviewDetails: true, + priority: 1, + title: 'Settings' + } as WizardStepConfig; + + // Step 3 + this.step3Config = { + id: 'step3', + priority: 2, + title: 'Review' + } as WizardStepConfig; + this.step3aConfig = { + id: 'step3a', + priority: 0, + title: 'Summary' + } as WizardStepConfig; + this.step3bConfig = { + id: 'step3b', + priority: 1, + title: 'Deploy' + } as WizardStepConfig; + + // Wizard + this.wizardConfig = { + embedInPage: true, + loadingTitle: 'Wizard loading', + loadingSecondaryInfo: 'ipsum dolor sit amet, porta at suspendisse ac, ut wisi vivamus, lorem sociosqu.', + title: 'Wizard Title', + sidebarStyleClass: 'example-wizard-sidebar', + stepStyleClass: 'example-wizard-step' + } as WizardConfig; + + this.setNavAway(false); + } + + // Methods + + nextClicked($event: WizardEvent): void { + if ($event.step.config.id === 'step3b') { + this.wizardExample.closeModal($event); + } + } + + startDeploy(): void { + this.deployComplete = false; + this.wizardConfig.done = true; + + // Simulate a delay + setTimeout(() => { + this.deployComplete = true; + }, 2500); + } + + stepChanged($event: WizardEvent) { + if ($event.step.config.id === 'step1a') { + this.setNavAway(false); + } else if ($event.step.config.id === 'step3a') { + this.wizardConfig.nextTitle = 'Deploy'; + } else if ($event.step.config.id === 'step3b') { + // Note: The next button is not disabled by default when wizard is done + this.step3Config.nextEnabled = false; + } else { + this.wizardConfig.nextTitle = 'Next >'; + } + } + + updateName(): void { + this.step1aConfig.nextEnabled = (this.data.name !== undefined && this.data.name.length > 0); + this.setNavAway(this.step1aConfig.nextEnabled); + } + + // Private + + private setNavAway(allow: boolean) { + this.step1aConfig.allowNavAway = allow; + + this.step1Config.allowClickNav = allow; + this.step1aConfig.allowClickNav = allow; + this.step1bConfig.allowClickNav = allow; + + this.step2Config.allowClickNav = allow; + this.step2aConfig.allowClickNav = allow; + this.step2bConfig.allowClickNav = allow; + + this.step3Config.allowClickNav = allow; + this.step3aConfig.allowClickNav = allow; + this.step3bConfig.allowClickNav = allow; + } +} diff --git a/src/app/wizard/examples/wizard-example.component.css b/src/app/wizard/examples/wizard-example.component.css new file mode 100644 index 000000000..50e41b69f --- /dev/null +++ b/src/app/wizard/examples/wizard-example.component.css @@ -0,0 +1,2 @@ +.example-wizard-sidebar,.example-wizard-step{height:500px;max-height:500px;overflow-y:auto} +/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFwcC93aXphcmQvZXhhbXBsZXMvd2l6YXJkLWV4YW1wbGUuY29tcG9uZW50Lmxlc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7RUFDRSxhQUFBO0VBQ0EsaUJBQUE7RUFDQSxnQkFBQTs7QUFFRjtFQUNFLGFBQUE7RUFDQSxpQkFBQTtFQUNBLGdCQUFBIiwic291cmNlc0NvbnRlbnQiOlsiLmV4YW1wbGUtd2l6YXJkLXNpZGViYXIge1xuICBoZWlnaHQ6IDUwMHB4O1xuICBtYXgtaGVpZ2h0OiA1MDBweDtcbiAgb3ZlcmZsb3cteTogYXV0bztcbn1cbi5leGFtcGxlLXdpemFyZC1zdGVwIHtcbiAgaGVpZ2h0OiA1MDBweDtcbiAgbWF4LWhlaWdodDogNTAwcHg7XG4gIG92ZXJmbG93LXk6IGF1dG87XG59XG4iXSwiZmlsZSI6ImFwcC93aXphcmQvZXhhbXBsZXMvd2l6YXJkLWV4YW1wbGUuY29tcG9uZW50LmNzcyJ9 */ diff --git a/src/app/wizard/examples/wizard-example.component.html b/src/app/wizard/examples/wizard-example.component.html new file mode 100644 index 000000000..0d51f4a44 --- /dev/null +++ b/src/app/wizard/examples/wizard-example.component.html @@ -0,0 +1,109 @@ +
+
+
+

Wizard Component Example

+
+
+
+ + +
+
+
+
+ + + + +
+
+
+
+
+

Code

+
+
+
+
+ + + + + + + + + + + +
+
+
+ +
+
+
+
+ +
+ This example disables navigation until simulated processes are complete. +
+ + + +
+
+
+
+
+

Code

+
+
+
+
+ + + + + + + + + + + +
+
+
+ +
+
+
+
+ +
+
+
+
+
+

Code

+
+
+
+
+ + + + + + + + + + + +
+
+
+
+
diff --git a/src/app/wizard/examples/wizard-example.component.less b/src/app/wizard/examples/wizard-example.component.less new file mode 100644 index 000000000..497a3ddca --- /dev/null +++ b/src/app/wizard/examples/wizard-example.component.less @@ -0,0 +1,15 @@ +@import (reference) "../../../assets/stylesheets/patternfly-ng"; + +.example-wizard-sidebar { + height: 500px; + max-height: 500px; + overflow-y: auto; +} +.example-wizard-step { + height: 500px; + max-height: 500px; + overflow-y: auto; +} +.example-wizard-embed { + border: 1px solid @color-pf-black-300; +} diff --git a/src/app/wizard/examples/wizard-example.component.ts b/src/app/wizard/examples/wizard-example.component.ts new file mode 100644 index 000000000..82a2013d9 --- /dev/null +++ b/src/app/wizard/examples/wizard-example.component.ts @@ -0,0 +1,46 @@ +import { + Component, + OnInit, + TemplateRef, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; + +import { BsModalService } from 'ngx-bootstrap/modal'; +import { BsModalRef } from 'ngx-bootstrap/modal/modal-options.class'; +import { TabDirective } from 'ngx-bootstrap/tabs'; + +import { WizardEvent } from '../wizard-event'; + +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'wizard-example', + styleUrls: ['./wizard-example.component.less'], + templateUrl: './wizard-example.component.html' +}) +export class WizardExampleComponent implements OnInit { + @ViewChild('wizardTemplate') wizardTemplate: TemplateRef; + + activeTab: string = ''; + modalRef: BsModalRef; + + constructor(private modalService: BsModalService) { + } + + ngOnInit(): void { + } + + // Methods + + closeModal($event: WizardEvent): void { + this.modalRef.hide(); + } + + openModal(template: TemplateRef): void { + this.modalRef = this.modalService.show(template, {class: 'modal-lg'}); + } + + tabSelected($event: TabDirective): void { + this.activeTab = $event.heading; + } +} diff --git a/src/app/wizard/examples/wizard-example.module.ts b/src/app/wizard/examples/wizard-example.module.ts new file mode 100644 index 000000000..d7b948fc2 --- /dev/null +++ b/src/app/wizard/examples/wizard-example.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { ModalModule } from 'ngx-bootstrap/modal'; +import { TabsModule, TabsetConfig } from 'ngx-bootstrap/tabs'; + +import { DemoComponentsModule } from '../../../demo/components/demo-components.module'; +import { WizardBasicExampleComponent } from './wizard-basic-example.component'; +import { WizardEmbedExampleComponent } from './wizard-embed-example.component'; +import { WizardNavExampleComponent } from './wizard-nav-example.component'; +import { WizardExampleComponent } from './wizard-example.component'; +import { WizardModule } from '../wizard.module'; + +@NgModule({ + declarations: [ + WizardBasicExampleComponent, + WizardEmbedExampleComponent, + WizardNavExampleComponent, + WizardExampleComponent + ], + imports: [ + CommonModule, + DemoComponentsModule, + FormsModule, + ModalModule.forRoot(), + TabsModule.forRoot(), + WizardModule + ], + providers: [ TabsetConfig ] +}) +export class WizardExampleModule { + constructor() {} +} diff --git a/src/app/wizard/examples/wizard-nav-example.component.html b/src/app/wizard/examples/wizard-nav-example.component.html new file mode 100644 index 000000000..461b03b5c --- /dev/null +++ b/src/app/wizard/examples/wizard-nav-example.component.html @@ -0,0 +1,168 @@ + + + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+

Fetching data...

+

Lorem ipsum dolor sit amet, porta at suspendisse ac, ut wisi vivamus, lorem sociosqu eget nunc amet.

+
+
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ + + + + + + +
+
+
+

Deployment in progress

+

Lorem ipsum dolor sit amet, porta at suspendisse ac, ut wisi vivamus, lorem sociosqu eget nunc amet.

+
+
+
+

Deployment was successful

+

Lorem ipsum dolor sit amet, porta at suspendisse ac, ut wisi vivamus, lorem sociosqu eget nunc amet.

+ +
+
+
+
+
+ + +
+
+ Name: + {{data.name}} +
+
+ Description: + {{data.description}} +
+
+
+ + +
+
+ Lorem: + {{data.lorem}} +
+
+ Ipsum: + {{data.ipsum}} +
+
+
+ + +
+
+ Aliquam: + {{data.aliquam}} +
+
+ Fermentum: + {{data.fermentum}} +
+
+
+ + +
+
+ Consectetur: + {{data.consectetur}} +
+
+ Adipiscing: + {{data.adipiscing}} +
+
+
diff --git a/src/app/wizard/examples/wizard-nav-example.component.ts b/src/app/wizard/examples/wizard-nav-example.component.ts new file mode 100644 index 000000000..2f2f7bc84 --- /dev/null +++ b/src/app/wizard/examples/wizard-nav-example.component.ts @@ -0,0 +1,186 @@ +import { + Component, + Host, + OnInit, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; + +import { WizardConfig } from '../wizard-config'; +import { WizardComponent } from '../wizard.component'; +import { WizardExampleComponent } from './wizard-example.component'; +import { WizardEvent } from '../wizard-event'; +import { WizardStepConfig } from '../wizard-step-config'; + +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'wizard-nav-example', + templateUrl: './wizard-nav-example.component.html' +}) +export class WizardNavExampleComponent implements OnInit { + @ViewChild('wizard') wizard: WizardComponent; + + data: any = {}; + deployComplete: boolean = true; + fetchComplete: boolean = true; + + // Wizard Step 1 + step1Config: WizardStepConfig; + step1aConfig: WizardStepConfig; + step1bConfig: WizardStepConfig; + + // Wizard Step 2 + step2Config: WizardStepConfig; + step2aConfig: WizardStepConfig; + step2bConfig: WizardStepConfig; + + // Wizard Step 3 + step3Config: WizardStepConfig; + step3aConfig: WizardStepConfig; + step3bConfig: WizardStepConfig; + + // Wizard + wizardConfig: WizardConfig; + wizardExample: WizardExampleComponent; + + constructor(@Host() wizardExample: WizardExampleComponent) { + this.wizardExample = wizardExample; + } + + ngOnInit(): void { + // Step 1 + this.step1Config = { + id: 'step1', + priority: 0, + title: 'First Step' + } as WizardStepConfig; + this.step1aConfig = { + id: 'step1a', + expandReviewDetails: true, + nextEnabled: false, + priority: 0, + title: 'Details' + } as WizardStepConfig; + this.step1bConfig = { + id: 'step1b', + expandReviewDetails: true, + priority: 1, + title: 'Settings' + } as WizardStepConfig; + + // Step 2 + this.step2Config = { + id: 'step2', + priority: 0, + title: 'Second Step' + } as WizardStepConfig; + this.step2aConfig = { + id: 'step2a', + expandReviewDetails: true, + priority: 0, + title: 'Details' + } as WizardStepConfig; + this.step2bConfig = { + id: 'step2b', + expandReviewDetails: true, + priority: 1, + title: 'Settings' + } as WizardStepConfig; + + // Step 3 + this.step3Config = { + id: 'step3', + priority: 2, + title: 'Review' + } as WizardStepConfig; + this.step3aConfig = { + id: 'step3a', + priority: 0, + title: 'Summary' + } as WizardStepConfig; + this.step3bConfig = { + id: 'step3b', + priority: 1, + title: 'Deploy' + } as WizardStepConfig; + + // Wizard + this.wizardConfig = { + loadingTitle: 'Wizard loading', + loadingSecondaryInfo: 'ipsum dolor sit amet, porta at suspendisse ac, ut wisi vivamus, lorem sociosqu.', + title: 'Wizard Title', + ready: false, + sidebarStyleClass: 'example-wizard-sidebar', + stepStyleClass: 'example-wizard-step' + } as WizardConfig; + + // Simulate a delay + setTimeout(() => { + this.wizardConfig.ready = true; + }, 2500); + + this.setNavAway(false); + } + + // Methods + + nextClicked($event: WizardEvent): void { + if ($event.step.config.id === 'step1a') { + this.fetchComplete = false; + this.setNavAway(false); + + // Simulate a delay + setTimeout(() => { + this.fetchComplete = true; + this.setNavAway(true); + this.wizard.goToNextStep(); + }, 2500); + } else if ($event.step.config.id === 'step3b') { + this.wizardExample.closeModal($event); + } + } + + startDeploy(): void { + this.deployComplete = false; + this.wizardConfig.done = true; + + // Simulate a delay + setTimeout(() => { + this.deployComplete = true; + }, 2500); + } + + stepChanged($event: WizardEvent) { + if ($event.step.config.id === 'step1a') { + this.setNavAway(false); + } else if ($event.step.config.id === 'step3a') { + this.wizardConfig.nextTitle = 'Deploy'; + } else if ($event.step.config.id === 'step3b') { + this.wizardConfig.nextTitle = 'Close'; + } else { + this.wizardConfig.nextTitle = 'Next >'; + } + } + + updateName(): void { + this.step1aConfig.nextEnabled = (this.data.name !== undefined && this.data.name.length > 0); + } + + // Private + + private setNavAway(allow: boolean) { + this.step1aConfig.allowNavAway = allow; + + this.step1Config.allowClickNav = allow; + this.step1aConfig.allowClickNav = allow; + this.step1bConfig.allowClickNav = allow; + + this.step2Config.allowClickNav = allow; + this.step2aConfig.allowClickNav = allow; + this.step2bConfig.allowClickNav = allow; + + this.step3Config.allowClickNav = allow; + this.step3aConfig.allowClickNav = allow; + this.step3bConfig.allowClickNav = allow; + } +} diff --git a/src/app/wizard/wizard-base.ts b/src/app/wizard/wizard-base.ts new file mode 100644 index 000000000..63a520d75 --- /dev/null +++ b/src/app/wizard/wizard-base.ts @@ -0,0 +1,150 @@ +import { WizardStep } from './wizard-step'; + +import { find } from 'lodash'; + +/** + * A base class with common functionality for wizard and wizard-step + */ +export class WizardBase { + private _selectedStep: WizardStep; + private _steps: WizardStep[] = []; + + /** + * The default constructor + */ + constructor() {} + + // Accessors + + /** + * Returns the selected wizard step or substep + * + * @returns {WizardStep} The wizard step or substep + */ + get selectedStep(): WizardStep { + return this._selectedStep; + } + + /** + * Returns the selected wizard step or substep number + * + * @returns {number} The step index + */ + get selectedStepIndex(): number { + // Retrieve selected step number + return this.stepIndex(this.selectedStep) + 1; + } + + /** + * Set the selected wizard step or substep for this component + * + * @param {WizardStep} step The wizard step or substep + */ + set selectedStep(step: WizardStep) { + this._selectedStep = step; + } + + /** + * Returns the wizard steps or substeps for this component + * + * @returns {WizardStep[]} The wizard steps or substeps + */ + get steps(): WizardStep[] { + return this._steps; + } + + /** + * Set the wizard steps or substeps for this component + * + * @param {WizardStep[]} steps The wizard steps or substeps + */ + set steps(steps: WizardStep[]) { + this._steps = steps; + } + + // Methods + + /** + * Add a wizard step or substep to this component + * + * @param {WizardStep} step The wizard step or substep to add + */ + addStep(step: WizardStep): void { + // Insert the step into step array + let insertBefore = find(this.steps, (nextStep) => { + return nextStep.config.priority > step.config.priority; + }); + if (insertBefore) { + this.steps.splice(this.steps.indexOf(insertBefore), 0, step); + } else { + this.steps.push(step); + } + } + + /** + * Returns only enabled wizard steps + * + * @returns {WizardStep[]} The wizard stepd or substepd + */ + protected getEnabledSteps(): WizardStep[] { + return this.steps.filter((step: WizardStep) => { + return (step.config.disabled !== true); + }); + } + + /** + * Returns the step index for the given wizard step or substep + * + * @param {WizardStep} step The wizard step or substep + * @returns {number} The step number + */ + getStepIndex(step: WizardStep): number { + return this.stepIndex(step) + 1; + } + + /** + * Returns the wizard step or substep for the given title + * + * @param {string} title The title to find + * @returns {WizardStep} The wizard step or substep + */ + protected stepByTitle(title: string): WizardStep { + let foundStep; + this.getEnabledSteps().forEach((step: WizardStep) => { + if (step.config.title === title) { + foundStep = step; + } + }); + return foundStep; + } + + /** + * Returns the index for the given wizard step or substep + * + * @param {WizardStep} step The wizard step or substep + * @returns {number} The wizard step or substep index + */ + protected stepIndex(step: WizardStep): number { + let idx = 0; + let res = -1; + this.getEnabledSteps().forEach((currStep) => { + if (currStep === step) { + res = idx; + } + idx++; + }); + return res; + } + + /** + * Unselect all wizard steps and substeps + */ + protected unselectAll(): void { + // Traverse steps array and set each "selected" property to false + this.getEnabledSteps().forEach((step: WizardStep) => { + step.selected = false; + }); + // Set selectedStep variable to null + this.selectedStep = null; + } +} diff --git a/src/app/wizard/wizard-config.ts b/src/app/wizard/wizard-config.ts new file mode 100644 index 000000000..66ff0fb2b --- /dev/null +++ b/src/app/wizard/wizard-config.ts @@ -0,0 +1,92 @@ +/** + * A config containing properties for wizard + */ +export class WizardConfig { + /** + * The text to display on the cancel button + */ + cancelTitle?: string; + + /** + * The height the wizard content should be set to. This is used only if the stepStyleClass is not given. + * The default is 300px. + */ + contentHeight?: string; + + /** + * The current step can be changed externally - this is the title of the step to switch the wizard to + */ + currentStep?: string; + + /** + * Flag indicating that the wizard is done + */ + done?: boolean; + + /** + * Flag indicating that the wizard is embedded in a page (not a modal). This moves the navigation buttons to the left + * hand side of the footer and removes the close button. + */ + embedInPage?: boolean; + + /** + * Flag indicating to hide the step indicators in the header of the wizard + */ + hideIndicators?: boolean; + + /** + * Flag indicating to hide page navigation sidebar on the wizard pages + */ + hideSidebar?: boolean; + + /** + * Flag indicating to hide the title bar. Default is false + */ + hideHeader?: boolean; + + /** + * Flag indicating to hide the back button, useful in 2 step wizards. Default is false + */ + hidePreviousButton?: boolean; + + /** + * The text displayed when the wizard is loading + */ + loadingTitle?: string; + + /** + * Secondary descriptive information to display when the wizard is loading + */ + loadingSecondaryInfo?: string; + + /** + * The text to display on the next button + */ + nextTitle?: string; + + /** + * The text to display on the back button + */ + previousTitle?: string; + + /** + * Flag indicating that the wizard is ready + */ + ready?: boolean; + + /** + * CSS class to be give to the sidebar panel. Only used if the stepClass is also provided + */ + sidebarStyleClass?: string; + + /** + * CSS class to be given to the steps page container. Used for the sidebar panel as well unless + * sidebarStyleClass is provided + */ + stepStyleClass?: string; + + /** + * The wizard title displayed in the header + */ + title: string; +} diff --git a/src/app/wizard/wizard-event.ts b/src/app/wizard/wizard-event.ts new file mode 100644 index 000000000..65eecf71e --- /dev/null +++ b/src/app/wizard/wizard-event.ts @@ -0,0 +1,16 @@ +import { WizardStep } from './wizard-step'; + +/** + * An object containing properties for wizard events + */ +export class WizardEvent { + /** + * The order of the wizard step of substep + */ + index?: number; + + /** + * The current wizard step or substep + */ + step: WizardStep; +} diff --git a/src/app/wizard/wizard-review.component.html b/src/app/wizard/wizard-review.component.html new file mode 100644 index 000000000..41db25123 --- /dev/null +++ b/src/app/wizard/wizard-review.component.html @@ -0,0 +1,34 @@ + diff --git a/src/app/wizard/wizard-review.component.ts b/src/app/wizard/wizard-review.component.ts new file mode 100644 index 000000000..a3697206e --- /dev/null +++ b/src/app/wizard/wizard-review.component.ts @@ -0,0 +1,73 @@ +import { + Component, + Host, + OnInit, + ViewEncapsulation +} from '@angular/core'; + +import { WizardComponent } from './wizard.component'; +import { WizardStepComponent } from './wizard-step.component'; +import { WizardSubstepComponent } from './wizard-substep.component'; +import { WizardStep } from './wizard-step'; + +/** + * Wizard review component + * + * Note: This component is expected to be direct descendant of wizard-step or wizard-substep. + */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-wizard-review', + templateUrl: './wizard-review.component.html' +}) +export class WizardReviewComponent implements OnInit { + private wizard: WizardComponent; + + /** + * The default constructor + */ + constructor(@Host() wizard: WizardComponent) { + this.wizard = wizard; + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + } + + // Methods + + /** + * Returns only wizard steps with review templates + * + * @returns {WizardStep[]} The wizard stepd or substepd + */ + getReviewSteps(): WizardStep[] { + return this.wizard.getReviewSteps(); + } + + // Private + + // Returns the step number for the given wizard step and substep + private getSubstepNumber(step: WizardStepComponent, substep: WizardSubstepComponent) { + return step.getDisplayNumber(substep); + } + + // Returns only wizard steps with review templates + private getReviewSubsteps(step: WizardStepComponent): WizardStep[] { + return step.getReviewSteps(); + } + + // Toggles the review step control + private toggleReview(step: WizardStep): void { + step.config.expandReview = !step.config.expandReview; + }; + + // Toggles the review details control + private toggleReviewDetails(step: WizardStep): void { + step.config.expandReviewDetails = !step.config.expandReviewDetails; + }; +} diff --git a/src/app/wizard/wizard-step-config.ts b/src/app/wizard/wizard-step-config.ts new file mode 100644 index 000000000..8a512866b --- /dev/null +++ b/src/app/wizard/wizard-step-config.ts @@ -0,0 +1,79 @@ +/** + * A config containing properties for wizard steps and substeps + */ +export class WizardStepConfig { + /** + * Indicates that the user can click on numeric step indicators to navigate directly to a step + */ + allowClickNav?: boolean; + + /** + * Indicates that the user may navigate away from this wizard step or substep + */ + allowNavAway?: boolean; + + /** + * Indicates that this step has been completed + */ + completed?: boolean; + + /** + * The wizard step or substep description + */ + description?: string; + + /** + * Indicates that the wizard is disabled + */ + disabled?: boolean; + + /** + * Indicates whether review information should be expanded by default when the review step is reached + */ + expandReview?: boolean; + + /** + * Indicators whether review details information should be expanded by default when the review step is reached + */ + expandReviewDetails?: boolean; + + /** + * The wizard step or substep identifier + */ + id?: string; + + /** + * Indicates the priority of this wizard step or substep relative to other wizard steps. Steps are expected to be + * numbered sequentially in the order they should be viewed. + */ + priority?: number; + + /** + * Indicates that the next button is enabled when the wizard step or substep is displayed + */ + nextEnabled?: boolean; + + /** + * The text to display as a tooltip for the next button when the wizard step is displayed. + * + * Not applicable for the wizard-substep component. + */ + nextTooltip?: string; + + /** + * Indicates that the previous button is enabled when the wizard step or substep is displayed + */ + previousEnabled?: boolean; + + /** + * The text to display as a tooltip for the previous button when the wizard step is displayed + * + * Not applicable for the wizard-substep component. + */ + previousTooltip?: string; + + /** + * The title for the wizard step or substep to be displayed in the header and review page + */ + title: string; +} diff --git a/src/app/wizard/wizard-step.component.css b/src/app/wizard/wizard-step.component.css new file mode 100644 index 000000000..4f53717c7 --- /dev/null +++ b/src/app/wizard/wizard-step.component.css @@ -0,0 +1,2 @@ +.pfng-wizard-single-step{margin-left:0} +/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFwcC93aXphcmQvd2l6YXJkLXN0ZXAuY29tcG9uZW50Lmxlc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7RUFDRSxjQUFBIiwic291cmNlc0NvbnRlbnQiOlsiQGltcG9ydCAocmVmZXJlbmNlKSBcIi4uLy4uL2Fzc2V0cy9zdHlsZXNoZWV0cy9wYXR0ZXJuZmx5LW5nXCI7XG5cbi5wZm5nLXdpemFyZC1zaW5nbGUtc3RlcCB7XG4gIG1hcmdpbi1sZWZ0OiAwO1xufVxuIl0sImZpbGUiOiJhcHAvd2l6YXJkL3dpemFyZC1zdGVwLmNvbXBvbmVudC5jc3MifQ== */ diff --git a/src/app/wizard/wizard-step.component.html b/src/app/wizard/wizard-step.component.html new file mode 100644 index 000000000..8b52bd895 --- /dev/null +++ b/src/app/wizard/wizard-step.component.html @@ -0,0 +1,24 @@ +
+ +
+
+ +
+
+
diff --git a/src/app/wizard/wizard-step.component.less b/src/app/wizard/wizard-step.component.less new file mode 100644 index 000000000..668213815 --- /dev/null +++ b/src/app/wizard/wizard-step.component.less @@ -0,0 +1,5 @@ +@import (reference) "../../assets/stylesheets/patternfly-ng"; + +.pfng-wizard-single-step { + margin-left: 0; +} diff --git a/src/app/wizard/wizard-step.component.ts b/src/app/wizard/wizard-step.component.ts new file mode 100644 index 000000000..5dbc5079b --- /dev/null +++ b/src/app/wizard/wizard-step.component.ts @@ -0,0 +1,325 @@ +import { + Component, + EventEmitter, + Host, + Input, + OnInit, + Output, + TemplateRef, + ViewEncapsulation +} from '@angular/core'; + +import { WizardBase } from './wizard-base'; +import { WizardComponent } from './wizard.component'; +import { WizardEvent } from './wizard-event'; +import { WizardStep } from './wizard-step'; +import { WizardStepConfig } from './wizard-step-config'; + +import { cloneDeep, defaults, isEqual } from 'lodash'; + +/** + * Wizard step component. Each step can stand alone or have substeps. + * + * Note: This component is expected to be a child of wizard. + */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-wizard-step', + styleUrls: ['./wizard-step.component.less'], + templateUrl: './wizard-step.component.html' +}) +export class WizardStepComponent extends WizardBase implements OnInit, WizardStep { + /** + * The wizard step config containing component properties + */ + @Input() config: WizardStepConfig; + + /** + * The wizard step template used for the review details screen + */ + @Input() reviewTemplate: TemplateRef; + + /** + * The event emitted when this wizard step is shown + */ + @Output('onShow') onShow = new EventEmitter(); + + private defaultConfig = { + allowClickNav: true, + allowNavAway: true, + completed: false, + data: {}, + disabled: false, + expandReview: true, + expandReviewDetails: false, + priority: 999, + nextEnabled: true, + title: '' + } as WizardStepConfig; + private init: boolean = true; + private pageIndex: number = 0; + private prevConfig: WizardStepConfig; + private _selected: boolean = false; + private wizard: WizardComponent; + + /** + * The default constructor + */ + constructor(@Host() wizard: WizardComponent) { + super(); + this.wizard = wizard; + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + if (this.wizard !== undefined && this.selectedStep === undefined) { + this.wizard.addStep(this); + } + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + if (this.wizard !== undefined) { + this.pageIndex = this.wizard.getStepIndex(this); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + this.prevConfig = cloneDeep(this.config); + } + + // Accessors + + /** + * Indicates that this wizard step has substeps + * + * @returns {boolean} true if this wizard step has substeps + */ + get hasSubsteps() { + return this.steps.length > 0; + } + + /** + * Indicates that the next button is enabled + * + * @returns {boolean} true if the next button is enabled + */ + get nextEnabled() { + let enabled = this.config.nextEnabled; + if (this.hasSubsteps) { + this.getEnabledSteps().forEach((step: WizardStep) => { + enabled = enabled && step.config.nextEnabled; + }); + } + return enabled; + } + + /** + * Indicates that the previous button is enabled + * + * @returns {boolean} true if the previous button is enabled + */ + get previousEnabled() { + let enabled = this.config.previousEnabled; + if (this.hasSubsteps) { + this.getEnabledSteps().forEach((step: WizardStep) => { + enabled = enabled && step.config.previousEnabled; + }); + } + return enabled; + } + + /** + * Indicates that this wizard step is selected + * + * @returns {boolean} True if this wizard step is selected + */ + get selected() { + return this._selected; + } + + /** + * Sets a flag indicating that this wizard step is selected + * + * @param {boolean} selected True if this wizard step is selected + */ + set selected(selected: boolean) { + this._selected = selected; + } + + // Methods + + /** + * Returns the step number to be displayed for the given wizard step or substep + * + * @param {WizardStep} step The wizard step or substep + * @returns {string} The step number to be displayed + */ + getDisplayNumber(step: WizardStep): string { + return this.pageIndex + String.fromCharCode(65 + this.stepIndex(step)) + '.'; + } + + /** + * Returns only wizard substeps with review templates + * + * @returns {WizardStep[]} The wizard stepd or substepd + */ + getReviewSteps(): WizardStep[] { + let reviewSteps = this.getEnabledSteps().filter((step: WizardStep) => { + return (step.reviewTemplate !== undefined); + }); + return reviewSteps; + } + + /** + * Navigate to the first wizard substep + */ + goToFirstStep(): void { + this.goTo(this.getEnabledSteps()[0]); + } + + /** + * Navigate to the last wizard substep + */ + goToLastStep(): void { + let enabledSteps = this.getEnabledSteps(); + this.goTo(enabledSteps[enabledSteps.length - 1]); + } + + /** + * Navigate to the next wizard step or substep + */ + goToNextStep(): void { + this.next(false); + } + + /** + * Navigate to the previous wizard step or substep + */ + goToPreviousStep(): void { + this.previous(false); + } + + /** + * Called when the next button has been selected. + * + * @param {boolean} emitEvent True to emit the wizard's onNext event + */ + next(emitEvent: boolean): boolean { + let enabledSteps: WizardStep[] = this.getEnabledSteps(); + + // Save the step you were on when next() was invoked + let index = this.stepIndex(this.selectedStep); + + let wizEvent = { + index: index, + step: this.selectedStep + } as WizardEvent; + + if (emitEvent !== false) { + this.wizard.onNext.emit(wizEvent); + } + + // Set completed property, which may be used to add/remove a style class from progress bar + this.selectedStep.config.completed = true; + + // Ensure this is not the last step. + if (index === enabledSteps.length - 1) { + return false; + } + this.goTo(enabledSteps[index + 1]); + return true; + } + + /** + * Called when the previous button has been selected. + * + * @param {boolean} emitEvent True to emit the wizard's onPrevious event + */ + previous(emitEvent: boolean): boolean { + let index = this.stepIndex(this.selectedStep); + let wizEvent = { + index: index, + step: this.selectedStep + } as WizardEvent; + + if (emitEvent !== false) { + this.wizard.onPrevious.emit(wizEvent); + } + + // Ensure this is not the first step + if (index === 0) { + return false; + } + this.goTo(this.getEnabledSteps()[index - 1]); + return true; + } + + /** + * Emits an event when a wizard step or substep is shown + */ + show(index: number) { + this.onShow.emit({ + index: index, + step: this + } as WizardEvent); + } + + // Private + + // Navigate to the given wizard substep + private goTo(step: WizardStep): void { + if (step === undefined || this.wizard === undefined || this.wizard.config.done + || (!this.init && this.selectedStep !== undefined && !this.selectedStep.config.allowNavAway)) { + return; + } + if (this.init || this.isPreviousStepsComplete(step) + || (this.getStepIndex(step) < this.selectedStepIndex && this.selectedStep.config.previousEnabled)) { + this.unselectAll(); + this.selectedStep = step; + step.selected = true; + step.show(this.stepIndex(step)); + this.wizard.stepChanged(step, this.stepIndex(step)); + this.wizard.updateStepIndex(this.stepIndex(this.selectedStep)); + this.init = false; + } + } + + // Indicates all previous substeps are complete for this wizard step + private isPreviousStepsComplete(nextStep: WizardStep): boolean { + let nextIdx = this.stepIndex(nextStep); + let complete = true; + this.getEnabledSteps().forEach((step: WizardStep, stepIndex) => { + if (stepIndex < nextIdx) { + complete = complete && step.config.nextEnabled; + } + }); + return complete; + } + + // Handle step navigation + private stepClick(step: WizardStep): void { + if (step.config.allowClickNav) { + this.goTo(step); + } + } +} diff --git a/src/app/wizard/wizard-step.ts b/src/app/wizard/wizard-step.ts new file mode 100644 index 000000000..a0c770c06 --- /dev/null +++ b/src/app/wizard/wizard-step.ts @@ -0,0 +1,91 @@ +import { + TemplateRef +} from '@angular/core'; + +import { WizardComponent } from './wizard.component'; +import { WizardStepConfig } from './wizard-step-config'; + +/** + * Wizard step + */ +export interface WizardStep { + /** + * The wizard step config containing component properties + */ + config: WizardStepConfig; + + /** + * Returns the step number to be displayed for the given wizard step or substep. + */ + getDisplayNumber?: Function; + + /** + * Returns only wizard steps with review templates. + */ + getReviewSteps?: Function; + + /** + * Navigate to the first wizard substep. + */ + goToFirstStep?: Function; + + /** + * Navigate to the last wizard substep. + */ + goToLastStep?: Function; + + /** + * Navigate to the next wizard step or substep + */ + goToNextStep?: Function; + + /** + * Navigate to the previous wizard step or substep + */ + goToPreviousStep?: Function; + + /** + * Indicates that this wizard step has substeps. + */ + hasSubsteps?: boolean; + + /** + * Called when the next button has been selected. + */ + next?: Function; + + /** + * Indicates that the next button is enabled when the wizard step is displayed. + */ + nextEnabled?: boolean; + + /** + * Called when the previous button has been selected. + */ + previous?: Function; + + /** + * Indicates that the previous button is enabled when the wizard step is displayed. + */ + previousEnabled?: boolean; + + /** + * The wizard step template used for the review details screen + */ + reviewTemplate: TemplateRef; + + /** + * Indicates that this wizard step or substep is selected + */ + selected: boolean; + + /** + * Returns the selected wizard step or substep index. + */ + selectedStepIndex?: number; + + /** + * Emits an event when a wizard step or substep is shown + */ + show: Function; +} diff --git a/src/app/wizard/wizard-substep.component.html b/src/app/wizard/wizard-substep.component.html new file mode 100644 index 000000000..b3ad55c97 --- /dev/null +++ b/src/app/wizard/wizard-substep.component.html @@ -0,0 +1 @@ + diff --git a/src/app/wizard/wizard-substep.component.ts b/src/app/wizard/wizard-substep.component.ts new file mode 100644 index 000000000..c0e514fd1 --- /dev/null +++ b/src/app/wizard/wizard-substep.component.ts @@ -0,0 +1,141 @@ +import { + Component, + EventEmitter, + Host, + Input, + OnInit, + Output, + TemplateRef, + ViewEncapsulation +} from '@angular/core'; + +import { WizardEvent } from './wizard-event'; +import { WizardStep } from './wizard-step'; +import { WizardStepConfig } from './wizard-step-config' +import { WizardStepComponent } from './wizard-step.component'; + +import { cloneDeep, defaults, isEqual } from 'lodash'; + +/** + * Wizard substep component. + * + * Note: This component is expected to be a child of wizard-step. + */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-wizard-substep', + templateUrl: './wizard-substep.component.html' +}) +export class WizardSubstepComponent implements OnInit, WizardStep { + /** + * The wizard step config containing component properties + */ + @Input() config: WizardStepConfig; + + /** + * The wizard substep template used for the review details screen + */ + @Input() reviewTemplate: TemplateRef; + + /** + * The event emitted when this wizard substep is shown + */ + @Output('onShow') onShow = new EventEmitter(); + + private defaultConfig = { + allowClickNav: true, + allowNavAway: true, + completed: false, + data: {}, + disabled: false, + expandReview: true, + expandReviewDetails: false, + priority: 999, + nextEnabled: true, + okToNavAway: true, + previousEnabled: true, + title: '' + } as WizardStepConfig; + private prevConfig: WizardStepConfig; + private _selected: boolean = false; + private step: WizardStepComponent; + + /** + * The default constructor + */ + constructor(@Host() step: WizardStepComponent) { + this.step = step; + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + + if (this.step !== undefined) { + this.step.config.allowClickNav = this.config.allowClickNav; + this.step.config.nextEnabled = this.config.nextEnabled; + this.step.config.allowNavAway = this.config.allowNavAway; + this.step.config.previousEnabled = this.config.previousEnabled; + + this.step.addStep(this); + } + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + this.prevConfig = cloneDeep(this.config); + } + + // Accessors + + /** + * Indicates that this wizard substep is selected + * + * @returns {boolean} True if this wizard substep is selected + */ + get selected(): boolean { + return this._selected; + } + + /** + * Sets a flag indicating that this wizard substep is selected + * + * @param {boolean} selected True if this wizard substep is selected + */ + set selected(selected: boolean) { + this._selected = selected; + } + + // Methods + + /** + * Emits an event when this wizard substep is shown + */ + show(index: number) { + this.onShow.emit({ + index: index, + step: this + } as WizardEvent); + } +} diff --git a/src/app/wizard/wizard.component.css b/src/app/wizard/wizard.component.css new file mode 100644 index 000000000..3e67c1efd --- /dev/null +++ b/src/app/wizard/wizard.component.css @@ -0,0 +1,2 @@ +.pfng-wizard-cancel-inline{margin-left:25px}.pfng-wizard-footer-inline{text-align:left}.pfng-wizard-main{margin-left:0}.pfng-wizard-position-override{position:relative}.wizard-pf-footer .btn-cancel.wizard-pf-cancel-no-back{margin-right:0}.wizard-pf-steps-indicator li a.disabled{cursor:default}.wizard-pf-steps-indicator li a.disabled:hover .wizard-pf-step-number{background-color:#fff;border-color:#bbb;color:#bbb} +/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFwcC93aXphcmQvd2l6YXJkLmNvbXBvbmVudC5sZXNzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUVBO0VBQ0UsaUJBQUE7O0FBRUY7RUFDRSxnQkFBQTs7QUFFRjtFQUNFLGNBQUE7O0FBRUY7RUFDRSxrQkFBQTs7QUFJRSxpQkFERixZQUNHO0VBQ0MsZUFBQTs7QUFJTiwwQkFBMkIsR0FBRyxFQUFDO0VBQzdCLGVBQUE7O0FBQ0EsMEJBRnlCLEdBQUcsRUFBQyxTQUU1QixNQUNDO0VBQ0Usc0JBQUE7RUFDQSxrQkFBQTtFQUNBLFdBQUEiLCJzb3VyY2VzQ29udGVudCI6WyJAaW1wb3J0IChyZWZlcmVuY2UpIFwiLi4vLi4vYXNzZXRzL3N0eWxlc2hlZXRzL3BhdHRlcm5mbHktbmdcIjtcblxuLnBmbmctd2l6YXJkLWNhbmNlbC1pbmxpbmUge1xuICBtYXJnaW4tbGVmdDogMjVweDtcbn1cbi5wZm5nLXdpemFyZC1mb290ZXItaW5saW5lIHtcbiAgdGV4dC1hbGlnbjogbGVmdDtcbn1cbi5wZm5nLXdpemFyZC1tYWluIHtcbiAgbWFyZ2luLWxlZnQ6IDA7XG59XG4ucGZuZy13aXphcmQtcG9zaXRpb24tb3ZlcnJpZGUge1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG59XG4ud2l6YXJkLXBmLWZvb3RlciB7XG4gIC5idG4tY2FuY2VsIHtcbiAgICAmLndpemFyZC1wZi1jYW5jZWwtbm8tYmFjayB7XG4gICAgICBtYXJnaW4tcmlnaHQ6IDA7XG4gICAgfVxuICB9XG59XG4ud2l6YXJkLXBmLXN0ZXBzLWluZGljYXRvciBsaSBhLmRpc2FibGVkIHtcbiAgY3Vyc29yOiBkZWZhdWx0O1xuICAmOmhvdmVyIHtcbiAgICAud2l6YXJkLXBmLXN0ZXAtbnVtYmVyIHtcbiAgICAgIGJhY2tncm91bmQtY29sb3I6IEBjb2xvci1wZi13aGl0ZTtcbiAgICAgIGJvcmRlci1jb2xvcjogQGNvbG9yLXBmLWJsYWNrLTQwMDtcbiAgICAgIGNvbG9yOiBAY29sb3ItcGYtYmxhY2stNDAwO1xuICAgIH1cbiAgfVxufVxuIl0sImZpbGUiOiJhcHAvd2l6YXJkL3dpemFyZC5jb21wb25lbnQuY3NzIn0= */ diff --git a/src/app/wizard/wizard.component.html b/src/app/wizard/wizard.component.html new file mode 100644 index 000000000..5d1b9c670 --- /dev/null +++ b/src/app/wizard/wizard.component.html @@ -0,0 +1,59 @@ + + + diff --git a/src/app/wizard/wizard.component.less b/src/app/wizard/wizard.component.less new file mode 100644 index 000000000..fdce54b86 --- /dev/null +++ b/src/app/wizard/wizard.component.less @@ -0,0 +1,31 @@ +@import (reference) "../../assets/stylesheets/patternfly-ng"; + +.pfng-wizard-cancel-inline { + margin-left: 25px; +} +.pfng-wizard-footer-inline { + text-align: left; +} +.pfng-wizard-main { + margin-left: 0; +} +.pfng-wizard-position-override { + position: relative; +} +.wizard-pf-footer { + .btn-cancel { + &.wizard-pf-cancel-no-back { + margin-right: 0; + } + } +} +.wizard-pf-steps-indicator li a.disabled { + cursor: default; + &:hover { + .wizard-pf-step-number { + background-color: @color-pf-white; + border-color: @color-pf-black-400; + color: @color-pf-black-400; + } + } +} diff --git a/src/app/wizard/wizard.component.spec.ts b/src/app/wizard/wizard.component.spec.ts new file mode 100644 index 000000000..2f2314fff --- /dev/null +++ b/src/app/wizard/wizard.component.spec.ts @@ -0,0 +1,37 @@ +import { + async, + ComponentFixture, + TestBed +} from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { FormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; + +import { WizardComponent } from './wizard.component'; + +describe('Wizard component - ', () => { + let comp: WizardComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserAnimationsModule, + FormsModule + ], + declarations: [ + WizardComponent + ] + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(WizardComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + })); + +}); diff --git a/src/app/wizard/wizard.component.ts b/src/app/wizard/wizard.component.ts new file mode 100644 index 000000000..5f3ab64c3 --- /dev/null +++ b/src/app/wizard/wizard.component.ts @@ -0,0 +1,394 @@ +import { + Component, + EventEmitter, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; + +import { WizardBase } from './wizard-base'; +import { WizardConfig } from './wizard-config'; +import { WizardEvent } from './wizard-event'; +import { WizardStep } from './wizard-step'; + +import { cloneDeep, defaults, isEqual } from 'lodash'; + +/** + * Wizard component + */ +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'pfng-wizard', + styleUrls: ['./wizard.component.less'], + templateUrl: './wizard.component.html' +}) +export class WizardComponent extends WizardBase implements OnInit { + /** + * The wizard config containing component properties + */ + @Input() config: WizardConfig; + + /** + * The event emitted when the cancel button has been selected + */ + @Output('onCancel') onCancel = new EventEmitter(); + + /** + * The event emitted when all wizard steps and substeps have finished + */ + @Output('onFinish') onFinish = new EventEmitter(); + + /** + * The event emitted when the next button has been selected + */ + @Output('onNext') onNext = new EventEmitter(); + + /** + * The event emitted when the back button has been selected + */ + @Output('onPrevious') onPrevious = new EventEmitter(); + + /** + * The event emitted when a step has changed + */ + @Output('onStepChange') onStepChange = new EventEmitter(); + + private contentStyle: any; + private defaultConfig = { + cancelTitle: 'Cancel', + done: false, + contentHeight: '300px', + embedInPage: false, + hideIndicators: false, + hideSidebar: false, + hideHeader: false, + hidePreviousButton: false, + nextTitle: 'Next >', + previousTitle: '< Back', + ready: true + } as WizardConfig; + private init: boolean = true; + private _firstStep: boolean = false; + private prevConfig: WizardConfig; + + /** + * The default constructor + */ + constructor() { + super(); + } + + // Initialization + + /** + * Setup component configuration upon initialization + */ + ngOnInit(): void { + this.setupConfig(); + if (this.init && this.config.ready) { + setTimeout(() => { + this.initFirstStep(); + }, 10); + } + } + + /** + * Check if the component config has changed + */ + ngDoCheck(): void { + // Do a deep compare on config + if (!isEqual(this.config, this.prevConfig)) { + this.setupConfig(); + } + } + + /** + * Set up default config + */ + protected setupConfig(): void { + if (this.config !== undefined) { + defaults(this.config, this.defaultConfig); + } else { + this.config = cloneDeep(this.defaultConfig); + } + + // If a step class is given use it for all steps + if (this.config.stepStyleClass !== undefined) { + // If a sidebarStyleClass is given, us it for sidebar panel, if not, apply the stepsClass to the sidebar panel + if (this.config.sidebarStyleClass === undefined) { + this.config.sidebarStyleClass = this.config.stepStyleClass; + } + } else { + this.contentStyle = { + 'height': this.config.contentHeight, + 'max-height': this.config.contentHeight, + 'overflow-y': 'auto' + }; + } + + // Ready state changed + if (this.prevConfig !== undefined && !isEqual(this.config.ready, this.prevConfig.ready)) { + this.initFirstStep(); + } + this.prevConfig = cloneDeep(this.config); + } + + // Accessors + + /** + * Indicates that the selected step is also the first wizard step or substep + * + * @returns {boolean} True if the selected step is the first wizard step or substep + */ + get firstStep(): boolean { + return this._firstStep; + } + + /** + * Set a flag indicating that the selected step is also the first wizard step or substep + * + * @param {boolean} firstStep True if the selected step is the first wizard step or substep + */ + set firstStep(firstStep: boolean) { + this._firstStep = firstStep; + } + + // Methods + + /** + * Add a wizard step or substep to this component + * + * @param {WizardStep} step The wizard step or substep + */ + addStep(step: WizardStep): void { + super.addStep(step) + + let enabledSteps: WizardStep[] = this.getEnabledSteps(); + if (this.config.ready && (enabledSteps.length > 0) && (step === enabledSteps[0])) { + this.goTo(enabledSteps[0], true, false); + } + }; + + /** + * Returns only wizard steps with review templates + * + * @returns {WizardStep[]} The wizard stepd or substepd + */ + getReviewSteps(): WizardStep[] { + let reviewSteps = this.getEnabledSteps().filter((step: WizardStep) => { + return (step.reviewTemplate !== undefined || step.getReviewSteps().length > 0); + }); + return reviewSteps; + } + + /** + * Navigate to the next wizard step or substep + */ + goToNextStep(): void { + this.next(false); + } + + /** + * Navigate to the previous wizard step or substep + */ + goToPreviousStep(): void { + this.previous(false); + } + + /** + * Navigate to the given wizard step index + * + * @param {number} stepIndex The step number to navigate to + * @param {boolean} resetStepNav True if the first substep (if exists) should be selected + */ + goToStep(stepIndex: number, resetStepNav: boolean): void { + let enabledSteps: WizardStep[] = this.getEnabledSteps(); + if (stepIndex < enabledSteps.length) { + this.goTo(enabledSteps[stepIndex], resetStepNav, false); + } + } + + /** + * Called when the next button has been selected. + * + * @param {boolean} emitEvent True to emit the onNext event + */ + next(emitEvent: boolean): void { + let enabledSteps: WizardStep[] = this.getEnabledSteps(); + + // Save the step you were on when next() was invoked + let index = this.stepIndex(this.selectedStep); + + let wizEvent = { + index: index, + step: this.selectedStep + } as WizardEvent; + + if (this.selectedStep.hasSubsteps) { + // Handle navigation in substep + if (this.selectedStep.next(emitEvent)) { + return; + } + } else { + if (emitEvent !== false) { + this.onNext.emit(wizEvent); + } + } + + // Set completed property, which may be used to add/remove a style class from progress bar + this.selectedStep.config.completed = true; + + // Ensure this is not the last step + if (index === enabledSteps.length - 1) { + this.finish(); + } else { + this.goTo(enabledSteps[index + 1], true, false); + } + } + + /** + * Called when the previous button has been selected. + * + * @param {boolean} emitEvent True to emit the onNext event + */ + previous(emitEvent: boolean): void { + let index = this.stepIndex(this.selectedStep); + + let wizEvent = { + index: index, + step: this.selectedStep + } as WizardEvent; + + if (this.selectedStep.hasSubsteps) { + // Handle navigation in substep + if (this.selectedStep.previous(emitEvent)) { + return; + } + } else { + if (emitEvent !== false) { + this.onPrevious.emit(wizEvent); + } + } + + // Ensure this is not the first step + if (index === 0) { + throw new Error("Can't go back. Already at first step"); + } else { + this.goTo(this.getEnabledSteps()[index - 1], false, true); + } + } + + /** + * Emits an event when the wizard step or substep has changed + * + * @param {WizardStep} step The wizard step or substep + * @param {number} index The order of the wizard step of substep within its parent + */ + stepChanged(step: WizardStep, index: number): void { + this.onStepChange.emit({ + index: index, + step: step + } as WizardEvent); + } + + /** + * Set a flag indicating that the selected step is also the first wizard step or substep + * + * @param {number} stepIndex The step index + */ + updateStepIndex(stepIndex: number): void { + this.firstStep = this.stepIndex(this.selectedStep) === 0 && stepIndex === 0; + }; + + // Private + + // Indicates that the user can click on numeric step indicators to navigate directly to a step + private allowStepIndicatorClick(step: WizardStep): boolean { + if (step === undefined || this.selectedStep === undefined) { + return false; + } + return !this.config.done + && step.config.allowClickNav + && this.selectedStep.config.allowNavAway + && (this.selectedStep.config.nextEnabled || (step.config.priority < this.selectedStep.config.priority)) + && (this.selectedStep.config.previousEnabled || (step.config.priority > this.selectedStep.config.priority)); + } + + // Emits an event inidcating that the cancel button has been selected + private cancel(): void { + this.onCancel.emit({ + index: this.stepIndex(this.selectedStep), + step: this.selectedStep + } as WizardEvent); + this.reset(); + } + + // Emits an event inidcating that all wizard steps and substeps have finished + private finish(): void { + this.onFinish.emit({ + index: this.stepIndex(this.selectedStep), + step: this.selectedStep + } as WizardEvent); + this.reset(); + } + + // Navigate to the given substep + private goTo(step: WizardStep, goToFirstSubstep: boolean, goToLastSubstep: boolean): void { + if (step === undefined || this.config.done + || (!this.init && this.selectedStep !== undefined && !this.selectedStep.config.allowNavAway)) { + return; + } + if (this.init || (this.getStepIndex(step) < this.selectedStepIndex && this.selectedStep.previousEnabled) + || this.selectedStep.nextEnabled) { + this.unselectAll(); + if (step.hasSubsteps && goToFirstSubstep) { + step.goToFirstStep(); + } else if (step.hasSubsteps && goToLastSubstep) { + step.goToLastStep(); + } else { + step.show(this.stepIndex(step)); + this.stepChanged(step, this.stepIndex(step)); + } + this.selectedStep = step; + step.selected = true; + } + + if (!this.selectedStep.hasSubsteps) { + this.firstStep = this.stepIndex(this.selectedStep) === 0; + } else { + this.firstStep = this.stepIndex(this.selectedStep) === 0 && this.selectedStep.selectedStepIndex === 1; + } + } + + // Initializes the first step based on the ready state and whether a current step has been provided + private initFirstStep(): void { + // Set currentStep equal to selected step title + if (this.config !== undefined && this.config.currentStep !== undefined + && !isEqual(this.config.currentStep, this.prevConfig.currentStep) + && (this.selectedStep !== undefined && this.selectedStep.config.title !== this.config.currentStep)) { + this.goTo(this.stepByTitle(this.config.currentStep), true, false); + } else { + let enabledSteps: WizardStep[] = this.getEnabledSteps(); + this.goTo(enabledSteps[0], true, false); + } + this.init = false; + } + + // Reset wizard state + private reset(): void { + // Traverse steps array and set each "completed" property to false + this.getEnabledSteps().forEach((step: WizardStep) => { + step.config.completed = false; + }); + // Go to first step + this.goToStep(0, true); + } + + // Handle step navigation + private stepClick(step: WizardStep): void { + if (step.config.allowClickNav) { + this.goTo(step, true, false); + } + } +} diff --git a/src/app/wizard/wizard.module.ts b/src/app/wizard/wizard.module.ts new file mode 100644 index 000000000..16775d8bd --- /dev/null +++ b/src/app/wizard/wizard.module.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TooltipConfig, TooltipModule } from 'ngx-bootstrap/tooltip'; + +import { WizardBase } from './wizard-base'; +import { WizardComponent } from './wizard.component'; +import { WizardConfig } from './wizard-config'; +import { WizardEvent } from './wizard-event'; +import { WizardReviewComponent } from './wizard-review.component'; +import { WizardStep } from './wizard-step'; +import { WizardStepComponent } from './wizard-step.component'; +import { WizardStepConfig } from './wizard-step-config'; +import { WizardSubstepComponent } from './wizard-substep.component'; + +export { + WizardBase, + WizardConfig, + WizardEvent, + WizardStep, + WizardStepConfig +} + +/** + * A module containing objects associated with the wizard component + */ +@NgModule({ + imports: [ + CommonModule, + TooltipModule.forRoot() + ], + declarations: [ WizardComponent, WizardReviewComponent, WizardStepComponent, WizardSubstepComponent ], + exports: [ WizardComponent, WizardReviewComponent, WizardStepComponent, WizardSubstepComponent ], + providers: [ TooltipConfig ] +}) +export class WizardModule { } diff --git a/src/demo/app-routing.module.ts b/src/demo/app-routing.module.ts index ee713932a..d8e1b4aff 100644 --- a/src/demo/app-routing.module.ts +++ b/src/demo/app-routing.module.ts @@ -13,6 +13,7 @@ import { SortExampleComponent } from '../app/sort/examples/sort-example.componen import { ToastNotificationExampleComponent } from '../app/notification/examples/toast-notification-example.component'; import { ToolbarExampleComponent } from '../app/toolbar/examples/toolbar-example.component'; import { WelcomeComponent } from './components/welcome.component'; +import { WizardExampleComponent } from '../app/wizard/examples/wizard-example.component'; const routes: Routes = [{ path: '', @@ -51,6 +52,9 @@ const routes: Routes = [{ }, { path: 'toolbar', component: ToolbarExampleComponent + }, { + path: 'wizard', + component: WizardExampleComponent }]; @NgModule({ diff --git a/src/demo/app.component.html b/src/demo/app.component.html index df07bcfae..84aeb0612 100644 --- a/src/demo/app.component.html +++ b/src/demo/app.component.html @@ -40,6 +40,9 @@

Components

  • Toolbar
  • +
  • + Wizard +

  • Directives

    diff --git a/src/demo/app.module.ts b/src/demo/app.module.ts index 99afff249..c9fb7d74c 100644 --- a/src/demo/app.module.ts +++ b/src/demo/app.module.ts @@ -22,6 +22,7 @@ import { SearchHighlightExampleModule } from '../app/search-highlight/examples/s import { SortExampleModule } from '../app/sort/examples/sort-example.module'; import { ToolbarExampleModule } from '../app/toolbar/examples/toolbar-example.module'; import { WelcomeComponent } from './components/welcome.component'; +import { WizardExampleModule } from '../app/wizard/examples/wizard-example.module'; @NgModule({ imports: [ @@ -38,7 +39,8 @@ import { WelcomeComponent } from './components/welcome.component'; SampleExampleModule, SearchHighlightExampleModule, SortExampleModule, - ToolbarExampleModule + ToolbarExampleModule, + WizardExampleModule ], declarations: [ AppComponent,