[SalesForce] LWC pass callback function dynamically

I have a modal component where I am passing in two-button handler methods which are callbacks.

These two buttons are hooked up to a getter with the intent that I can override them, which I am having trouble doing.

Code Sample:

export default class CreateCaseApp extends LightningElement {

  // Default button handlers
  _saveHandler = {
    cb: this._modalSaveHandler,
    label: "Create Case",
    variant: "brand",
  };
  _cancelHandler = {
    cb: this._modalCancelHandler,
    label: "Cancel",
    variant: "netrual",
    class: "btn-left",
  };

  /**
   * Getter: Save handler callback and button props
   */
  get saveHandler() {
    return this._saveHandler;
  }

  /**
   * Getter: Cancel handler callback and button props
   */
  get cancelHandler() {
    return this._cancelHandler;
  }

  /**
   * Save event send to modal
   * @param {*} event
   */
  _modalSaveHandler = (event) => {
    console.log("Save Clicked");
  };

  /**
   * Handler event sent to modal
   * @param {*} event
   */
  _modalCancelHandler = (event) => {
    console.log("Cancel Clicked");
  };


  // Questionable code below


  /**
   * Handler event sent to modal for override
   * @param {*} event
   */
  _modalOverrideHandler = (event) => {
    console.log("Override Clicked");
  };

  onDispatchedEventFromChild(event) {
    if (event.detail == "nodata") {
      // Override save handler button
      this._saveHandler = {
        cb: this._modalOverrideHandler,
        label: "Okay",
        variant: "brand"
      };
    }
  }
}

In the above code, the initial saveHandler() and cancelHandler() are passing the fat arrow methods correctly via the cb property in the object. I can click on these buttons in the child component and they execute the method from this parent.

In the bottom section of code, I have an event listener onDispatchedEventFromChild() that receives data from a child component. When the logic in this method is met, I am trying to override the _saveHandler button by setting it with this._saveHandler={...}.

This updates the button details such as label and variant, but when I click on it, the new referenced callback isn't being fired.

Is this possible to do, or am I approaching this incorrectly? I tried using @track on the two variables, but that also didn't work.

Edit:

Parent Component HTML

<template>
    
    <c-modal
      modal-header={modalHeader}
      modal-save-handler={saveHandler}
    >
      <div
        slot="modalContent"
        class="modalContent slds-modal__content slds-p-around_medium"
      >
        <!-- Wrapper -->
        <c-create-case-wrapper
          contact-id={recordId}
          onmodalheaderupdate={handleModalHeaderEvent}
          onmodalhandlerupdate={handleModalHandlerEvent}
        ></c-create-case-wrapper>
        <!-- Wrapper -->

      </div>
    </c-modal>
  
    <!-- Main Modal Launch Button -->
    <lightning-button
      variant="brand"
      label="Create Enterprise Case"
      title="Create Enterprise Case"
      onclick={handleClick}
      class="slds-m-left_x-small"
    ></lightning-button>
    <!-- Main Modal Launch Button -->

  </template>

The parent is providing the callback for the button click within the modal component. When a button is clicked, the parent executes that method (in the parent).

In this example, the <c-create-case-wrapper> is also loading child components based on logic. When certain logic is met within those components, I need to change the button callback and label. This is where a child would dispatch an event to the parent so that I know I need to update the modal button.

I assumed that since the button was being passed to the modal reactively through the getter that I could just update that object/callback to override its defaults. This works for the label and variant properties just fine, its passing another/different callback that is not working.

Best Answer

There are a few ways to accomplish your requirement as mentioned below. Please note that code snippets are based on the github repo you pointed out and hence, you'll have to tweak it a bit if needed. Also, I've only given the code snippets that are to be added or updated (rest of your code would remain as-is).

Option 1:

In the parent component, introduce a private boolean field overrideSave and a method _modalSaveDefaultHandler. Update _modalSaveHandler code to execute based on the boolean flag value. Update onDispatchedEventFromChild code to toggle the boolean flag based on your specific conditions met. Now, the child modal component will get the callback reference to both _modalSaveDefaultHandler & _modalOverrideHandler through _modalSaveHandler.

overrideSave = false;

_modalSaveHandler = (event) => {
    if(!this.overrideSave){
        this._modalSaveDefaultHandler(event);
    }
    else{
        this._modalOverrideHandler(event);
    }
};

_modalSaveDefaultHandler = (event) => {
    console.log("Save Clicked");
};

onDispatchedEventFromChild() {
    if (event.detail == "nodata") {
        this.overrideSave = true;
    }
}

Option 2:

In the modal component HTML, you probably have the click event wired like this <button class="slds-button slds-button_brand save" onclick={modalSaveHandler.cb}>. Replace this with <button data-id="modalSaveBtn" class="slds-button slds-button_brand save">. Note that we have removed the declarative event wiring and will be doing it programmatically instead. Also, we have included a data- attribute to be able to query for the specific button element via code.

In the modal component JS, introduce a new private field handleRef which will be used to hold the reference to the callback. Modify the renderedCallback method to pass the callback reference (from modalSaveHandler public property) to handleRef and programmatically add the click event handler on the specific button. Add a new public method updateRegisteredEvent which will be called by the parent component and this method will be responsible for removing the old event listener as well as adding new event listener.

handlerRef;
isFirstRender = true;

renderedCallback() {
    if (this.isFirstRender) {
        this.handlerRef = this.modalSaveHandler.cb;
        this.template.querySelector('[data-id="modalSaveBtn"]').addEventListener('click', this.handlerRef);
        this.isFirstRender = false;
    }
}

@api updateRegisteredEvent(overrideFn) {
    this.template.querySelector('[data-id="modalSaveBtn"]').removeEventListener('click', this.handlerRef);   
    this.template.querySelector('[data-id="modalSaveBtn"]').addEventListener('click', overrideFn);        
}

In the parent component JS, update onDispatchedEventFromChild code to call the child component's public method (passing the override method as input param).

onDispatchedEventFromChild() {
    if (event.detail == "nodata") {
        // Override save handler button
        this._saveHandler = {
            label: "Okay",
            variant: "brand"
        };
        
        this.template.querySelector('c-modal').updateRegisteredEvent(this._modalOverrideHandler);
    }
}

Option 3:

In the modal component JS, define the event handler methods and hook them up in the HTML onclick attribute declaratively. The event handler code should only pass the modal component details (or form details) to the parent component via custom event. So, it doesn't know how the data will be processed by the parent component.

In the parent component, hook up event handlers corresponding to the custom events (dispatched by modal component) and implement your custom logic in these event handler based on your requirements (and specific conditions to be met).

Not providing any code samples here because this is the straight-forward LWC way of event communication and hence, should be easier to do. I would suggest to take this approach since its inline with LWC standard, defines specific function/ responsibility to each component and can be developed as reusable components.

Additional Info: In your current code, the callback function is passed via saveHandler property during the component initialization and both the components go thru connected- and rendered- callback lifecycle hooks. So, the LWC init and render lifecycle hooks wire up the callback to the button element's click event. However, when onDispatchedEventFromChild execution updates the callback via object literal (this._savehandler), only the renderedCallback lifecycle hook executes on both the components. This basically re-evaluates the UI elements and rerenders them, but doesn't do anything about the callback fn wiring to the button click event. This could be because of the way LWC event registration works internally.

Related Topic