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](https://github.com/appsmithorg/appsmith/issues/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: 6943ba5d0df915256cf29831df53e9ff9880d617
> <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 -->
This commit is contained in:
Ankita Kinger 2025-06-06 15:51:36 +05:30 committed by GitHub
parent 9c855e36a9
commit fde8d013aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 338 additions and 92 deletions

View File

@ -1,3 +1,4 @@
import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../../../../src/constants/WidgetConstants";
import { agHelper } from "../../../../../support/Objects/ObjectsCore";
const widgetName = "inputwidgetv2";
@ -385,7 +386,9 @@ describe(
cy.get(widgetInput).clear();
cy.wait(300);
// Input text and hit enter key
cy.get(widgetInput).type("test{enter}");
cy.get(widgetInput).type("test{enter}", {
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
});
// Assert if the Text widget contains the whole value, test
cy.get(".t--widget-textwidget").should("have.text", "test");
});

View File

@ -1,6 +1,7 @@
const publishLocators = require("../../../../../locators/publishWidgetspage.json");
const commonlocators = require("../../../../../locators/commonlocators.json");
import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../../../../src/constants/WidgetConstants";
import * as _ from "../../../../../support/Objects/ObjectsCore";
const widgetSelector = (name) => `[data-widgetname-cy="${name}"]`;
@ -76,7 +77,7 @@ describe(
cy.get(".t--draggable-inputwidgetv2").each(($inputWidget, index) => {
cy.wrap($inputWidget)
.find("input")
.type(index + 1);
.type(index + 1, { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE });
});
// Verify the typed value
@ -101,7 +102,7 @@ describe(
cy.get(".t--draggable-inputwidgetv2").each(($inputWidget, index) => {
cy.wrap($inputWidget)
.find("input")
.type(index + 4);
.type(index + 4, { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE });
});
// Verify the typed value

View File

@ -1,3 +1,4 @@
import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../../../../src/constants/WidgetConstants";
const nestedListDSL = require("../../../../../fixtures/Listv2/nestedList.json");
const commonlocators = require("../../../../../locators/commonlocators.json");
@ -22,10 +23,14 @@ describe(
"{{showAlert(`${level_1.currentView.Text1.text} _ ${level_1.currentItem.id} _ ${level_1.currentIndex} _ ${level_1.currentView.Input1.text} _ ${currentView.Input2.text}`)}}",
);
// Enter text in the parent list widget's text input
cy.get(widgetSelector("Input1")).find("input").type("outer input");
cy.get(widgetSelector("Input1"))
.find("input")
.type("outer input", { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE });
// Enter text in the child list widget's text input in first row
cy.get(widgetSelector("Input2")).find("input").type("inner input");
cy.get(widgetSelector("Input2"))
.find("input")
.type("inner input", { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE });
// click the button on inner list 1st row.
cy.get(widgetSelector("Button3")).find("button").click({ force: true });
@ -40,13 +45,17 @@ describe(
cy.get(widgetSelector("Input1"))
.find("input")
.clear()
.type("outer input updated");
.type("outer input updated", {
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
});
// Enter text in the child list widget's text input in first row
cy.get(widgetSelector("Input2"))
.find("input")
.clear()
.type("inner input updated");
.type("inner input updated", {
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
});
// click the button on inner list 1st row.
cy.get(widgetSelector("Button3")).find("button").click({ force: true });

View File

@ -7,6 +7,7 @@ import { EntityItems } from "./AssertHelper";
import EditorNavigator from "./EditorNavigation";
import { EntityType } from "./EditorNavigation";
import ClickOptions = Cypress.ClickOptions;
import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../src/constants/WidgetConstants";
type ElementType = string | JQuery<HTMLElement>;
@ -945,10 +946,13 @@ export class AggregateHelper {
.focus()
.type("{backspace}".repeat(charCount), { timeout: 2, force: true })
.wait(50)
.type(totype);
.type(totype, { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE });
else {
if (charCount == -1) this.GetElement(selector).eq(index).clear();
this.TypeText(selector, totype, index);
this.TypeText(selector, totype, {
index,
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
});
}
}
@ -973,7 +977,10 @@ export class AggregateHelper {
force = false,
) {
this.ClearTextField(selector, force, index);
return this.TypeText(selector, totype, index);
return this.TypeText(selector, totype, {
index,
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
});
}
public TypeText(
@ -1323,7 +1330,10 @@ export class AggregateHelper {
toClear && this.ClearInputText(name);
cy.xpath(this.locator._inputWidgetValueField(name, isInput))
.trigger("click")
.type(input, { parseSpecialCharSequences: false });
.type(input, {
parseSpecialCharSequences: false,
delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
});
}
public ClearInputText(name: string, isInput = true) {

View File

@ -279,3 +279,6 @@ export type PasteWidgetReduxAction = {
groupWidgets: boolean;
existingWidgets?: unknown;
} & EitherMouseLocationORGridPosition;
// Constant for debouncing the input change to avoid multiple Execute calls in reactive flow
export const DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE = 300;

View File

@ -12,7 +12,7 @@ import {
getCountryCodeFromCurrencyCode,
} from "../component/CurrencyCodeDropdown";
import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
import _ from "lodash";
import _, { debounce } from "lodash";
import derivedProperties from "./parsedDerivedProperties";
import BaseInputWidget from "widgets/BaseInputWidget";
import type { BaseInputWidgetProps } from "widgets/BaseInputWidget/widget";
@ -42,7 +42,10 @@ import { DynamicHeight } from "utils/WidgetFeatures";
import { getDefaultCurrency } from "../component/CurrencyCodeDropdown";
import IconSVG from "../icon.svg";
import ThumbnailSVG from "../thumbnail.svg";
import { WIDGET_TAGS } from "constants/WidgetConstants";
import {
DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
WIDGET_TAGS,
} from "constants/WidgetConstants";
import { appsmithTelemetry } from "instrumentation";
export function defaultValueValidation(
@ -154,10 +157,19 @@ export function defaultValueValidation(
};
}
interface CurrencyInputWidgetState extends WidgetState {
inputValue: string;
}
class CurrencyInputWidget extends BaseInputWidget<
CurrencyInputWidgetProps,
WidgetState
CurrencyInputWidgetState
> {
constructor(props: CurrencyInputWidgetProps) {
super(props);
this.state = { inputValue: props.text ?? "" };
}
static type = "CURRENCY_INPUT_WIDGET";
static getConfig() {
@ -457,6 +469,12 @@ class CurrencyInputWidget extends BaseInputWidget<
}
componentDidUpdate(prevProps: CurrencyInputWidgetProps) {
if (prevProps.text !== this.props.text) {
this.setState({ inputValue: this.props.text ?? "" });
// Cancel any pending debounced calls when value is updated externally
this.debouncedOnValueChange.cancel();
}
if (
prevProps.text !== this.props.text &&
!this.props.isFocused &&
@ -481,6 +499,10 @@ class CurrencyInputWidget extends BaseInputWidget<
}
}
componentWillUnmount() {
this.debouncedOnValueChange.cancel();
}
formatText() {
if (!!this.props.text && !this.isTextFormatted()) {
try {
@ -506,6 +528,22 @@ class CurrencyInputWidget extends BaseInputWidget<
}
}
// debouncing the input change to avoid multiple Execute calls in reactive flow
debouncedOnValueChange = debounce((value: string) => {
// text is stored as what user has typed
this.props.updateWidgetMetaProperty("text", String(value), {
triggerPropertyName: "onTextChanged",
dynamicString: this.props.onTextChanged,
event: {
type: EventType.ON_TEXT_CHANGE,
},
});
if (!this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", true);
}
}, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE);
onValueChange = (value: string) => {
let formattedValue = "";
const decimalSeperator = getLocaleDecimalSeperator();
@ -524,18 +562,8 @@ class CurrencyInputWidget extends BaseInputWidget<
});
}
// text is stored as what user has typed
this.props.updateWidgetMetaProperty("text", String(formattedValue), {
triggerPropertyName: "onTextChanged",
dynamicString: this.props.onTextChanged,
event: {
type: EventType.ON_TEXT_CHANGE,
},
});
if (!this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", true);
}
this.setState({ inputValue: formattedValue });
this.debouncedOnValueChange(formattedValue);
};
isTextFormatted = () => {
@ -623,7 +651,7 @@ class CurrencyInputWidget extends BaseInputWidget<
};
getWidgetView() {
const value = this.props.text ?? "";
const value = this.state.inputValue ?? "";
const isInvalid =
"isValid" in this.props && !this.props.isValid && !!this.props.isDirty;
const currencyCode = this.props.currencyCode;

View File

@ -18,6 +18,7 @@ import type { ExecutionResult } from "constants/AppsmithActionConstants/ActionCo
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import type { TextSize } from "constants/WidgetConstants";
import {
DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
GridDefaults,
RenderModes,
WIDGET_TAGS,
@ -47,6 +48,7 @@ import {
import type { InputType } from "../constants";
import { InputTypes } from "../constants";
import IconSVG from "../icon.svg";
import { debounce } from "lodash";
export function defaultValueValidation(
// TODO: Fix this the next time the file is edited
@ -141,14 +143,31 @@ export function defaultValueValidation(
};
}
class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
interface InputWidgetState extends WidgetState {
inputValue: string;
}
class InputWidget extends BaseWidget<InputWidgetProps, InputWidgetState> {
constructor(props: InputWidgetProps) {
super(props);
this.state = {
text: props.text,
inputValue: props.text ?? "",
};
}
componentDidUpdate(prevProps: InputWidgetProps) {
if (prevProps.text !== this.props.text) {
this.setState({ inputValue: this.props.text ?? "" });
// Cancel any pending debounced calls when value is updated externally
this.debouncedOnValueChange.cancel();
}
}
componentWillUnmount() {
this.debouncedOnValueChange.cancel();
}
static type = "INPUT_WIDGET";
static getConfig() {
@ -877,7 +896,8 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
};
}
onValueChange = (value: string) => {
// debouncing the input change to avoid multiple Execute calls in reactive flow
debouncedOnValueChange = debounce((value: string) => {
this.props.updateWidgetMetaProperty("text", value, {
triggerPropertyName: "onTextChanged",
dynamicString: this.props.onTextChanged,
@ -889,6 +909,11 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
if (!this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", true);
}
}, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE);
onValueChange = (value: string) => {
this.setState({ inputValue: value });
this.debouncedOnValueChange(value);
};
onCurrencyTypeChange = (code?: string) => {
@ -967,12 +992,13 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
getFormattedText = () => {
if (this.props.isFocused || this.props.inputType !== InputTypes.CURRENCY) {
return this.props.text !== undefined ? this.props.text : "";
return this.state.inputValue !== undefined ? this.state.inputValue : "";
}
if (this.props.text === "" || this.props.text === undefined) return "";
if (this.state.inputValue === "" || this.state.inputValue === undefined)
return "";
const valueToFormat = String(this.props.text);
const valueToFormat = String(this.state.inputValue);
const locale = getLocale();
const decimalSeparator = getDecimalSeparator(locale);

View File

@ -18,7 +18,7 @@ import {
INPUT_TEXT_MAX_CHAR_ERROR,
} from "ee/constants/messages";
import type { SetterConfig, Stylesheet } from "entities/AppTheming";
import { isNil, isNumber, merge, toString } from "lodash";
import { debounce, isNil, isNumber, merge, toString } from "lodash";
import React from "react";
import { DynamicHeight } from "utils/WidgetFeatures";
import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
@ -48,7 +48,10 @@ import type {
PropertyUpdates,
SnipingModeProperty,
} from "WidgetProvider/constants";
import { WIDGET_TAGS } from "constants/WidgetConstants";
import {
DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
WIDGET_TAGS,
} from "constants/WidgetConstants";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { ResponsiveBehavior } from "layoutSystems/common/utils/constants";
@ -299,11 +302,16 @@ function InputTypeUpdateHook(
return updates;
}
class InputWidget extends BaseInputWidget<InputWidgetProps, WidgetState> {
interface InputWidgetState extends WidgetState {
inputValue: string;
}
class InputWidget extends BaseInputWidget<InputWidgetProps, InputWidgetState> {
constructor(props: InputWidgetProps) {
super(props);
this.state = {
isFocused: false,
inputValue: props.inputText ?? "",
};
}
@ -706,6 +714,12 @@ class InputWidget extends BaseInputWidget<InputWidgetProps, WidgetState> {
};
componentDidUpdate = (prevProps: InputWidgetProps) => {
if (prevProps.inputText !== this.props.inputText) {
this.setState({ inputValue: this.props.inputText ?? "" });
// Cancel any pending debounced calls when value is updated externally
this.debouncedOnValueChange.cancel();
}
if (
prevProps.inputText !== this.props.inputText &&
this.props.inputText !== toString(this.props.text)
@ -732,7 +746,12 @@ class InputWidget extends BaseInputWidget<InputWidgetProps, WidgetState> {
}
};
onValueChange = (value: string) => {
componentWillUnmount() {
this.debouncedOnValueChange.cancel();
}
// debouncing the input change to avoid multiple Execute calls in reactive flow
debouncedOnValueChange = debounce((value: string) => {
/*
* Ideally text property should be derived property. But widgets
* with derived properties won't work as expected inside a List
@ -755,6 +774,11 @@ class InputWidget extends BaseInputWidget<InputWidgetProps, WidgetState> {
if (!this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", true);
}
}, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE);
onValueChange = (value: string) => {
this.setState({ inputValue: value });
this.debouncedOnValueChange(value);
};
static getSetterConfig(): SetterConfig {
@ -790,7 +814,7 @@ class InputWidget extends BaseInputWidget<InputWidgetProps, WidgetState> {
};
getWidgetView() {
const value = this.props.inputText ?? "";
const value = this.state.inputValue ?? "";
let isInvalid = false;
if (this.props.isDirty) {

View File

@ -12,7 +12,7 @@ import {
ISDCodeDropdownOptions,
} from "../component/ISDCodeDropdown";
import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
import _ from "lodash";
import _, { debounce } from "lodash";
import BaseInputWidget from "widgets/BaseInputWidget";
import derivedProperties from "./parsedDerivedProperties";
import type { BaseInputWidgetProps } from "widgets/BaseInputWidget/widget";
@ -37,7 +37,10 @@ import { DynamicHeight } from "utils/WidgetFeatures";
import { getDefaultISDCode } from "../component/ISDCodeDropdown";
import IconSVG from "../icon.svg";
import ThumbnailSVG from "../thumbnail.svg";
import { WIDGET_TAGS } from "constants/WidgetConstants";
import {
DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
WIDGET_TAGS,
} from "constants/WidgetConstants";
import { appsmithTelemetry } from "instrumentation";
export function defaultValueValidation(
@ -84,10 +87,21 @@ export function defaultValueValidation(
};
}
interface PhoneWidgetState extends WidgetState {
inputValue: string;
}
class PhoneInputWidget extends BaseInputWidget<
PhoneInputWidgetProps,
WidgetState
PhoneWidgetState
> {
constructor(props: PhoneInputWidgetProps) {
super(props);
this.state = {
inputValue: props.text ?? "",
};
}
static type = "PHONE_INPUT_WIDGET";
static getConfig() {
@ -356,6 +370,12 @@ class PhoneInputWidget extends BaseInputWidget<
}
componentDidUpdate(prevProps: PhoneInputWidgetProps) {
if (prevProps.text !== this.props.text) {
this.setState({ inputValue: this.props.text ?? "" });
// Cancel any pending debounced calls when value is updated externally
this.debouncedOnValueChange.cancel();
}
if (prevProps.dialCode !== this.props.dialCode) {
this.onISDCodeChange(this.props.dialCode);
}
@ -390,6 +410,10 @@ class PhoneInputWidget extends BaseInputWidget<
}
}
componentWillUnmount() {
this.debouncedOnValueChange.cancel();
}
onISDCodeChange = (dialCode?: string) => {
const countryCode = getCountryCode(dialCode);
@ -403,21 +427,13 @@ class PhoneInputWidget extends BaseInputWidget<
}
};
onValueChange = (value: string) => {
let formattedValue;
// Don't format, as value is typed, when user is deleting
if (value && value.length > this.props.text?.length) {
formattedValue = this.getFormattedPhoneNumber(value);
} else {
formattedValue = value;
}
// debouncing the input change to avoid multiple Execute calls in reactive flow
debouncedOnValueChange = debounce((value: string) => {
this.props.updateWidgetMetaProperty(
"value",
parseIncompletePhoneNumber(formattedValue),
parseIncompletePhoneNumber(value),
);
this.props.updateWidgetMetaProperty("text", formattedValue, {
this.props.updateWidgetMetaProperty("text", value, {
triggerPropertyName: "onTextChanged",
dynamicString: this.props.onTextChanged,
event: {
@ -428,6 +444,20 @@ class PhoneInputWidget extends BaseInputWidget<
if (!this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", true);
}
}, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE);
onValueChange = (value: string) => {
let formattedValue;
// Don't format, as value is typed, when user is deleting
if (value && value.length > this.props.text?.length) {
formattedValue = this.getFormattedPhoneNumber(value);
} else {
formattedValue = value;
}
this.setState({ inputValue: formattedValue });
this.debouncedOnValueChange(formattedValue);
};
handleFocusChange = (focusState: boolean) => {
@ -487,7 +517,7 @@ class PhoneInputWidget extends BaseInputWidget<
}
getWidgetView() {
const value = this.props.text ?? "";
const value = this.state.inputValue;
const isInvalid =
"isValid" in this.props && !this.props.isValid && !!this.props.isDirty;
const countryCode = this.props.countryCode;

View File

@ -34,7 +34,11 @@ import type {
SnipingModeProperty,
PropertyUpdates,
} from "WidgetProvider/constants";
import { WIDGET_TAGS } from "constants/WidgetConstants";
import {
DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE,
WIDGET_TAGS,
} from "constants/WidgetConstants";
import { debounce } from "lodash";
export enum RTEFormats {
MARKDOWN = "markdown",
@ -48,10 +52,21 @@ const RichTextEditorComponent = lazy(async () =>
const converter = new showdown.Converter();
interface RichTextEditorWidgetState extends WidgetState {
inputValue: string;
}
class RichTextEditorWidget extends BaseWidget<
RichTextEditorWidgetProps,
WidgetState
RichTextEditorWidgetState
> {
constructor(props: RichTextEditorWidgetProps) {
super(props);
this.state = {
inputValue: props.text ?? "",
};
}
static type = "RICH_TEXT_EDITOR_WIDGET";
static getConfig() {
@ -491,6 +506,12 @@ class RichTextEditorWidget extends BaseWidget<
}
componentDidUpdate(prevProps: RichTextEditorWidgetProps): void {
if (prevProps.text !== this.props.text) {
this.setState({ inputValue: this.props.text ?? "" });
// Cancel any pending debounced calls when value is updated externally
this.debouncedOnValueChange.cancel();
}
if (this.props.defaultText !== prevProps.defaultText) {
if (this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", false);
@ -498,7 +519,12 @@ class RichTextEditorWidget extends BaseWidget<
}
}
onValueChange = (text: string) => {
componentWillUnmount() {
this.debouncedOnValueChange.cancel();
}
// debouncing the input change to avoid multiple Execute calls in reactive flow
debouncedOnValueChange = debounce((text: string) => {
if (!this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", true);
}
@ -510,6 +536,11 @@ class RichTextEditorWidget extends BaseWidget<
type: EventType.ON_TEXT_CHANGE,
},
});
}, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE);
onValueChange = (text: string) => {
this.setState({ inputValue: text });
this.debouncedOnValueChange(text);
};
static getSetterConfig(): SetterConfig {
@ -532,7 +563,7 @@ class RichTextEditorWidget extends BaseWidget<
}
getWidgetView() {
let value = this.props.text ?? "";
let value = this.state.inputValue;
if (this.props.inputType === RTEFormats.MARKDOWN) {
value = converter.makeHtml(value);

View File

@ -1,4 +1,4 @@
import _ from "lodash";
import _, { debounce } from "lodash";
import React from "react";
import log from "loglevel";
import type { WidgetState } from "widgets/BaseWidget";
@ -30,11 +30,20 @@ import { WDSBaseInputWidget } from "widgets/wds/WDSBaseInputWidget";
import { getCountryCodeFromCurrencyCode, validateInput } from "./helpers";
import type { KeyDownEvent } from "widgets/wds/WDSBaseInputWidget/component/types";
import { appsmithTelemetry } from "instrumentation";
import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "constants/WidgetConstants";
interface WDSCurrencyInputWidgetState extends WidgetState {
inputValue: string;
}
class WDSCurrencyInputWidget extends WDSBaseInputWidget<
CurrencyInputWidgetProps,
WidgetState
WDSCurrencyInputWidgetState
> {
constructor(props: CurrencyInputWidgetProps) {
super(props);
this.state = { inputValue: props.rawText ?? "" };
}
static type = "WDS_CURRENCY_INPUT_WIDGET";
static getConfig() {
@ -152,6 +161,12 @@ class WDSCurrencyInputWidget extends WDSBaseInputWidget<
}
componentDidUpdate(prevProps: CurrencyInputWidgetProps) {
if (prevProps.rawText !== this.props.rawText) {
this.setState({ inputValue: this.props.rawText ?? "" });
// Cancel any pending debounced calls when value is updated externally
this.debouncedOnValueChange.cancel();
}
if (
prevProps.text !== this.props.text &&
!this.props.isFocused &&
@ -176,6 +191,27 @@ class WDSCurrencyInputWidget extends WDSBaseInputWidget<
}
}
componentWillUnmount(): void {
this.debouncedOnValueChange.cancel();
}
// debouncing the input change to avoid multiple Execute calls in reactive flow
debouncedOnValueChange = debounce((value: string, formattedValue: string) => {
this.props.updateWidgetMetaProperty("text", String(formattedValue));
this.props.updateWidgetMetaProperty("rawText", value, {
triggerPropertyName: "onTextChanged",
dynamicString: this.props.onTextChanged,
event: {
type: EventType.ON_TEXT_CHANGE,
},
});
if (!this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", true);
}
}, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE);
onValueChange = (value: string) => {
let formattedValue = "";
const decimalSeperator = getLocaleDecimalSeperator();
@ -194,19 +230,8 @@ class WDSCurrencyInputWidget extends WDSBaseInputWidget<
});
}
this.props.updateWidgetMetaProperty("text", String(formattedValue));
this.props.updateWidgetMetaProperty("rawText", value, {
triggerPropertyName: "onTextChanged",
dynamicString: this.props.onTextChanged,
event: {
type: EventType.ON_TEXT_CHANGE,
},
});
if (!this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", true);
}
this.setState({ inputValue: formattedValue });
this.debouncedOnValueChange(value, formattedValue);
};
onFocusChange = (isFocused?: boolean) => {
@ -323,7 +348,7 @@ class WDSCurrencyInputWidget extends WDSBaseInputWidget<
}
getWidgetView() {
const value = this.props.rawText ?? "";
const value = this.state.inputValue;
const validation = validateInput(this.props);
return (

View File

@ -1,5 +1,5 @@
import React from "react";
import { isNumber, merge, toString } from "lodash";
import { debounce, isNumber, merge, toString } from "lodash";
import * as config from "../config";
import InputComponent from "../component";
import type { InputWidgetProps } from "./types";
@ -14,8 +14,21 @@ import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import type { KeyDownEvent } from "widgets/wds/WDSBaseInputWidget/component/types";
import type { WidgetBaseConfiguration } from "WidgetProvider/constants";
import { INPUT_TYPES } from "widgets/wds/WDSBaseInputWidget/constants";
import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "constants/WidgetConstants";
interface WDSInputWidgetState extends WidgetState {
inputValue: string;
}
class WDSInputWidget extends WDSBaseInputWidget<
InputWidgetProps,
WDSInputWidgetState
> {
constructor(props: InputWidgetProps) {
super(props);
this.state = { inputValue: props.rawText ?? "" };
}
class WDSInputWidget extends WDSBaseInputWidget<InputWidgetProps, WidgetState> {
static type = "WDS_INPUT_WIDGET";
static getConfig(): WidgetBaseConfiguration {
@ -155,6 +168,12 @@ class WDSInputWidget extends WDSBaseInputWidget<InputWidgetProps, WidgetState> {
componentDidUpdate = (prevProps: InputWidgetProps) => {
const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props;
if (prevProps.rawText !== this.props.rawText) {
this.setState({ inputValue: this.props.rawText ?? "" });
// Cancel any pending debounced calls when value is updated externally
this.debouncedOnValueChange.cancel();
}
if (
prevProps.rawText !== this.props.rawText &&
this.props.rawText !== toString(this.props.text)
@ -183,7 +202,12 @@ class WDSInputWidget extends WDSBaseInputWidget<InputWidgetProps, WidgetState> {
commitBatchMetaUpdates();
};
onValueChange = (value: string) => {
componentWillUnmount(): void {
this.debouncedOnValueChange.cancel();
}
// debouncing the input change to avoid multiple Execute calls in reactive flow
debouncedOnValueChange = debounce((value: string) => {
const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props;
// Ideally text property should be derived property. But widgets with
@ -205,6 +229,11 @@ class WDSInputWidget extends WDSBaseInputWidget<InputWidgetProps, WidgetState> {
}
commitBatchMetaUpdates();
}, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE);
onValueChange = (value: string) => {
this.setState({ inputValue: value });
this.debouncedOnValueChange(value);
};
resetWidgetText = () => {
@ -226,9 +255,9 @@ class WDSInputWidget extends WDSBaseInputWidget<InputWidgetProps, WidgetState> {
};
getWidgetView() {
const { inputType, rawText } = this.props;
const { inputType } = this.props;
const value = rawText ?? "";
const value = this.state.inputValue;
const { errorMessage, validationStatus } = validateInput(this.props);
return (

View File

@ -21,11 +21,22 @@ import { PhoneInputComponent } from "../component";
import type { PhoneInputWidgetProps } from "./types";
import { getCountryCode, validateInput } from "./helpers";
import { appsmithTelemetry } from "instrumentation";
import { debounce } from "lodash";
import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "constants/WidgetConstants";
interface WDSPhoneInputWidgetState extends WidgetState {
inputValue: string;
}
class WDSPhoneInputWidget extends WDSBaseInputWidget<
PhoneInputWidgetProps,
WidgetState
WDSPhoneInputWidgetState
> {
constructor(props: PhoneInputWidgetProps) {
super(props);
this.state = { inputValue: props.text ?? "" };
}
static type = "WDS_PHONE_INPUT_WIDGET";
static getConfig() {
@ -171,6 +182,12 @@ class WDSPhoneInputWidget extends WDSBaseInputWidget<
}
componentDidUpdate(prevProps: PhoneInputWidgetProps) {
if (prevProps.text !== this.props.text) {
this.setState({ inputValue: this.props.text ?? "" });
// Cancel any pending debounced calls when value is updated externally
this.debouncedOnValueChange.cancel();
}
if (prevProps.dialCode !== this.props.dialCode) {
this.onISDCodeChange(this.props.dialCode);
}
@ -205,6 +222,10 @@ class WDSPhoneInputWidget extends WDSBaseInputWidget<
}
}
componentWillUnmount(): void {
this.debouncedOnValueChange.cancel();
}
onISDCodeChange = (dialCode?: string) => {
const countryCode = getCountryCode(dialCode);
@ -218,21 +239,13 @@ class WDSPhoneInputWidget extends WDSBaseInputWidget<
}
};
onValueChange = (value: string) => {
let formattedValue;
// Don't format, as value is typed, when user is deleting
if (value && value.length > this.props.text?.length) {
formattedValue = this.getFormattedPhoneNumber(value);
} else {
formattedValue = value;
}
// debouncing the input change to avoid multiple Execute calls in reactive flow
debouncedOnValueChange = debounce((value: string) => {
this.props.updateWidgetMetaProperty(
"rawText",
parseIncompletePhoneNumber(formattedValue),
parseIncompletePhoneNumber(value),
);
this.props.updateWidgetMetaProperty("text", formattedValue, {
this.props.updateWidgetMetaProperty("text", value, {
triggerPropertyName: "onTextChanged",
dynamicString: this.props.onTextChanged,
event: {
@ -243,6 +256,20 @@ class WDSPhoneInputWidget extends WDSBaseInputWidget<
if (!this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", true);
}
}, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE);
onValueChange = (value: string) => {
let formattedValue;
// Don't format, as value is typed, when user is deleting
if (value && value.length > this.props.text?.length) {
formattedValue = this.getFormattedPhoneNumber(value);
} else {
formattedValue = value;
}
this.setState({ inputValue: formattedValue });
this.debouncedOnValueChange(formattedValue);
};
onFocusChange = (focusState: boolean) => {
@ -300,7 +327,7 @@ class WDSPhoneInputWidget extends WDSBaseInputWidget<
};
getWidgetView() {
const rawText = this.props.text ?? "";
const rawText = this.state.inputValue;
const validation = validateInput(this.props);