Sample code for hooks and events
This article provides sample codes that use hooks and events in the extensibility framework.
Use async / await for long-running async JS tasks.
Sample code 1
Following is a sample code that uses the following hooks and events:
- beforeproductAdd
- afterProductAdd
- beforeProductUpdate
- afterProductUpdate
- beforeRulesExecution
- afterRulesExecution
- objectfieldconfig event
import { LightningElement, api } from "lwc"; export default class ComponentHeadless extends LightningElement { prefix = "zqu__"; applyConfigsForQuotes = true; @api quoteState; @api metricState; @api pageState; @api masterQuoteState; @api parentQuoteState; @api beforeProductAdd(ratePlanMap, effectiveDate) { if (ratePlanMap.hasOwnProperty("a0aRL0000084T2aYAK")) { return { // If the Product Rate Plan to be added is a0aRL0000084T2aYAK. Then stop product add proceed: false }; } const ratePlanToAdd = "a0aRL000008Q3AaYAK"; //Product Rate Plan Id to add const quantity = 2; //Quantity of the above product rate plan to be added ratePlanMap = { ...ratePlanMap, [ratePlanToAdd]: quantity }; let newEffectiveDate; if ( this.quoteState.quote.zqu__Billing_Account_Name__c === "Test Account" && effectiveDate === "2024-10-10" ) { // Change the effective date based on conditions newEffectiveDate = "2024-10-20"; //Optional } this.dispatchEvent( new CustomEvent("toastmessagedisplay", { detail: { message: "From beforeProductAdd", theme: "success", }, }) ); if (!newEffectiveDate) { // Effective Date prop is optional, if not provided, the existing effective date will be considered return { proceed: true, newRatePlanMap: ratePlanMap }; } return { proceed: true, newRatePlanMap: ratePlanMap, effectiveDate: newEffectiveDate, }; } @api afterProductAdd(newProductTimeline) { this.processProductTimelines(newProductTimeline, false); } @api beforeProductUpdate( updatedCharges, updatedAmendment, updatedRatePlan, isRevert ) { if (updatedCharges && !(updatedAmendment && updatedRatePlan)) { //Quote Rate Plan Charge update if (updatedCharges[0].Name === "Economy") { updatedCharges[0].zqu__Quantity__c = 5; // Update the quantity field of the charge based on condition return { proceed: true, updatedCharges }; } else if (updatedCharges[0].Name === "Standard") { return { // Stop the Charge Update proceed: false }; } return { // Proceed with the Charge Update If no changes are required proceed: true, updatedCharges }; } else if (updatedAmendment && !(updatedCharges && updatedRatePlan)) { // Quote Amendment update // Quote Amendment record is present in updatedAmendment.amendment only for Amendment update if ( updatedAmendment.amendment.zqu__ContractEffectiveDate__c === "2024-10-10" ) { // update the Quote Amendment fields based on conditions updatedAmendment.amendment.zqu__ContractEffectiveDate__c = "2024-10-20"; updatedAmendment.amendment.zqu__CustomerAcceptanceDate__c = "2024-10-20"; updatedAmendment.amendment.zqu__ServiceActivationDate__c = "2024-10-20"; } else if (updatedAmendment.specificUpdateDate === "2024-10-12") { // specificUpdateDate will be present in the updatedAmendment object only if there is an update on that property in the order Action Modal updatedAmendment.specificUpdateDate === "2024-11-20"; // Update the specificUpdateDate based on conditions } else if ( updatedAmendment.amendment.zqu__ContractEffectiveDate__c === "2024-10-10" ) { return { // Stop the Quote Amendment update proceed: false }; } return { // Proceed with the Amendment field Update If no changes are required proceed: true, updatedAmendment }; } else if (updatedRatePlan && !(updatedCharges && updatedAmendment)) { //Quote Rate Plan custom fields update if (updatedRatePlan.Name === "Recurring Fee") { updatedRatePlan.zqu__Custom_Field = 20; // Update Quote Rate Plan Custom Fields based on conditions return { proceed: true, updatedRatePlan }; } else if (updatedCharges[0].Name === "Flat Fee") { return { // Stop Quote Rate Plan Custom Field Update proceed: false }; } return { proceed: true, updatedRatePlan }; } else if ( updatedCharges && updatedAmendment && updatedRatePlan && !isRevert ) { //Future Dated Update if (updatedRatePlan.Name === "Standard Plan") { // Update Quote Rate Plan, Quote Amendment and Charge fields based on conditions if it is a Future Dated Update updatedRatePlan.zqu__Custom_Field = 11; updatedAmendment.zqu__ContractEffectiveDate__c = "2024-10-11"; updatedCharges[0].zqu_Quantity__c = 10; } return { proceed: true, updatedRatePlan, updatedCharges, updatedAmendment, }; } else if ( updatedCharges && updatedAmendment && updatedRatePlan && isRevert ) { // Future Dated Update revert or Future Dated Remove revert return { proceed: true }; } } @api afterProductUpdate(updatedCharges, updatedRatePlan) { let stopLoop = false; let configs = []; const timelines = this.quoteState.subscription.productTimelines; for (const [, timeline] of Object.entries(timelines)) { if (stopLoop) break; for (const [index, version] of timeline.versions.entries()) { if (stopLoop) break; const nextEffectiveDate = timeline.versions[index + 1]?.amendment.record[ this.prefix + "ContractEffectiveDate__c" ]; const currentEffectiveDate = version.amendment.record[this.prefix + "ContractEffectiveDate__c"]; if ( !timeline.versions[index + 1]?.voidAction && currentEffectiveDate === nextEffectiveDate ) // Skip if the effective date is same as the next version (Scenario: While amending the original charge on the same date) continue; for (const [chargeIndex, charge] of version.charges.entries()) { if ( updatedRatePlan[0].Id === version.Id && (!updatedCharges || updatedCharges.find( (updatedCharge) => updatedCharge[this.prefix + "ProductRatePlanCharge__c"] === charge.record[this.prefix + "ProductRatePlanCharge__c"] )) ) { const quantity = charge.record[this.prefix + "Quantity__c"]; configs.push( ...this.generateConfigsForCharges( //Generate configs for the charge fields based on the change in quantity field chargeIndex, timeline.timelineId, version.amendment.record[ this.prefix + "ContractEffectiveDate__c" ], quantity ) ); if (quantity >= = 10) { configs.push({ // Apply configs for custom Quote Rate Plan fields in the Quote Rate Plan Modal field: this.prefix + "Custom_Field__c", effectiveDate: currentEffectiveDate, timelineId: timeline.timelineId, object: this.prefix + "QuoteRatePlan__c", backgroundColor: "orange", readOnly: false, }); configs.push({ // Apply configs for Quote Amendment fields in the Order Action Detail Modal field: this.prefix + "ContractEffectiveDate__c", effectiveDate: currentEffectiveDate, timelineId: timeline.timelineId, object: this.prefix + "QuoteAmendment__c", backgroundColor: "blue", helpText: "Quanity cannot be greater than 9", disabled: true, }); configs.push({ // Apply configs for Quote Amendment fields in the Future Dated Update Modal field: this.prefix + "ContractEffectiveDate__c", timelineId: timeline.timelineId, object: this.prefix + "QuoteAmendment__c", hidden: true }); } else { configs.push({ field: this.prefix + "Custom_Field__c", effectiveDate: currentEffectiveDate, timelineId: timeline.timelineId, object: this.prefix + "QuoteRatePlan__c", backgroundColor: "default", readOnly: true, }); configs.push({ field: this.prefix + "ContractEffectiveDate__c", effectiveDate: currentEffectiveDate, timelineId: timeline.timelineId, object: this.prefix + "QuoteAmendment__c", backgroundColor: "default", helpText: null, disabled: false, }); configs.push({ field: this.prefix + "ContractEffectiveDate__c", timelineId: timeline.timelineId, object: this.prefix + "QuoteAmendment__c", hidden: false });; stopLoop = true; //Stop the loop after the execution is completed for the particular charge update } } } } if (configs.length > 0) { this.dispatchEvent( new CustomEvent("objectfieldconfig", { detail: { configs } }) ); } } } @api async beforeRulesExecution(context) { console.log(`Running beforeRulesExecution in context : ${context.triggerEvent}`); const rulesIdsToExecute = this.pageState.rules?.active.filter(rule => rule.name === 'Test Rule')?.map(fiteredRule => fiteredRule.Id); //Filter the rule Ids based on criteria return { rulesToExecute: rulesIdsToExecute, runRules: true } } @api afterRulesExecution(rulesResponse) { if ( rulesResponse.newlyAddedTimelines && Object.keys(rulesResponse.newlyAddedTimelines).length > 0 ) // If an new product is added through rules engine this.processProductTimelines(rulesResponse.newlyAddedTimelines); const updatedProductTimelines = rulesResponse.updatedQuoteState.subscription.productTimelines; if (updatedProductTimelines) // If there are changes in the product timelines this.processProductTimelines(updatedProductTimelines, true); } /* Generates config for charges based on quantity change and returns back the JS object of config */ generateConfigsForCharges(chargeIndex, timelineId, effectiveDate, quantity) { const baseConfig = { chargeIndex, timelineId, effectiveDate, object: this.prefix + "QuoteRatePlanCharge__c", }; const customConfig = { ...baseConfig, field: "Custom_Field__c" }; const discountConfig = { ...baseConfig, field: this.prefix + "Discount__c", readOnly: false, disabled: false, hidden: false, }; if (quantity === 1) { customConfig.backgroundColor = "red"; customConfig.helpText = "Quantity is 1"; } else if (quantity >= 2 && quantity <= 5) { customConfig.backgroundColor = "#ff6347"; customConfig.helpText = "Quantity is between 2 and 5"; discountConfig.hidden = true; } else if (quantity >= 6 && quantity <= 10) { customConfig.backgroundColor = "rgba(255, 0, 191, 0.5)"; customConfig.helpText = "Quantity is between 6 and 10"; discountConfig.disabled = true; } else if (quantity >= 11 && quantity <= 15) { customConfig.backgroundColor = "hsl(105, 100%, 50%)"; customConfig.helpText = "Quantity is between 11 and 15"; discountConfig.readOnly = true; } else { customConfig.backgroundColor = "default"; customConfig.helpText = null; } return [customConfig, discountConfig]; } /* Process the product timeline and fire the objectfieldconfig event */ processProductTimelines(productTimelines, rulesExecuted) { const productTimelineEntries = Object.entries(productTimelines); if (!productTimelineEntries.length) return; let configs = []; for (const [, timeline] of productTimelineEntries) { for (const [index, version] of timeline.versions.entries()) { // Check if version is changed in the rules engine and voidAction (deleted rate plan) is not true if ( !version.voidAction && (!rulesExecuted || version.isChangedInRulesEngine) ) { const currentEffectiveDate = version.amendment.record[this.prefix + "ContractEffectiveDate__c"]; const nextEffectiveDate = timeline.versions[index + 1]?.amendment.record[ this.prefix + "ContractEffectiveDate__c" ]; if (currentEffectiveDate === nextEffectiveDate) continue; // Skip if the effective date is same as the next version (Scenario: While amending the original charge on the same date) for (const [chargeIndex, charge] of version.charges.entries()) { if ( !rulesExecuted || charge.fieldsChangedInRulesEngine?.length > 0 ) { const quantity = charge.record[this.prefix + "Quantity__c"]; configs.push( ...this.generateConfigs( chargeIndex, timeline.timelineId, currentEffectiveDate, quantity ) ); } } } } } if (this.applyConfigsForQuotes) { if (this.quoteState?.quote) { // Apply configs for the quote configs.push({ field: this.prefix + 'ValidUntil__c', object: this.prefix + 'Quote__c', quoteId: this.quoteState.quote.Id, backgroundColor: 'blue', helpText: 'Fill the Valid Till Date for the quote', }); configs.push({ field: this.prefix + 'InitialTerm__c', object: this.prefix + 'Quote__c', quoteId: this.quoteState.quote.Id, readOnly: true, helpText: 'Default Initial term cannot be changed' }); configs.push({ field: 'Name', object: this.prefix + 'Quote__c', quoteId: this.quoteState.quote.Id, backgroundColor: 'red', helpText: 'Fill the Quote Name', }); configs.push({ field: this.prefix + 'Opportunity__c', object: this.prefix + 'Quote__c', quoteId: this.quoteState.quote.Id, hidden: true }); } if (this.parentQuoteState?.quote) { // Applying configs for parent quote in the MSQ context configs.push({ field: 'Name', object: this.prefix + 'Quote__c', quoteId: this.parentQuoteState.quote.Id, backgroundColor: 'pink' }); configs.push({ field: 'Name', object: this.prefix + 'Quote__c', quoteId: this.parentQuoteState.quote.Id, helpText: 'Fill the Parent Quote Name', hidden: true }); configs.push({ field: this.prefix + 'Opportunity__c', object: this.prefix + 'Quote__c', quoteId: this.parentQuoteState.quote.Id, backgroundColor: 'violet', helpText: 'Fill the opportunity Name', hidden: false }); } this.applyConfigsForQuotes = false; } if (configs.length > 0) { this.dispatchEvent( new CustomEvent("objectfieldconfig", { detail: { configs } }) ); this.executed = true; } } }
Sample code 2
Following is the sample code to auto-populate billing language picklist values when the Sold To Contact field is modified.
import { LightningElement, api } from 'lwc'; const PREFIX = 'zqci15__'; export default class HeadLessComp extends LightningElement { @api pageState; _previousQuoteState; _currentQuoteState; @api metricState; connectedCallback() { console.log('Component Loaded...'); } @api get quoteState() { return this._currentQuoteState; } set quoteState(newState) { this._currentQuoteState = newState; // Destructure quotes for easier access const newQuote = newState?.quote; const oldQuote = this._previousQuoteState?.quote; if (oldQuote && this.hasFieldChanged(oldQuote, newQuote, 'SoldToContact__c')) { const updatedQuote = { ...newQuote, [`${PREFIX}Custom_Picklist__c`]: 'Value2' }; // Store the current state and dispatch the event this._previousQuoteState = { ...newState }; this.dispatchQuoteUpdate(updatedQuote); } else { // Simply store the current state if no changes detected this._previousQuoteState = { ...newState }; } } hasFieldChanged(oldQuote, newQuote, fieldName) { return newQuote?.[`${PREFIX}${fieldName}`] && oldQuote?.[`${PREFIX}${fieldName}`] !== newQuote?.[`${PREFIX}${fieldName}`]; } dispatchQuoteUpdate(updatedQuote) { this.dispatchEvent(new CustomEvent('updatequote', { detail: { quote: updatedQuote } })); } }
Sample code 3
The following is the sample code to change the quote name before saving execution. This example uses a hook before saving and an event before the preview call.
import { LightningElement, api } from 'lwc'; export default class HeadLessComp extends LightningElement { @api quoteState; @api metricState; @api pageState; sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); connectedCallback() { console.log('Custom Component is Loaded,..'); this.dispatchEvent(new CustomEvent('toastmessagedisplay', { detail: { message: 'Custom Component is Loaded...', theme: 'success' } })); } @api async beforeSave() { //Use Async / Await to dely execution - If any long running task like external API Calls to be completed and if it's response has a dependency on the logics in this method await this.sleep(5000); // will be executed after 5000 ms this.dispatchEvent(new CustomEvent('toastmessagedisplay', { detail: { message: 'Ssve logic from Custom Component', theme: 'error' } })); this.quoteState = JSON.parse(JSON.stringify(this.quoteState)); this.quoteState.quote.Name = 'Quote Name from Custom Comp'; this.dispatchEvent(new CustomEvent('updatequote', { detail: { quote: this.quoteState.quote } })); //continue Quote Studio SAVE Logic return true; } @api beforePreviewCall() { this.dispatchEvent(new CustomEvent('toastmessagedisplay', { detail: { message: 'Preview Call logic from Custom Component', theme: 'success' } })); //continue Quote Studio Preview on-demand Logic return true; } @api beforeSubmit() { this.dispatchEvent(new CustomEvent('toastmessagedisplay', { detail: { message: 'Submit logic from Custom Component', theme: 'error' } })); //STOP Quote Studio SUBMIT Logic return false; } }
Sample code 4
The following is a sample code for opensidebarcomponent, closesidebar and changesidebarwidth.
When the Save button is clicked, the sidebar component opens to allow input for the Initial Term value. After clicking the Save button in the custom component, the sidebar closes, and the quote is saved.
//commonUtils to hold the static value on whether beforeSave hook should execute or not let _executeSaveLogic = true; export const getExecuteSaveLogic = () => { return _executeSaveLogic; }; export const setExecuteSaveLogic = (value) => { _executeSaveLogic = value; };
// Custom sidebar component which takes the initial term value as input, updates the quote with new initial term and save the quote import { LightningElement, api } from 'lwc'; import { setExecuteSaveLogic } from 'c/commonUtils'; export default class CustomSidebarComponent extends LightningElement { @api quoteState; @api metricState; @api pageState; @api masterQuoteState; @api parentQuoteState; initialTerm; // To store the Initial Term values handleClick() { this.dispatchEvent(new CustomEvent('closesidebar')); // Event to close the sidebar component let quoteClone = Object.assign({}, this.quoteState.quote); quoteClone.zqci15__InitialTerm__c = this.initialTerm; const updateQuoteEvent = new CustomEvent('updatequote', { // Update the quote with new initial term value detail: { quote: quoteClone } }); this.dispatchEvent(updateQuoteEvent); setTimeout(() => { // Save the quote after update quote event is executed setExecuteSaveLogic(false); // Do not run the beforeSave hook this.dispatchEvent(new CustomEvent('savequote')); }, 0); } handleChange(event) { this.initialTerm = event.target.value; // To store the Initial Term value on change } }
import { LightningElement, api } from 'lwc'; import { getExecuteSaveLogic } from 'c/commonUtils'; export default class ComponentHeadless extends LightningElement { @api quoteState; @api metricState; @api pageState; @api masterQuoteState; @api parentQuoteState; connectedCallback() { this.dispatchEvent( new CustomEvent('changesidebarwidth', { detail: { width: '80vw' } }) // Event to change the sidebar width ); } @api beforeSave() { if (getExecuteSaveLogic()) { // Check whether to execute the logic in beforeSave hook this.dispatchEvent( new CustomEvent('opensidebarcomponent', { // Open Sidebar component detail: { componentName: 'componentHead' } }) ); return false; } return true; } }