Lang
Blog

Building Dynamic Filter Rules with Salesforce Lightning Web Components

ByAditya Kumawat
February 28th . 5 min read
Building Dynamic Filter Rules with Salesforce Lightning Web Components

Introduction

The Rules component is meticulously crafted to empower Salesforce users, providing them with a seamless experience in effortlessly creating dynamic filter rules for Opportunities.

Harnessing the inherent flexibility of Lightning Web Components, this module stands out by delivering a user-friendly interface that simplifies the process of setting up intricate filters based on various field types. With an intuitive design, users can navigate through the complexities of filter customization with ease, ensuring a streamlined and efficient approach to enhancing their Opportunity management within the Salesforce platform.

Component Structure

There are three components involved - HTML, Javascript & CSS.

  • HTML Component: The HTML markup defines the structure of the component, including the main container for rules, dynamic input elements, and the "Add Rule" button. The use of Lightning Design System (SLDS) classes ensures a consistent and visually appealing layout.
  • Javascript Component: The JavaScript logic of the component is responsible for dynamic data fetching, handling user interactions, and updating the component state in real-time. Noteworthy functionalities include fetching object fields, dynamically updating options based on user selections, and managing rule addition/removal.
  • CSS Component: The CSS styles define the visual presentation of the component. It ensures proper alignment, spacing, and a clean, modern look. The use of SLDS classes and custom styling contributes to a cohesive and responsive design.

Let's break down the structure of the Rules component:

HTML Markup

<template>
  <template for:each={rules} for:item="rule">
    <div key={rule.id} class="rules-container">
      <div class="main-label-container">
        <h3>Rule {rule.id}</h3>
      </div>
      <div class="main-combobox-container">
        <lightning-combobox data-id={rule.id} class="main-combobox" label="Field" value={rule.selectedRuleField} options={ruleFieldOptions} disabled={rule.isFieldDisabled} onchange={handleRuleFieldChange} dropdown-alignment="auto"></lightning-combobox>
      </div>
      <div class="text-input-dropdown dynamic-combobox-container">
        <lightning-combobox data-id={rule.id} class="dynamic-combobox" label="Operator" value={rule.selectedRuleOperator} options={rule.ruleOperatorOptions} disabled={rule.isOperatorDisabled} onchange={handleOperatorChange} dropdown-alignment="auto">
        </lightning-combobox>
      </div>
      <div class="text-input-container">
        <template if:true={rule.isType}>
          <lightning-input data-id={rule.id} type={rule.type} variant="standard" formatter={rule.formatter} value={rule.selectedRuleValue} name={rule.selectedFieldCategory} label="Value" disabled={rule.isValueDisabled} placeholder={rule.placeholder} onblur={handleValueBlur} onchange={handleValueChange}></lightning-input>
        </template>
        <template if:false={rule.isType}>
          <lightning-combobox data-id={rule.id} class="rule-value-combobox" label="Value" placeholder="Select Value..." value={rule.selectedRuleValue} options={rule.ruleValueOptions} disabled={rule.isValueDisabled} onchange={handleValueChange} dropdown-alignment="auto">
          </lightning-combobox>
        </template>
      </div>
      <div class="remove-rules icon-wrapper">
        <lightning-icon class={removeIcon} icon-name="utility:close" alternative-text="removeRule" onclick={handleRemoveRule} data-id={rule.id}></lightning-icon>
      </div>
    </div>
  </template>
  <div class="add-rules">
    <div class="main-label-container"></div>
    <lightning-button variant="base" label="+ Add Rule" title="+ Add Rule" onclick={handleAddRules} class="add-button slds-m-left_x-small"></lightning-button>
  </div>
  </div>
</template>

JavaScript Logic

import { LightningElement, track, wire } from 'lwc';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';
import OPPORTUNITY_OBJECT from '@salesforce/schema/Opportunity';
const fieldToOperatorsMap = {
string: [
{ label: "Equals", value: "equals" },
{ label: "Does Not Equals", value: "doesNotEquals" },
{ label: "Start With", value: "startWith" },
{ label: "End With", value: "endWith" },
{ label: "Contains", value: "contains" },
{ label: "Is Null", value: "isNull" },
],
integer: [
{ label: "Equals", value: "equals" },
{ label: "Does Not Equals", value: "doesNotEquals" },
{ label: "Greater Than", value: "greaterThan" },
{ label: "Less Than", value: "lessThan" },
{ label: "Greater Than Or Equal", value: "greaterThanOrEqual" },
{ label: "Less Than Or Equal", value: "lessThanOrEqual" },
{ label: "Contains", value: "contains" },
{ label: "Is Null", value: "isNull" },
],
currency: [
{ label: "Equals", value: "equals" },
{ label: "Does Not Equals", value: "doesNotEquals" },
{ label: "Greater Than", value: "greaterThan" },
{ label: "Less Than", value: "lessThan" },
{ label: "Greater Than Or Equal", value: "greaterThanOrEqual" },
{ label: "Less Than Or Equal", value: "lessThanOrEqual" },
{ label: "Is Null", value: "isNull" },
],
boolean: [
{ label: "Equals", value: "equals" },
{ label: "Does Not Equals", value: "doesNotEquals" },
{ label: "Is Null", value: "isNull" },
],
picklist: [
{ label: "Equals", value: "equals" },
{ label: "Does Not Equals", value: "doesNotEquals" },
{ label: "Contains", value: "contains" },
],
date: [
{ label: "Equals", value: "equals" },
{ label: "Does Not Equals", value: "doesNotEquals" },
{ label: "Greater Than", value: "greaterThan" },
{ label: "Less Than", value: "lessThan" },
{ label: "Greater Than Or Equal", value: "greaterThanOrEqual" },
{ label: "Less Than Or Equal", value: "lessThanOrEqual" },
{ label: "Is Null", value: "isNull" },
],
percent: [
{ label: "Equals", value: "equals" },
{ label: "Does Not Equals", value: "doesNotEquals" },
{ label: "Greater Than", value: "greaterThan" },
{ label: "Less Than", value: "lessThan" },
{ label: "Greater Than Or Equal", value: "greaterThanOrEqual" },
{ label: "Less Than Or Equal", value: "lessThanOrEqual" },
{ label: "Is Null", value: "isNull" },
],
};
const ruleValueOptions = {
equals: [
{ label: 'True', value: 'True' },
{ label: 'False', value: 'False' }
],
doesNotEquals: [
{ label: 'True', value: 'True' },
{ label: 'False', value: 'False' }
],
isNull: [
{ label: 'True', value: 'True' },
{ label: 'False', value: 'False' }
],
}
export default class Rules extends LightningElement {
ruleFieldOptions = [];
ruleOperatorOptions = [];
ruleValueOptions = [];
selectedFieldDataDetails = [];

@track rules = [
{
id: 1,
selectedRuleField: null,
selectedRuleOperator: null,
selectedRuleValue: null,
isFieldDisabled: false,
isOperatorDisabled: true,
isValueDisabled: true,
selectedFieldCategory: 'string',
selectedOperatorCategory: 'equals',
type: 'text',
placeholder: 'Enter a value',
},

];

@wire(getObjectInfo, { objectApiName: OPPORTUNITY_OBJECT })
wiredObjectInfo({ error, data }) {
if (data) {
const allowedDataTypes = ['String', 'Currency',];
this.ruleFieldOptions = Object.values(data.fields)
.filter(field => allowedDataTypes.includes(field.dataType))
.map(field => ({
label: `${field.label} (${field.dataType})`,
value: field.apiName,
dataType: field.dataType
}));
} else if (error) {
console.error(error);
}
}

shouldRenderInput() {
const hasRules = this.rules && this.rules.length > 0;
if (hasRules) {
// Check if any rule satisfies the conditions
return this.rules.some(rule => {
const isNotNullOrChange = !(rule.selectedOperatorCategory === 'isNull' || rule.selectedOperatorCategory === 'isChanged');
const isStringType = rule.selectedFieldCategory.toLowerCase() === 'string';
const isNumberType = rule.selectedFieldCategory.toLowerCase() === 'currency';
const isDateType = rule.selectedFieldCategory.toLowerCase() === 'date';
return isNotNullOrChange && (isStringType || isNumberType || isDateType);
});
}
return false;
}

get inputType() {
return this.defaultValueType.selectedFieldCategory.toLowerCase() === 'date'
? 'date'
: 'text';
}

selectedFieldDataType(value) {
this.selectedFieldDataPattern = this.ruleFieldOptions.find((field) => field.value === value);
return this.selectedFieldDataPattern ? this.selectedFieldDataPattern.dataType : "";
}

handleRuleFieldChange(event) {
const ruleId = event.target.dataset.id;
const selectedFieldOption = event.detail.value;
let rules = this.rules.map((rule) => {
if (rule.id == ruleId) {
let selectedField = this.selectedFieldDataType(selectedFieldOption);
let fieldLabel = this.selectedFieldDataPattern.label;
let selectedOperatorOptions = fieldToOperatorsMap[selectedField.toLowerCase()] || [];
return {
...rule,
selectedRuleField: selectedFieldOption,
selectedFieldLabel: fieldLabel,
selectedRuleOperator: "",
selectedRuleValue: "",
isOperatorDisabled: false,
isValueDisabled: true,
selectedFieldCategory: selectedField,
ruleOperatorOptions: selectedOperatorOptions,
};
} else {
return rule;
}
});

this.rules = rules;
}


handleOperatorChange(event) {
const ruleId = event.target.dataset.id;
const selectedOperatorOption = event.detail.value;
let rules = this.rules.map((rule) => {
if (rule.id == ruleId) {
let selectedField = this.selectedFieldDataType(rule.selectedRuleField);
const type = selectedField.toLowerCase();
let isPicklistType = this.selectedFieldDataPattern.isValuePicklistType;
let picklistOptions = this.selectedFieldDataPattern.picklistTypeOptions;
const isNullOrChange = selectedOperatorOption === "isNull";
const isValueInputType = ['string','integer','currency', 'percent', 'date'].includes(type.toLowerCase()) && !isNullOrChange ? true : false;
if (!isValueInputType && !isPicklistType || (isValueInputType && isNullOrChange)) {
return {
...rule,
selectedRuleOperator: selectedOperatorOption,
selectedRuleValue: "",
isValuePicklistType: isPicklistType,
isValueDisabled: false,
ruleValueOptions: ruleValueOptions[selectedOperatorOption] || [],
isType: isValueInputType,
};
} else if (!isValueInputType && isPicklistType) {
return {
...rule,
selectedRuleOperator: selectedOperatorOption,
selectedRuleValue: "",
isValuePicklistType: isPicklistType,
isValueDisabled: false,
ruleValueOptions: picklistOptions || [],
isType: isValueInputType,
};
} else {
return {
...rule,
selectedRuleOperator: selectedOperatorOption,
selectedRuleValue: "",
isValuePicklistType: isPicklistType,
isValueDisabled: false,
selectedOperatorCategory: type,
type: type === "string" ? "text" : ['integer','currency','percent'].includes(type.toLowerCase()) ? 'number': type,
placeholder: type === "date" ? "DD-MMM-YYYY" : "Enter a value",
isType: isValueInputType,
formatter: ['currency','percent'].includes(type.toLowerCase()) ? type: '',
};
}
} else {
return rule;
}
});
this.rules = rules;
}

handleValueChange(event) {
const ruleId = event.target.dataset.id;
const selectedValueOption = event.detail.value;
let updateRule = {};
let rules = this.rules.map((rule) => {
if (rule.id == ruleId) {
return {
...rule,
selectedRuleValue: selectedValueOption,
};
} else {
return rule;
}
});
this.rules = rules;
}

handleRemoveRule(event) {
let ruleId = event.target.dataset.id;
let defaultRules = JSON.parse(JSON.stringify((this.rules)));
if (defaultRules.length > 1) {
defaultRules.splice(ruleId - 1, 1);
defaultRules.forEach(function (item, index) {
item.id = index + 1;
});
this.rules = defaultRules;
}
}

handleAddRules() {
const newRule = {
id: this.rules.length + 1,
selectedRuleField: null,
selectedFieldLabel: '',
selectedRuleOperator: null,
selectedRuleValue: null,
isFieldDisabled: false,
isOperatorDisabled: true,
isValueDisabled: true,
ruleOperatorOptions: [],
selectedFieldCategory: "string",
selectedOperatorCategory: "equals",
type: "text",
placeholder: "Enter a value",
isType: true,
};
this.rules.push(newRule);
}
}

Styling with CSS

.rules-container {
	display: flex;
	align-items: center;
	width: 100%;
}

.main-label-container {
	margin: 17px 0px 0px 20px;
	width: 6%;
}

.rule-logic-label {
	margin: 17px 0px 0px 20px;
}

.main-combobox-container {
	margin: 0 10px;
	width: 34.5% !important;
}

.main-rulelogic-container {
	margin: 0 10px;
	width: 95% !important;
}

.dynamic-content-container {
	margin: 0 10px;
	width: 12%;
}

.text-input-dropdown {
	margin: 0px 10px 0px 0px;
	width: 15%;
	/* line-height: 10px; */
}

.remove-rules {
	margin: 23px 0 0 10px;
	width: 3%;
}

.add-rules {
	line-height: 30px;
	font-size: 15px;
	margin: 10px 0px -10px 0px;
	display: flex;
}

.rules-container-box .icon-wrapper {
	margin-top: 15px;
}

.rules-container-box {
	display: grid;
	align-items: center;
	grid-template-columns: 7% 63.2% auto;
	grid-gap: 10px;
}

Key Features

  • Dynamic Field Selection: The component fetches and categorizes fields dynamically using the getObjectInfo method, providing users with a dropdown for selecting the field to base their filter rule on.
  • Real-time Updates: As users make selections, the component dynamically updates the available options for operators and values, ensuring a smooth and interactive user experience.
  • User-friendly Interface: The component features an intuitive interface with disabled/enabled fields based on user selections, guiding users through the rule creation process step by step.
  • Responsive Design: The CSS styling ensures a responsive design, adapting to various screen sizes and maintaining a consistent layout.

How to Use the Rules Component

  • Add the Component to a Lightning Page: Drag and drop the Rules component onto your desired Lightning page using the Lightning App Builder.
  • Configure Field Filtering: Choose the field for your rule from the dynamic dropdown. Select the operator and set the desired value based on the field type.
  • Create Additional Rules: Click the "Add Rule" button to create additional filter rules. Remove rules using the "Remove Rule" button when needed.
  • Apply and Test: Apply the configured rules to filter Opportunities. Test different scenarios to ensure the rules work as expected.

Output of the Code

image (3).png

image (4).png

image (5).png

image (6).png

Conclusion

The Rules Lightning Web Component provides a powerful and flexible solution for creating dynamic filter rules within Salesforce.

Its dynamic field selection, real-time updates, and user-friendly interface make it a valuable tool for enhancing the filtering capabilities of your Salesforce Opportunities.

Share:
0
+0