Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/dirty-sheep-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ensembleui/react-kitchen-sink": patch
"@ensembleui/react-runtime": patch
---

add debounce functionality to TextInput widget
2 changes: 2 additions & 0 deletions apps/kitchen-sink/src/ensemble/screens/forms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ View:
Text:
text: Text mask input
onChange: console.log("formTextInput onChange", value)
debounceMs: ${ensemble.storage.get('mockApiStatusCode')}
onKeyDown: console.log("formTextInput onKeyDown", event)
- TextInput:
id: minMaxTextInput
Expand All @@ -186,6 +187,7 @@ View:
hintStyle:
color: red
maxLength: 3
debounceMs: 1000
validator:
required: true
maxLength: 3
Expand Down
32 changes: 28 additions & 4 deletions packages/runtime/src/widgets/Form/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useEffect, useMemo, useState, useCallback, useRef } from "react";
import type { RefCallback, FormEvent } from "react";
import { runes } from "runes2";
import type { Rule } from "antd/es/form";
import { forEach, isObject, omitBy } from "lodash-es";
import { forEach, isObject, omitBy, debounce } from "lodash-es";
import IMask, { type InputMask } from "imask";
import type { EnsembleWidgetProps } from "../../shared/types";
import { WidgetRegistry } from "../../registry";
Expand All @@ -29,7 +29,10 @@ export type TextInputProps = {
"none" | "enforced" | "truncateAfterCompositionEnds"
>;
inputType?: "email" | "phone" | "number" | "text" | "url"; //| "ipAddress";
onChange?: EnsembleAction;
onChange?: {
debounceMs?: number;
} & EnsembleAction;
debounceMs?: number;
Comment thread
anserwaseem marked this conversation as resolved.
Outdated
mask?: string;
validator?: {
minLength?: number;
Expand Down Expand Up @@ -61,12 +64,20 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
const action = useEnsembleAction(props.onChange);
const onKeyDownAction = useEnsembleAction(props.onKeyDown);

const debouncedOnChange = useMemo(
() =>
debounce((inputValue: string) => {
action?.callback({ value: inputValue });
}, values?.debounceMs ?? 0),
[action?.callback, values?.debounceMs],
);

const handleChange = useCallback(
(newValue: string) => {
setValue(newValue);
action?.callback({ value: newValue });
debouncedOnChange(newValue);
},
[action?.callback],
[debouncedOnChange],
);

const handleRef: RefCallback<never> = (node) => {
Expand Down Expand Up @@ -123,6 +134,19 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
}
}, [values?.mask]);

// cleanup debounced function when component unmounts or changes
useEffect(() => {
return () => {
if (
debouncedOnChange &&
typeof debouncedOnChange === "function" &&
"cancel" in debouncedOnChange
) {
(debouncedOnChange as ReturnType<typeof debounce>).cancel();
}
};
}, [debouncedOnChange]);

const inputType = useMemo(() => {
switch (values?.inputType) {
case "email":
Expand Down
49 changes: 48 additions & 1 deletion packages/runtime/src/widgets/Form/__tests__/TextInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/* eslint-disable react/no-children-prop */
import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import {
fireEvent,
render,
screen,
waitFor,
act,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Form } from "../../index";
import { FormTestWrapper } from "./__shared__/fixtures";
Expand Down Expand Up @@ -357,5 +363,46 @@ describe("TextInput", () => {
);
});
});

test("debounces onChange events according to debounceMs", () => {
jest.useFakeTimers();

render(
<Form
children={[
{
name: "TextInput",
properties: {
label: "Debounced input",
id: "debouncedInput",
debounceMs: 500,
onChange: {
executeCode: "console.log('changed:', value)",
},
},
},
]}
id="form"
/>,
{ wrapper: FormTestWrapper },
);

const input = screen.getByLabelText("Debounced input");

fireEvent.change(input, { target: { value: "test value" } });
expect(logSpy).not.toHaveBeenCalledWith("changed:", "test value");

act(() => {
jest.advanceTimersByTime(300);
});
expect(logSpy).not.toHaveBeenCalledWith("changed:", "test value");

act(() => {
jest.advanceTimersByTime(210);
});
expect(logSpy).toHaveBeenCalledWith("changed:", "test value");

jest.useRealTimers();
});
});
/* eslint-enable react/no-children-prop */