OmniStudio : The Power of extending OmniscriptTypeahead

OmniStudio : The Power of extending OmniscriptTypeahead

ยท

4 min read

Intro:

In the vibrant world of OmniStudio, it's often tempting to dive deep into creating custom Lightning Web Components (LWC) for specific functionalities. However, there exists immense power in simply extending existing components to suit our needs. In this blog post, we will explore how to create a combobox with both autocomplete and multi-select features by extending OmniscriptTypeahead.

Note: This method works properly only if OmniStudio Runtime is enabled.

1. Understanding OmniscriptTypeahead:

OmniscriptTypeahead serves as a basis for any input field that requires type-ahead capabilities. Instead of creating an LWC from scratch, extending this component can provide a solid foundation and save development time.

2. Steps to Extend the Component:

a. Setting Up Your Environment:
Ensure OmniStudio Runtime is enabled. This is crucial for our extended component to function properly.

b. Creating the ComboBox:
We just have to copy paste the omnistuido base element html as it is with just a minor change.

OmniStudio Base Elements Repo

<template>
    <slot>
        <div class="omni-typeahead-container">
            <omnistudio-typeahead type="text"
                                  if:false={_propSetMap.enableLookup}
                                  field-level-help={_handleHelpText}
                                  field-level-help-position={_propSetMap.helpTextPos}
                                  label={_propSetMap.label}
                                  onblur={handleBlur}
                                  onlastitemclick={enterEditMode}
                                  onselect={handleSelect}
                                  onclear={handleClear}
                                  icon-name-right="utility:clear"
                                  options={options}
                                  placeholder={_placeholder}
                                  required={_propSetMap.required}
                                  min-length={_propSetMap.minLength}
                                  max-length={_propSetMap.maxLength}
                                  required-label={allCustomLabelsUtil.OmniRequired}
                                  message-when-value-missing={_messageWhenValueMissing}
                                  message-when-too-short={_messageWhenTooShort}
                                  message-when-too-long={_messageWhenTooLong}
                                  theme={_theme}
                                  value={elementValue}
                                  remote-source={_useRemoteSource}
                                  disable-filter={_disableFilter}
                                  data-omni-input>
                <button if:false={_propSetMap.hideEditButton}
                        class="slds-button slds-button_icon nds-button nds-button_icon"
                        slot="iconRight"
                        aria-expanded={_isEditMode}
                        onclick={handleCustomClear}>
                    <omnistudio-icon icon-name="utility:clear"
                                     extraclass="slds-button__icon nds-button__icon"
                                     size="x-small"
                                     theme={_theme}
                                     alternative-text={_editLabel}></omnistudio-icon>
                </button>

                <a slot="lastItem"
                   if:true={_newItemLabel}
                   label={_newItemLabel}>{_newItemLabel}</a>

            </omnistudio-typeahead>

            <omnistudio-typeahead type="text"
                                  if:true={_propSetMap.enableLookup}
                                  field-level-help={_handleHelpText}
                                  field-level-help-position={_propSetMap.helpTextPos}
                                  label={_propSetMap.label}
                                  onblur={handleBlur}
                                  onselect={handleSelect}
                                  icon-name-right="utility:down"
                                  options={options}
                                  placeholder={_placeholder}
                                  required={_propSetMap.required}
                                  required-label={allCustomLabelsUtil.OmniRequired}
                                  min-length={_propSetMap.minLength}
                                  max-length={_propSetMap.maxLength}
                                  message-when-value-missing={_messageWhenValueMissing}
                                  message-when-too-short={_messageWhenTooShort}
                                  message-when-too-long={_messageWhenTooLong}
                                  theme={_theme}
                                  value={elementValue}
                                  remote-source
                                  disable-filter={_disableFilter}
                                  data-omni-input>
            </omnistudio-typeahead>

            <div if:true={errorMessage}
                 class="slds-has-error nds-has-error">
                <span class="slds-form-element__help nds-form-element__help nds-form-element__help_text-transform__none">{errorMessage}</span>
            </div>

        </div>

        <div class="slds-var-p-around_xx-small">
            <template for:each={pillValues} for:item="eachValue">
                <lightning-pill if:true={eachValue.selected} key={eachValue} name={eachValue.value} label={eachValue.label} class="slds-p-around_xx-small" onremove={handleRemove}></lightning-pill>
            </template>
        </div>
    </slot>
</template>

The only change here is the addition of this code, which creates the pill, with the selected value from the dropdown.

<div class="slds-var-p-around_xx-small">
            <template for:each={pillValues} for:item="eachValue">
                <lightning-pill if:true={eachValue.selected} key={eachValue} name={eachValue.value} label={eachValue.label} class="slds-p-around_xx-small" onremove={handleRemove}></lightning-pill>
            </template>
        </div>

And the javascript with few methods overriding the omniscriptTypeAhead base template.

/**
 * Created by nagendrakumar.singh on 30/08/23.
 */

import OmniscriptTypeahead from "omnistudio/omniscriptTypeahead";
import tmpl from "./customTypeAheadWithPill.html";
import {dispatchOmniEvent} from "omnistudio/omniscriptUtils";
import {track} from "lwc";

export default class CustomTypeAheadWithPill extends OmniscriptTypeahead {
    @track pillValues = [];

    render() {
        return tmpl;
    }


    /**
     * Overriding connected callback to create pills back if the related data is found in the customNodeName of the
     * omniscript
     */
    connectedCallback() {
        super.connectedCallback();

        // Initialize the data based on custom attributes from Component JSON.
        let customNodeName = this.jsonDef.propSetMap.customNodeName;

        // This code will load the data back to the component based on the data from database.
        if (
            customNodeName &&
            this.jsonData &&
            this.jsonData[customNodeName] &&
            this.jsonData[customNodeName].length > 0
        ) {
            this.pillValues = this.jsonData[customNodeName];
        }
    }

    //This clears the data from the typeahead input element.
    handleCustomClear(event) {
        event.stopPropagation();
        event.preventDefault();
        this.elementValue = "";
    }

    // This gets called when we select a value from dropdown via enter or click.
    handleSelect(event) {
        super.handleSelect(event);
        if (event.detail && event.detail.item && event.detail.item.name) {
            const newValue = {
                label: event.detail.item.name,
                value: event.detail.item.value,
                selected: true
            };

            // Check if value is already in the array
            const existingValueIndex = this.pillValues.findIndex(
                (item) => item.value === newValue.value
            );

            // As pillValues are treated as read only variables, so cannot be changed directly, thus deep cloning it
            let copyPillValues = JSON.parse(JSON.stringify(this.pillValues));

            if (existingValueIndex === -1) {
                //value does not exist in the array, push the new value
                copyPillValues.push(newValue);
            } else {
                //remove the existing value from its current position
                copyPillValues.splice(existingValueIndex, 1);

                //Push the updated value to end of the array
                copyPillValues.push(newValue);
            }

            // Assing copyPillValues values back to pillValues;
            this.pillValues = copyPillValues;
        }
        this.updateDataInParentJSON();
        this.elementValue = "";
    }

    handleRemove(event) {
        let removedName = event.target.name;

        // Filter out the object with the specified value from this.pillValues
        this.makeNodeFalseBasedOnValue(this.pillValues, removedName);

        this.updateDataInParentJSON();
    }


    /**
     * Makes value as false based on removed node
     * @param pillValues Pill values to build the pill.
     * @param targetValue Target value which got removed.
     */
    makeNodeFalseBasedOnValue(pillValues, targetValue) {

        // As pillValues are treated as read only variables, so cannot be changed directly, thus deep cloning it
        let copyOforiginalArray = JSON.parse(JSON.stringify(pillValues));
        for (let eachArrayObject of copyOforiginalArray) {
            if (eachArrayObject.value === targetValue) {
                eachArrayObject.selected = false;
                break;
            }
        }

        // Assigning back the values to pillValues
        this.pillValues = copyOforiginalArray;
    }


    /**
     * This updates teh data in Parent omniscript JSON.
     * This is an internal method from Omniscript and the data has to be passed in the same way.
     */
    updateDataInParentJSON() {
        let customNodeName = this.jsonDef.propSetMap.customNodeName;
        let finalValue = [];
        for (let i = 0; i < this.pillValues.length; i++) {
            finalValue.push({
                label: this.pillValues[i].label,
                value: this.pillValues[i].value,
                selected: this.pillValues[i].selected
            });
        }

        let apiResponseData = {
            [customNodeName]: finalValue
        };
        let myData = {
            apiResponse: apiResponseData
        };

        //Fire event for updating parent omniscript json
        dispatchOmniEvent(this, myData, "omniactionbtn");
    }

}

The component blocks duplicate additions of values. It updates the parent JSON too based on the values selected.

In the Type Ahead Block properties's json view, we need to pass "customNodeName" as "multiSelectedData" so that this can be accessed in our extended component and the same can be updated when dropdown values are selected.

3.Benefits of Extending OmniscriptTypeahead:

  • Saves Time: Avoid the overhead of starting from scratch.

  • Maintain Consistency: Leveraging existing components ensures UI consistency.

  • Efficient Maintenance: Updates to the base component can benefit the extended component, ensuring you're not left behind when updates roll out.

4. Components

Here is the repository link.

Github Repo Link

5. Demo

Did you find this article valuable?

Support Nagendra Singh by becoming a sponsor. Any amount is appreciated!

ย