Skip to content

Commit fde8d01

Browse files
authored
chore: Adding debounce to onValueChange for input widgets (#40849)
## Description Adding debounce to `onValueChange` for input widgets to fix multiple Execute API calls happening in reactive queries flow. Fixes [#40813](#40813) ## Automation /ok-to-test tags="@tag.All" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/15486342735> > Commit: 6943ba5 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=15486342735&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.All` > Spec: > <hr>Fri, 06 Jun 2025 09:40:52 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Input widgets now update their displayed values instantly while saving changes in the background with a short delay, improving typing responsiveness. - Input changes are grouped and saved after a brief pause, reducing unnecessary updates and enhancing performance. - **Bug Fixes** - Input fields now stay in sync with external updates and clear any pending background updates when needed, preventing outdated or duplicate changes. - **Chores** - Improved cleanup of background processes when input widgets are removed, ensuring smoother operation. - **Tests** - Added typing delays in input simulation during Cypress tests to better mimic real user input timing. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 9c855e3 commit fde8d01

File tree

13 files changed

+338
-92
lines changed

13 files changed

+338
-92
lines changed

app/client/cypress/e2e/Regression/ClientSide/Widgets/Input/Inputv2_spec.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../../../../src/constants/WidgetConstants";
12
import { agHelper } from "../../../../../support/Objects/ObjectsCore";
23

34
const widgetName = "inputwidgetv2";
@@ -385,7 +386,9 @@ describe(
385386
cy.get(widgetInput).clear();
386387
cy.wait(300);
387388
// Input text and hit enter key
388-
cy.get(widgetInput).type("test{enter}");
389+
cy.get(widgetInput).type("test{enter}", {
390+
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
391+
});
389392
// Assert if the Text widget contains the whole value, test
390393
cy.get(".t--widget-textwidget").should("have.text", "test");
391394
});

app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicClientSideData_spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const publishLocators = require("../../../../../locators/publishWidgetspage.json");
22
const commonlocators = require("../../../../../locators/commonlocators.json");
33

4+
import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../../../../src/constants/WidgetConstants";
45
import * as _ from "../../../../../support/Objects/ObjectsCore";
56

67
const widgetSelector = (name) => `[data-widgetname-cy="${name}"]`;
@@ -76,7 +77,7 @@ describe(
7677
cy.get(".t--draggable-inputwidgetv2").each(($inputWidget, index) => {
7778
cy.wrap($inputWidget)
7879
.find("input")
79-
.type(index + 1);
80+
.type(index + 1, { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE });
8081
});
8182

8283
// Verify the typed value
@@ -101,7 +102,7 @@ describe(
101102
cy.get(".t--draggable-inputwidgetv2").each(($inputWidget, index) => {
102103
cy.wrap($inputWidget)
103104
.find("input")
104-
.type(index + 4);
105+
.type(index + 4, { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE });
105106
});
106107

107108
// Verify the typed value

app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_Nested_EventBindings_spec.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../../../../src/constants/WidgetConstants";
12
const nestedListDSL = require("../../../../../fixtures/Listv2/nestedList.json");
23
const commonlocators = require("../../../../../locators/commonlocators.json");
34

@@ -22,10 +23,14 @@ describe(
2223
"{{showAlert(`${level_1.currentView.Text1.text} _ ${level_1.currentItem.id} _ ${level_1.currentIndex} _ ${level_1.currentView.Input1.text} _ ${currentView.Input2.text}`)}}",
2324
);
2425
// Enter text in the parent list widget's text input
25-
cy.get(widgetSelector("Input1")).find("input").type("outer input");
26+
cy.get(widgetSelector("Input1"))
27+
.find("input")
28+
.type("outer input", { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE });
2629

2730
// Enter text in the child list widget's text input in first row
28-
cy.get(widgetSelector("Input2")).find("input").type("inner input");
31+
cy.get(widgetSelector("Input2"))
32+
.find("input")
33+
.type("inner input", { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE });
2934

3035
// click the button on inner list 1st row.
3136
cy.get(widgetSelector("Button3")).find("button").click({ force: true });
@@ -40,13 +45,17 @@ describe(
4045
cy.get(widgetSelector("Input1"))
4146
.find("input")
4247
.clear()
43-
.type("outer input updated");
48+
.type("outer input updated", {
49+
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
50+
});
4451

4552
// Enter text in the child list widget's text input in first row
4653
cy.get(widgetSelector("Input2"))
4754
.find("input")
4855
.clear()
49-
.type("inner input updated");
56+
.type("inner input updated", {
57+
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
58+
});
5059

5160
// click the button on inner list 1st row.
5261
cy.get(widgetSelector("Button3")).find("button").click({ force: true });

app/client/cypress/support/Pages/AggregateHelper.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { EntityItems } from "./AssertHelper";
77
import EditorNavigator from "./EditorNavigation";
88
import { EntityType } from "./EditorNavigation";
99
import ClickOptions = Cypress.ClickOptions;
10+
import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../src/constants/WidgetConstants";
1011

1112
type ElementType = string | JQuery<HTMLElement>;
1213

@@ -945,10 +946,13 @@ export class AggregateHelper {
945946
.focus()
946947
.type("{backspace}".repeat(charCount), { timeout: 2, force: true })
947948
.wait(50)
948-
.type(totype);
949+
.type(totype, { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE });
949950
else {
950951
if (charCount == -1) this.GetElement(selector).eq(index).clear();
951-
this.TypeText(selector, totype, index);
952+
this.TypeText(selector, totype, {
953+
index,
954+
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
955+
});
952956
}
953957
}
954958

@@ -973,7 +977,10 @@ export class AggregateHelper {
973977
force = false,
974978
) {
975979
this.ClearTextField(selector, force, index);
976-
return this.TypeText(selector, totype, index);
980+
return this.TypeText(selector, totype, {
981+
index,
982+
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
983+
});
977984
}
978985

979986
public TypeText(
@@ -1323,7 +1330,10 @@ export class AggregateHelper {
13231330
toClear && this.ClearInputText(name);
13241331
cy.xpath(this.locator._inputWidgetValueField(name, isInput))
13251332
.trigger("click")
1326-
.type(input, { parseSpecialCharSequences: false });
1333+
.type(input, {
1334+
parseSpecialCharSequences: false,
1335+
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
1336+
});
13271337
}
13281338

13291339
public ClearInputText(name: string, isInput = true) {

app/client/src/constants/WidgetConstants.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,6 @@ export type PasteWidgetReduxAction = {
279279
groupWidgets: boolean;
280280
existingWidgets?: unknown;
281281
} & EitherMouseLocationORGridPosition;
282+
283+
// Constant for debouncing the input change to avoid multiple Execute calls in reactive flow
284+
export const DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE = 300;

app/client/src/widgets/CurrencyInputWidget/widget/index.tsx

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
getCountryCodeFromCurrencyCode,
1313
} from "../component/CurrencyCodeDropdown";
1414
import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
15-
import _ from "lodash";
15+
import _, { debounce } from "lodash";
1616
import derivedProperties from "./parsedDerivedProperties";
1717
import BaseInputWidget from "widgets/BaseInputWidget";
1818
import type { BaseInputWidgetProps } from "widgets/BaseInputWidget/widget";
@@ -42,7 +42,10 @@ import { DynamicHeight } from "utils/WidgetFeatures";
4242
import { getDefaultCurrency } from "../component/CurrencyCodeDropdown";
4343
import IconSVG from "../icon.svg";
4444
import ThumbnailSVG from "../thumbnail.svg";
45-
import { WIDGET_TAGS } from "constants/WidgetConstants";
45+
import {
46+
DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
47+
WIDGET_TAGS,
48+
} from "constants/WidgetConstants";
4649
import { appsmithTelemetry } from "instrumentation";
4750

4851
export function defaultValueValidation(
@@ -154,10 +157,19 @@ export function defaultValueValidation(
154157
};
155158
}
156159

160+
interface CurrencyInputWidgetState extends WidgetState {
161+
inputValue: string;
162+
}
163+
157164
class CurrencyInputWidget extends BaseInputWidget<
158165
CurrencyInputWidgetProps,
159-
WidgetState
166+
CurrencyInputWidgetState
160167
> {
168+
constructor(props: CurrencyInputWidgetProps) {
169+
super(props);
170+
this.state = { inputValue: props.text ?? "" };
171+
}
172+
161173
static type = "CURRENCY_INPUT_WIDGET";
162174

163175
static getConfig() {
@@ -457,6 +469,12 @@ class CurrencyInputWidget extends BaseInputWidget<
457469
}
458470

459471
componentDidUpdate(prevProps: CurrencyInputWidgetProps) {
472+
if (prevProps.text !== this.props.text) {
473+
this.setState({ inputValue: this.props.text ?? "" });
474+
// Cancel any pending debounced calls when value is updated externally
475+
this.debouncedOnValueChange.cancel();
476+
}
477+
460478
if (
461479
prevProps.text !== this.props.text &&
462480
!this.props.isFocused &&
@@ -481,6 +499,10 @@ class CurrencyInputWidget extends BaseInputWidget<
481499
}
482500
}
483501

502+
componentWillUnmount() {
503+
this.debouncedOnValueChange.cancel();
504+
}
505+
484506
formatText() {
485507
if (!!this.props.text && !this.isTextFormatted()) {
486508
try {
@@ -506,6 +528,22 @@ class CurrencyInputWidget extends BaseInputWidget<
506528
}
507529
}
508530

531+
// debouncing the input change to avoid multiple Execute calls in reactive flow
532+
debouncedOnValueChange = debounce((value: string) => {
533+
// text is stored as what user has typed
534+
this.props.updateWidgetMetaProperty("text", String(value), {
535+
triggerPropertyName: "onTextChanged",
536+
dynamicString: this.props.onTextChanged,
537+
event: {
538+
type: EventType.ON_TEXT_CHANGE,
539+
},
540+
});
541+
542+
if (!this.props.isDirty) {
543+
this.props.updateWidgetMetaProperty("isDirty", true);
544+
}
545+
}, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE);
546+
509547
onValueChange = (value: string) => {
510548
let formattedValue = "";
511549
const decimalSeperator = getLocaleDecimalSeperator();
@@ -524,18 +562,8 @@ class CurrencyInputWidget extends BaseInputWidget<
524562
});
525563
}
526564

527-
// text is stored as what user has typed
528-
this.props.updateWidgetMetaProperty("text", String(formattedValue), {
529-
triggerPropertyName: "onTextChanged",
530-
dynamicString: this.props.onTextChanged,
531-
event: {
532-
type: EventType.ON_TEXT_CHANGE,
533-
},
534-
});
535-
536-
if (!this.props.isDirty) {
537-
this.props.updateWidgetMetaProperty("isDirty", true);
538-
}
565+
this.setState({ inputValue: formattedValue });
566+
this.debouncedOnValueChange(formattedValue);
539567
};
540568

541569
isTextFormatted = () => {
@@ -623,7 +651,7 @@ class CurrencyInputWidget extends BaseInputWidget<
623651
};
624652

625653
getWidgetView() {
626-
const value = this.props.text ?? "";
654+
const value = this.state.inputValue ?? "";
627655
const isInvalid =
628656
"isValid" in this.props && !this.props.isValid && !!this.props.isDirty;
629657
const currencyCode = this.props.currencyCode;

app/client/src/widgets/InputWidget/widget/index.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { ExecutionResult } from "constants/AppsmithActionConstants/ActionCo
1818
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
1919
import type { TextSize } from "constants/WidgetConstants";
2020
import {
21+
DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
2122
GridDefaults,
2223
RenderModes,
2324
WIDGET_TAGS,
@@ -47,6 +48,7 @@ import {
4748
import type { InputType } from "../constants";
4849
import { InputTypes } from "../constants";
4950
import IconSVG from "../icon.svg";
51+
import { debounce } from "lodash";
5052

5153
export function defaultValueValidation(
5254
// TODO: Fix this the next time the file is edited
@@ -141,14 +143,31 @@ export function defaultValueValidation(
141143
};
142144
}
143145

144-
class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
146+
interface InputWidgetState extends WidgetState {
147+
inputValue: string;
148+
}
149+
150+
class InputWidget extends BaseWidget<InputWidgetProps, InputWidgetState> {
145151
constructor(props: InputWidgetProps) {
146152
super(props);
147153
this.state = {
148154
text: props.text,
155+
inputValue: props.text ?? "",
149156
};
150157
}
151158

159+
componentDidUpdate(prevProps: InputWidgetProps) {
160+
if (prevProps.text !== this.props.text) {
161+
this.setState({ inputValue: this.props.text ?? "" });
162+
// Cancel any pending debounced calls when value is updated externally
163+
this.debouncedOnValueChange.cancel();
164+
}
165+
}
166+
167+
componentWillUnmount() {
168+
this.debouncedOnValueChange.cancel();
169+
}
170+
152171
static type = "INPUT_WIDGET";
153172

154173
static getConfig() {
@@ -877,7 +896,8 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
877896
};
878897
}
879898

880-
onValueChange = (value: string) => {
899+
// debouncing the input change to avoid multiple Execute calls in reactive flow
900+
debouncedOnValueChange = debounce((value: string) => {
881901
this.props.updateWidgetMetaProperty("text", value, {
882902
triggerPropertyName: "onTextChanged",
883903
dynamicString: this.props.onTextChanged,
@@ -889,6 +909,11 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
889909
if (!this.props.isDirty) {
890910
this.props.updateWidgetMetaProperty("isDirty", true);
891911
}
912+
}, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE);
913+
914+
onValueChange = (value: string) => {
915+
this.setState({ inputValue: value });
916+
this.debouncedOnValueChange(value);
892917
};
893918

894919
onCurrencyTypeChange = (code?: string) => {
@@ -967,12 +992,13 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
967992

968993
getFormattedText = () => {
969994
if (this.props.isFocused || this.props.inputType !== InputTypes.CURRENCY) {
970-
return this.props.text !== undefined ? this.props.text : "";
995+
return this.state.inputValue !== undefined ? this.state.inputValue : "";
971996
}
972997

973-
if (this.props.text === "" || this.props.text === undefined) return "";
998+
if (this.state.inputValue === "" || this.state.inputValue === undefined)
999+
return "";
9741000

975-
const valueToFormat = String(this.props.text);
1001+
const valueToFormat = String(this.state.inputValue);
9761002

9771003
const locale = getLocale();
9781004
const decimalSeparator = getDecimalSeparator(locale);

0 commit comments

Comments
 (0)