├── .forceignore ├── .gitignore ├── LICENSE ├── README.md ├── lightning-wizard-examples └── main │ └── default │ ├── classes │ ├── ApexValidationWizardController.cls │ ├── ApexValidationWizardController.cls-meta.xml │ ├── ApexValidationWizardControllerTest.cls │ └── ApexValidationWizardControllerTest.cls-meta.xml │ ├── lwc │ ├── .eslintrc.json │ ├── apexValidationWizard │ │ ├── apexValidationWizard.html │ │ ├── apexValidationWizard.js │ │ └── apexValidationWizard.js-meta.xml │ ├── createAccountWithModalWizard │ │ ├── createAccountWithModalWizard.html │ │ ├── createAccountWithModalWizard.js │ │ └── createAccountWithModalWizard.js-meta.xml │ ├── createAccountWizard │ │ ├── createAccountWizard.html │ │ ├── createAccountWizard.js │ │ └── createAccountWizard.js-meta.xml │ ├── jsconfig.json │ └── wizardExamples │ │ ├── wizardExamples.html │ │ ├── wizardExamples.js │ │ └── wizardExamples.js-meta.xml │ └── tabs │ └── LightningWizardExamples.tab-meta.xml ├── lightning-wizard └── main │ └── default │ └── lwc │ ├── .eslintrc.json │ ├── jsconfig.json │ ├── wizard │ ├── wizard.html │ ├── wizard.js │ └── wizard.js-meta.xml │ └── wizardStep │ ├── wizardStep.html │ ├── wizardStep.js │ └── wizardStep.js-meta.xml ├── screenshots └── create-account-flow.gif └── sfdx-project.json /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__tests__/** -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .sfdx 2 | config -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Javier Martínez de Pisson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salesforce Wizard Component 2 | 3 | Flow Example 4 | 5 | ````xml 6 | 24 | ```` 25 | 26 | ## About 27 | 28 | This components aims to provide a way to programatically build flows with Salesforce Flow styles and functionality, having full control of the wizard behavior in any scenario. 29 | 30 | ### Features 31 | 32 | * Define wizard with Lightning Web components declaratively. 33 | * Override standard Navigation buttons definining specific step actions. 34 | * Customize flow using JavaScript function for step validation and post-processing. 35 | * Modify the flow steps using standard LWC templates. 36 | 37 | 38 | ## Description 39 | A `c-wizard` display a guided flow with multiple steps, only one is visible at a time. The progress is shown on the header along with a current step indicator. 40 | 41 | 42 | 43 | 44 | ## Specification 45 | 46 | ### c-wizard 47 | 48 | #### Attributes 49 | 50 | | Name | Type | Access | Required | Default | Description | 51 | |:---------------:|:------:|:------:|:--------:|:--------:|---------------------------------------------------------------------------------------------| 52 | | variant | string | global | | base | Wizard style. Valid values are base, base-shaded and path. | 53 | | previous-label | string | global | | Previous | Previous button label. | 54 | | next-label | string | global | | Next | Next button label. | 55 | | finish-label | string | global | | Finish | Finish button label. | 56 | | header | string | global | | | Header text shown on wizard. Leave blank for not displaying header. | 57 | | current-step | string | global | | | Sets the current step of the wizard. Defaults to first c-wizard-step on the markup if null. | 58 | 59 | #### Slots 60 | 61 | | Name | Description | 62 | |:---------------:|:---------------:| 63 | | header | Placeholder for wizard header. Overrides the header attribute set on the component.| 64 | | default | Placeholder for c-wizard-step. Defines the wizard flow.| 65 | 66 | #### Custom Events 67 | 68 | ##### change 69 | 70 | The event fired when the wizard advances or goes back following the configured step flow. An external change by setting the attribute current-step does not emit this event. 71 | 72 | The change event returns the following parameters. 73 | 74 | |Parameter | Type | Description | 75 | |:------:|:--------:|:--------:| 76 | | currentStep | string | The step name the wizard is moving to.| 77 | | oldStep | string | The step name the wizard is moving from| 78 | 79 | The change event properties are as follows. 80 | 81 | |Property | Value | Description| 82 | |:------:|:--------:|:--------:| 83 | |bubbles|false|This event does not bubble up through the DOM.| 84 | |cancelable|false|This event has no default behavior that can be canceled. You can't call preventDefault() on this event.| 85 | |composed|false|This event does not propagate outside of the component in which it was dispatched.| 86 | 87 | ##### complete 88 | 89 | The event fired when the wizard finishes and the user clicks on Finish button. 90 | 91 | The complete event properties are as follows. 92 | 93 | |Property | Value | Description | 94 | |:------:|:--------:|:--------:| 95 | |bubbles|false|This event does not bubble up through the DOM.| 96 | |cancelable|false|This event has no default behavior that can be canceled. You can't call preventDefault() on this event.| 97 | |composed|false|This event does not propagate outside of the component in which it was dispatched.| 98 | 99 | ### c-wizard-step 100 | 101 | #### Attributes 102 | 103 | | Name | Type | Access | Required | Default | Description | 104 | |:---------------:|:------:|:------:|:--------:|:--------:|---------------------------------------------------------------------------------------------| 105 | | name | string | global | true | | Step unique name. Identifies the step. | 106 | | label | string | global | true | | Step label shown on wizard progress. | 107 | | hide-previous-button | Boolean | global | | false |Hides the Previous button on this step. | 108 | | hide-next-button | string | global | | false | Hides the Next/Finish button on this step. | 109 | | before-change | function | global | | | Custom function to execute to perform post-processing action before advancing to the next step. It should return a promise with a true/false; if resolved with a falsy value, the wizard will mark the step as error and will not advance to the next step. 110 | 111 | #### Slots 112 | 113 | | Name | Description | 114 | |:---------------:|:---------------:| 115 | | actions | Placeholder for actionable components on the step such as lightning-button. The components are positioned next to Next button. Overrides the header attribute set on the component.| 116 | | default | Placeholder for step content.| -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/classes/ApexValidationWizardController.cls: -------------------------------------------------------------------------------- 1 | public with sharing class ApexValidationWizardController 2 | { 3 | 4 | @AuraEnabled 5 | public static Id saveAccount(Account newAccount) 6 | { 7 | try 8 | { 9 | insert newAccount; 10 | 11 | return newAccount.Id; 12 | } 13 | catch (Exception error) 14 | { 15 | throw new AuraHandledException(error.getMessage()); 16 | } 17 | } 18 | 19 | @AuraEnabled 20 | public static void validateName(String accountName) 21 | { 22 | try 23 | { 24 | if(!Pattern.matches('[A-z].*', accountName)) 25 | { 26 | throw new ApexValidationWizardControllerException('The Account Name should only contain characters.'); 27 | } 28 | } 29 | catch (Exception error) 30 | { 31 | throw new AuraHandledException(error.getMessage()); 32 | } 33 | } 34 | 35 | private class ApexValidationWizardControllerException extends Exception {} 36 | } 37 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/classes/ApexValidationWizardController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/classes/ApexValidationWizardControllerTest.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | private class ApexValidationWizardControllerTest 3 | { 4 | @isTest 5 | static void test_saveAccount_OK() 6 | { 7 | // When 8 | Account testAccount = new Account( Name = 'Test'); 9 | 10 | // Test 11 | Test.startTest(); 12 | 13 | ApexValidationWizardController.saveAccount(testAccount); 14 | 15 | Test.stoptest(); 16 | 17 | // Then 18 | List accounts = [SELECT Id, Name FROM Account LIMIT 1]; 19 | 20 | System.assertEquals(false, accounts.isEmpty()); 21 | System.assertEquals('Test', accounts[0].Name); 22 | } 23 | 24 | @isTest 25 | static void test_saveAccount_null() 26 | { 27 | // When 28 | Account testAccount = null; 29 | 30 | // Test 31 | Test.startTest(); 32 | 33 | AuraHandledException testedError; 34 | 35 | try 36 | { 37 | ApexValidationWizardController.saveAccount(testAccount); 38 | } 39 | catch(AuraHandledException error) 40 | { 41 | testedError = error; 42 | } 43 | 44 | Test.stoptest(); 45 | 46 | // Then 47 | List accounts = [SELECT Id, Name FROM Account LIMIT 1]; 48 | 49 | System.assertEquals(true, accounts.isEmpty()); 50 | System.assertNotEquals(null, testedError); 51 | } 52 | 53 | @isTest 54 | static void test_validateName_OK() 55 | { 56 | // Test 57 | Test.startTest(); 58 | 59 | AuraHandledException testedError; 60 | 61 | try 62 | { 63 | ApexValidationWizardController.validateName('Test'); 64 | } 65 | catch(AuraHandledException error) 66 | { 67 | testedError = error; 68 | } 69 | 70 | Test.stoptest(); 71 | 72 | // Then 73 | System.assertEquals(null, testedError); 74 | } 75 | 76 | @isTest 77 | static void test_validateName_NOK() 78 | { 79 | Test.startTest(); 80 | 81 | AuraHandledException testedError; 82 | 83 | try 84 | { 85 | ApexValidationWizardController.validateName('012'); 86 | } 87 | catch(AuraHandledException error) 88 | { 89 | testedError = error; 90 | } 91 | 92 | 93 | Test.stoptest(); 94 | 95 | // Then 96 | System.assertNotEquals(null, testedError); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/classes/ApexValidationWizardControllerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@salesforce/eslint-config-lwc/recommended" 3 | } -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/apexValidationWizard/apexValidationWizard.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/apexValidationWizard/apexValidationWizard.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, track } from 'lwc'; 2 | import { createRecord } from 'lightning/uiRecordApi'; 3 | import { ShowToastEvent } from 'lightning/platformShowToastEvent'; 4 | import { NavigationMixin } from 'lightning/navigation'; 5 | import saveAccount from '@salesforce/apex/ApexValidationWizardController.saveAccount'; 6 | import validateName from '@salesforce/apex/ApexValidationWizardController.validateName'; 7 | 8 | 9 | export default class ApexValidationWizard extends NavigationMixin(LightningElement) { 10 | 11 | /** 12 | * Takes the filled form and creates a new Account Record 13 | * @author jmartinezpisson 14 | */ 15 | saveAccount() { 16 | const accountRecord = this.createAccountRecord(); 17 | 18 | // 3 - Create the account using an Apex method 19 | // and show a toast message with the result 20 | saveAccount({ newAccount: accountRecord }).then(accountId => { 21 | this.dispatchEvent( 22 | new ShowToastEvent({ 23 | title: 'Success', 24 | message: `Account created, the Id is ${accountId}`, 25 | variant: 'success', 26 | }), 27 | ); 28 | 29 | this[NavigationMixin.Navigate]({ 30 | type: 'standard__recordPage', 31 | attributes: { 32 | recordId: accountId, 33 | objectApiName: 'Account', 34 | actionName: 'view' 35 | 36 | } 37 | }); 38 | }).catch(error => { 39 | this.dispatchEvent( 40 | new ShowToastEvent({ 41 | title: 'Error creating record', 42 | message: error.body.message, 43 | variant: 'error', 44 | }) 45 | ); 46 | }); 47 | } 48 | 49 | createAccountRecord() { 50 | const accountRecord = {}; 51 | 52 | // 1 - Get current form inputs 53 | let inputs = this.template.querySelectorAll('lightning-input'); 54 | 55 | // 2 - Loop the input list and get every input value and assign it to the desired field 56 | // The field name is set on every input dataset as the attribute "field" 57 | inputs.forEach(input => { 58 | accountRecord[input.dataset.fieldName] = input.value; 59 | }); 60 | 61 | return accountRecord; 62 | } 63 | 64 | /** 65 | * Validates the form, checking for lightning-input errors and 66 | * controlling that wizard should advance to the next step 67 | * 68 | * @author jmartinezpisson 69 | */ 70 | async validate() { 71 | // 1 - Takes all the inputs from the step - "this" is bind to wizard-step component 72 | const allValid = [...this.querySelectorAll('lightning-input')] 73 | .reduce((validSoFar, inputCmp) => { 74 | inputCmp.setCustomValidity(''); 75 | inputCmp.reportValidity(); 76 | return validSoFar && inputCmp.checkValidity(); 77 | }, true); 78 | 79 | // 2 - Calls an Apex Validation method to validate the Account Name. 80 | // If the method throws an exception, shows the message on the Input 81 | // Stops the wizard by returninng false. 82 | if(allValid) { 83 | let accountNameInput = this.querySelector('lightning-input[data-field-name="Name"]'); 84 | 85 | if(accountNameInput) { 86 | try { 87 | await validateName({ accountName: accountNameInput.value }); 88 | } catch(error) { 89 | accountNameInput.setCustomValidity(error.body.message); 90 | accountNameInput.reportValidity(); 91 | 92 | return false; 93 | } 94 | } 95 | } 96 | 97 | return allValid; 98 | } 99 | } -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/apexValidationWizard/apexValidationWizard.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51.0 4 | false 5 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/createAccountWithModalWizard/createAccountWithModalWizard.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/createAccountWithModalWizard/createAccountWithModalWizard.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, track } from 'lwc'; 2 | import { createRecord } from 'lightning/uiRecordApi'; 3 | import { ShowToastEvent } from 'lightning/platformShowToastEvent'; 4 | import { NavigationMixin } from 'lightning/navigation'; 5 | import ACCOUNT_OBJECT from '@salesforce/schema/Account'; 6 | import NAME_FIELD from '@salesforce/schema/Account.Name'; 7 | 8 | 9 | export default class CreateAccountWithModalWizard extends NavigationMixin(LightningElement) { 10 | 11 | 12 | /** 13 | * Shows/Hides the modal from the first step 14 | * @author jmpisson 15 | */ 16 | @track 17 | isModalShown = false; 18 | 19 | /** 20 | * Takes the filled form and creates a new Account Record 21 | * @author jmpisson 22 | */ 23 | saveAccount() { 24 | // 0 - Define the fields object and the current record to save 25 | const fields = {}; 26 | const accountRecord = { apiName: ACCOUNT_OBJECT.objectApiName, fields }; 27 | 28 | // 1 - Get current form inputs 29 | let inputs = this.template.querySelectorAll('lightning-input'); 30 | 31 | // 2 - Loop the input list and get every input value and assign it to the desired field 32 | // The field name is set on every input dataset as the attribute "field" 33 | inputs.forEach(input => { 34 | fields[input.dataset.fieldName] = input.value; 35 | }); 36 | 37 | // 3 - Create the account using Lightning UI Record API 38 | // and show a toast message with the result 39 | createRecord(accountRecord).then(account => { 40 | this.dispatchEvent( 41 | new ShowToastEvent({ 42 | title: 'Success', 43 | message: `Account created, the Id is ${account.id}`, 44 | variant: 'success', 45 | }), 46 | ); 47 | 48 | this[NavigationMixin.Navigate]({ 49 | type: 'standard__recordPage', 50 | attributes: { 51 | recordId: account.id, 52 | objectApiName: 'Account', 53 | actionName: 'view' 54 | 55 | } 56 | }); 57 | }).catch(error => { 58 | this.dispatchEvent( 59 | new ShowToastEvent({ 60 | title: 'Error creating record', 61 | message: error.body.message, 62 | variant: 'error', 63 | }) 64 | ); 65 | }); 66 | } 67 | 68 | /** 69 | * Opens the modal 70 | * 71 | * @author jmpisson 72 | */ 73 | openModal() { 74 | this.isModalShown = true; 75 | } 76 | 77 | /** 78 | * Opens the modal 79 | * 80 | * @author jmpisson 81 | */ 82 | closeModal() { 83 | this.isModalShown = false; 84 | } 85 | 86 | /** 87 | * Validates the form and goes to the next step 88 | * 89 | * @author jmpisson 90 | */ 91 | next() { 92 | if(this.validate()) { 93 | this.template.querySelector('c-wizard').currentStep = 'step-2'; 94 | } 95 | 96 | this.closeModal(); 97 | } 98 | 99 | 100 | /** 101 | * Validates the form, checking for lightning-input errors and 102 | * controlling that wizard should advance to the next step 103 | * 104 | * @author jmpisson 105 | */ 106 | validate() { 107 | // 1 - Takes all the inputs from the step - "this" is bind to wizard-step component 108 | const allValid = [...this.querySelectorAll('lightning-input')] 109 | .reduce((validSoFar, inputCmp) => { 110 | inputCmp.reportValidity(); 111 | return validSoFar && inputCmp.checkValidity(); 112 | }, true); 113 | 114 | // 2 - Returns true/false; if the validation were asynchronous, it should return a Promise instead 115 | return allValid; 116 | } 117 | } -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/createAccountWithModalWizard/createAccountWithModalWizard.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51.0 4 | true 5 | false 6 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/createAccountWizard/createAccountWizard.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/createAccountWizard/createAccountWizard.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, track } from 'lwc'; 2 | import { createRecord } from 'lightning/uiRecordApi'; 3 | import { ShowToastEvent } from 'lightning/platformShowToastEvent'; 4 | import { NavigationMixin } from 'lightning/navigation'; 5 | import ACCOUNT_OBJECT from '@salesforce/schema/Account'; 6 | import NAME_FIELD from '@salesforce/schema/Account.Name'; 7 | 8 | 9 | export default class TestLWCWizard extends NavigationMixin(LightningElement) { 10 | 11 | /** 12 | * Takes the filled form and creates a new Account Record 13 | * @author jmartinezpisson 14 | */ 15 | saveAccount() { 16 | // 0 - Define the fields object and the current record to save 17 | const fields = {}; 18 | const accountRecord = { apiName: ACCOUNT_OBJECT.objectApiName, fields }; 19 | 20 | // 1 - Get current form inputs 21 | let inputs = this.template.querySelectorAll('lightning-input'); 22 | 23 | // 2 - Loop the input list and get every input value and assign it to the desired field 24 | // The field name is set on every input dataset as the attribute "field" 25 | inputs.forEach(input => { 26 | fields[input.dataset.fieldName] = input.value; 27 | }); 28 | 29 | // 3 - Create the account using Lightning UI Record API 30 | // and show a toast message with the result 31 | createRecord(accountRecord).then(account => { 32 | this.dispatchEvent( 33 | new ShowToastEvent({ 34 | title: 'Success', 35 | message: `Account created, the Id is ${account.id}`, 36 | variant: 'success', 37 | }), 38 | ); 39 | 40 | this[NavigationMixin.Navigate]({ 41 | type: 'standard__recordPage', 42 | attributes: { 43 | recordId: account.id, 44 | objectApiName: 'Account', 45 | actionName: 'view' 46 | 47 | } 48 | }); 49 | }).catch(error => { 50 | this.dispatchEvent( 51 | new ShowToastEvent({ 52 | title: 'Error creating record', 53 | message: error.body.message, 54 | variant: 'error', 55 | }) 56 | ); 57 | }); 58 | } 59 | 60 | /** 61 | * Validates the form, checking for lightning-input errors and 62 | * controlling that wizard should advance to the next step 63 | * 64 | * @author jmartinezpisson 65 | */ 66 | validate() { 67 | // 1 - Takes all the inputs from the step - "this" is bind to wizard-step component 68 | const allValid = [...this.querySelectorAll('lightning-input')] 69 | .reduce((validSoFar, inputCmp) => { 70 | inputCmp.reportValidity(); 71 | return validSoFar && inputCmp.checkValidity(); 72 | }, true); 73 | 74 | // 2 - Returns true/false; if the validation were asynchronous, it should return a Promise instead 75 | return allValid; 76 | } 77 | } -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/createAccountWizard/createAccountWizard.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51.0 4 | false 5 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "baseUrl": ".", 5 | "paths": { 6 | "c/autocompleteCombobox": [ 7 | "autocompleteCombobox/autocompleteCombobox.js" 8 | ], 9 | "c/lwc2Pdf": [ 10 | "lwc2Pdf/lwc2Pdf.js" 11 | ], 12 | "c/testLWCWizard": [ 13 | "testLWCWizard/testLWCWizard.js" 14 | ], 15 | "c/wizard": [ 16 | "wizard/wizard.js" 17 | ], 18 | "c/wizardAction": [ 19 | "wizardAction/wizardAction.js" 20 | ], 21 | "c/wizardStep": [ 22 | "wizardStep/wizardStep.js" 23 | ] 24 | } 25 | }, 26 | "include": [ 27 | "**/*", 28 | "../../../../.sfdx/typings/lwc/**/*.d.ts" 29 | ], 30 | "typeAcquisition": { 31 | "include": [ 32 | "jest" 33 | ] 34 | }, 35 | "paths": { 36 | "c/*": [ 37 | "*" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/wizardExamples/wizardExamples.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/wizardExamples/wizardExamples.js: -------------------------------------------------------------------------------- 1 | import { LightningElement } from 'lwc'; 2 | 3 | export default class WizardExamples extends LightningElement {} -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/lwc/wizardExamples/wizardExamples.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51.0 4 | true 5 | Lightning Wizard Examples 6 | 7 | lightning__Tab 8 | 9 | -------------------------------------------------------------------------------- /lightning-wizard-examples/main/default/tabs/LightningWizardExamples.tab-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Wizard Examples developed with Lightning Wizard 4 | 5 | wizardExamples 6 | Custom83: Pencil 7 | 8 | -------------------------------------------------------------------------------- /lightning-wizard/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@salesforce/eslint-config-lwc/recommended" 3 | } -------------------------------------------------------------------------------- /lightning-wizard/main/default/lwc/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "baseUrl": ".", 5 | "paths": { 6 | "c/autocompleteCombobox": [ 7 | "autocompleteCombobox/autocompleteCombobox.js" 8 | ], 9 | "c/lwc2Pdf": [ 10 | "lwc2Pdf/lwc2Pdf.js" 11 | ], 12 | "c/testLWCWizard": [ 13 | "testLWCWizard/testLWCWizard.js" 14 | ], 15 | "c/wizard": [ 16 | "wizard/wizard.js" 17 | ], 18 | "c/wizardAction": [ 19 | "wizardAction/wizardAction.js" 20 | ], 21 | "c/wizardStep": [ 22 | "wizardStep/wizardStep.js" 23 | ] 24 | } 25 | }, 26 | "include": [ 27 | "**/*", 28 | "../../../../.sfdx/typings/lwc/**/*.d.ts" 29 | ], 30 | "typeAcquisition": { 31 | "include": [ 32 | "jest" 33 | ] 34 | }, 35 | "paths": { 36 | "c/*": [ 37 | "*" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lightning-wizard/main/default/lwc/wizard/wizard.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lightning-wizard/main/default/lwc/wizard/wizard.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api, track } from 'lwc'; 2 | 3 | export default class Wizard extends LightningElement { 4 | 5 | // #region public API properties 6 | @api variant = 'base'; 7 | @api previousLabel = 'Previous'; 8 | @api nextLabel = 'Next'; 9 | @api finishLabel = 'Finish'; 10 | @api header = ''; 11 | 12 | @track _currentStep = null; 13 | @api get currentStep() { 14 | return this._currentStep; 15 | } 16 | 17 | set currentStep(value) { 18 | this.setAttribute('current-step', value); 19 | this._currentStep = value; 20 | this.setActiveStep(); 21 | } 22 | 23 | // #endregion 24 | 25 | // #region Tracked Properties 26 | 27 | @track steps = {}; 28 | @track hasError = false; 29 | @track errorMessages = ''; 30 | @track flow = []; 31 | 32 | // #endregion 33 | 34 | // #region Non-tracked Properties 35 | 36 | isInit = false; 37 | progressIndicatorType = 'base'; 38 | progressIndicatorVariant = 'base'; 39 | 40 | //#endregion 41 | 42 | // #region LWC Licecycle Callbacks 43 | 44 | connectedCallback() { 45 | this.init(); 46 | } 47 | 48 | errorCallback(error, stack) { 49 | this.hasError = true; 50 | this.errorMessages = error + ' ' + stack; 51 | } 52 | 53 | // #endregion 54 | 55 | // #region Event Handlers 56 | 57 | /** 58 | * Handles changes on the slot body, which allows to configure the internal component stated base 59 | * on the c-wizard-step children. 60 | */ 61 | slotChange() { 62 | this.configSteps(); 63 | this.setActiveStep(); 64 | } 65 | 66 | /** 67 | * Register a wizard step defined in component template 68 | * 69 | * @param {CustomEvent} event 70 | * @param {Object} event.detail 71 | * @param {*} event.detail.for Defines a list of steps on which this action will be available 72 | * @param {Object} event.detail.methods WizardAction Private API 73 | * @param {Fuction} event.detail.methods.setActive Marks the step as current 74 | */ 75 | registerStep(event) { 76 | var step = event.detail; 77 | this.steps[event.detail.name] = step; 78 | 79 | step.methods.config({ 80 | labels: { 81 | next: this.nextLabel, 82 | previous: this.previousLabel, 83 | finish: this.finishLabel 84 | }, 85 | callbacks: { 86 | unregister: this.unregisterStep.bind(this), 87 | move: this.moveStep.bind(this) 88 | } 89 | }); 90 | } 91 | // #endregion 92 | 93 | // #region Private Methods 94 | 95 | /** 96 | * Initializes the component, applying the global style. 97 | */ 98 | init() { 99 | if (this.isInit) { 100 | return; 101 | } 102 | 103 | this.isInit = true; 104 | 105 | switch (this.variant) { 106 | case 'base-shaded': 107 | this.progressIndicatorVariant = 'shaded'; 108 | this.progressIndicatorType = 'base'; 109 | break; 110 | case 'path': 111 | this.progressIndicatorVariant = 'base'; 112 | this.progressIndicatorType = 'path'; 113 | break; 114 | default: 115 | this.progressIndicatorVariant = 'base'; 116 | this.progressIndicatorType = 'base'; 117 | } 118 | } 119 | 120 | /** 121 | * Unregister a wizard step defined in component template 122 | * 123 | * @param {String} Step name 124 | */ 125 | unregisterStep(stepName) { 126 | delete this.steps[stepName]; 127 | } 128 | 129 | /** 130 | * Sets the wizard current step 131 | * 132 | * @param {String} stepName Current Step name 133 | */ 134 | setActiveStep(stepName) { 135 | var self = this; 136 | 137 | if (stepName) { 138 | self.dispatchEvent(new CustomEvent('change', { 139 | detail: { 140 | oldStep: self._currentStep, 141 | currentStep: stepName 142 | } 143 | })); 144 | 145 | self._currentStep = stepName; 146 | } 147 | 148 | Object.values(self.steps).forEach(function (step) { 149 | step.methods.setActive(step.name === self._currentStep); 150 | }); 151 | 152 | } 153 | 154 | /** 155 | * Determines the wizard flow based on component body slot 156 | */ 157 | configSteps() { 158 | var stepComponents = this.querySelectorAll('c-wizard-step'), self = this; 159 | 160 | this.flow = Array.prototype.map.call(stepComponents, (step, index) => { 161 | self.steps[step.name].methods.config({ 162 | isFirst: index === 0, 163 | isLast: index === (stepComponents.length - 1) 164 | }) 165 | 166 | return self.steps[step.name]; 167 | }); 168 | 169 | if (!this.currentStep && this.flow) { 170 | this.currentStep = this.flow[0].name; 171 | } 172 | } 173 | 174 | /** 175 | * Moves to the next step, if available, and executes the customer-defined beforeChange hook of the current step. 176 | * If the beforeChange promise is resolve with a falsy value, the wizard stops at current step. 177 | * If the wizard is in its final step, dispatch the complete event. 178 | * 179 | * @param {String} direction Direction to move to. Valid values are next/previous 180 | */ 181 | async moveStep(direction) { 182 | let currentStep = this.steps[this._currentStep]; 183 | let currentStepIndex = this.flow.indexOf(currentStep); 184 | 185 | if (direction === 'next') { 186 | this.hasError = !(await this.beforeChange(this.steps[this._currentStep])); 187 | 188 | if (!this.hasError) { 189 | let newStep = this.flow[currentStepIndex + 1]; 190 | 191 | if (newStep) { 192 | this.setActiveStep(newStep.name); 193 | } else { 194 | this.dispatchEvent(new CustomEvent('complete')); 195 | } 196 | } 197 | } else { 198 | let newStep = this.flow[currentStepIndex - 1]; 199 | 200 | if (newStep) { 201 | this.setActiveStep(newStep.name); 202 | } 203 | } 204 | } 205 | 206 | /** 207 | * Execute flows the customer-defined beforeChange hook, fired whenever the wizard goes to the next step. 208 | * The hook is not invoked when a step change is a consequence of external causes 209 | * 210 | * @param {Object} step Step public definition, as defined on registerStep method 211 | * @returns {Promise(Boolean}) If the promise is resolve with a falsy value, the wizards stops at the current step, showing an error on the steo definition 212 | */ 213 | beforeChange(step) { 214 | return new Promise((resolve) => { 215 | if (!step.methods.beforeChange) { 216 | return resolve(true); 217 | } 218 | 219 | return resolve(step.methods.beforeChange()); 220 | }); 221 | } 222 | 223 | // #endregion 224 | } -------------------------------------------------------------------------------- /lightning-wizard/main/default/lwc/wizard/wizard.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54.0 4 | false 5 | -------------------------------------------------------------------------------- /lightning-wizard/main/default/lwc/wizardStep/wizardStep.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lightning-wizard/main/default/lwc/wizardStep/wizardStep.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api, track } from 'lwc'; 2 | 3 | export default class WizardStep extends LightningElement { 4 | // #region public API properties 5 | @api name; 6 | @api label; 7 | @api beforeChange = function() { return true; } 8 | @api hidePreviousButton = false; 9 | @api hideNextButton = false; 10 | 11 | @track _isActive = false; 12 | @api get isActive() { 13 | return this._isActive; 14 | } 15 | set isActive(value) { 16 | this._isActive = value; 17 | 18 | if(value) { 19 | this.setAttribute('is-active', true); 20 | this.setAttribute('aria-hidden', false); 21 | this.classList.add('slds-show'); 22 | this.classList.remove('slds-hide'); 23 | } else { 24 | this.removeAttribute('is-active'); 25 | this.setAttribute('aria-hidden', true); 26 | this.classList.remove('slds-show'); 27 | this.classList.add('slds-hide'); 28 | } 29 | } 30 | 31 | // #endregion 32 | 33 | // #region Tracked Properties 34 | @track isInit = false; 35 | // #endregion 36 | 37 | // #region Private Properties 38 | 39 | labels = { 40 | next: 'Next', 41 | previous: 'Previous', 42 | finish: 'Finish' 43 | } 44 | 45 | @track isLast = false; 46 | @track isFirst = false; 47 | 48 | get shouldHidePreviousButton() { 49 | return this.isFirst || this.hidePreviousButton? true:false; 50 | } 51 | 52 | get nextLabel() { 53 | return this.isLast? this.labels.finish:this.labels.next; 54 | } 55 | 56 | // #endregion 57 | 58 | // #region LWC Lifecycle Hooks 59 | 60 | connectedCallback() { 61 | this.dispatchEvent(new CustomEvent('stepregistered', { 62 | bubbles: true, 63 | detail: { 64 | name: this.name, 65 | label: this.label, 66 | methods: { 67 | setActive: this.setActive.bind(this), 68 | config: this.config.bind(this), 69 | beforeChange: typeof this.beforeChange === 'function'? this.beforeChange.bind(this):null 70 | } 71 | } 72 | })); 73 | } 74 | 75 | disconnectedCallback() { 76 | if(typeof this.unregister === 'function') { 77 | this.unregister(this.name); 78 | } 79 | } 80 | 81 | // #endregion 82 | 83 | // #region Private API 84 | 85 | setActive(isActive) { 86 | this.isActive = isActive; 87 | } 88 | 89 | config(props) { 90 | this.isFirst = props.isFirst; 91 | this.isLast = props.isLast; 92 | 93 | if(!this.isInit) { 94 | this.labels = props.labels; 95 | this.move = props.callbacks.move; 96 | this.unregister = props.callbacks.unregister; 97 | this.isInit = true; 98 | } 99 | } 100 | 101 | nextStep() { 102 | if(typeof this.move === 'function') { 103 | this.move('next'); 104 | } 105 | } 106 | 107 | previousStep() { 108 | if(typeof this.move === 'function') { 109 | this.move('previous'); 110 | } 111 | } 112 | 113 | move = null; 114 | unregister = null; 115 | 116 | // #endregion 117 | } -------------------------------------------------------------------------------- /lightning-wizard/main/default/lwc/wizardStep/wizardStep.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54.0 4 | false 5 | -------------------------------------------------------------------------------- /screenshots/create-account-flow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmpisson/lightning-wizard/88df08af0eb12474baefd0d9d47967a91769bcb3/screenshots/create-account-flow.gif -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "lightning-wizard", 5 | "default": true 6 | }, { 7 | "path": "lightning-wizard-examples", 8 | "default": false 9 | } 10 | ], 11 | "namespace": "", 12 | "sfdcLoginUrl": "https://login.salesforce.com", 13 | "sourceApiVersion": "51.0" 14 | } 15 | --------------------------------------------------------------------------------