["ref"];
+ inputDisabled?: boolean;
+}
- const focusInput = useCallback(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, [inputRef]);
+function FilterInput(props: FilterInputProps): ReactElement {
+ const { current: initialFilterType } = useRef(props.initialFilterType);
return (
{
- setType(prev => {
- if (prev === type) {
- return prev;
- }
- focusInput();
- return type;
- });
- },
- [focusInput]
- )}
+ defaultFilter={initialFilterType}
+ onChange={props.onFilterTypeClick}
options={
[
{ value: "greater", label: "Greater than" },
@@ -80,29 +57,49 @@ export function FilterComponent(props: FilterComponentProps): ReactElement {
{ value: "smallerEqual", label: "Smaller than or equal" },
{ value: "empty", label: "Empty" },
{ value: "notEmpty", label: "Not empty" }
- ] as Array<{ value: DefaultFilterEnum; label: string }>
+ ] as Array<{ value: FilterType; label: string }>
}
/>
)}
{
- const value = e.target.value;
- if (value && !isNaN(Number(value))) {
- setValueInput(value);
- onChange(new Big(Number(value)));
- } else {
- setValueInput(value);
- onChange(undefined);
- }
- }}
+ disabled={props.inputDisabled}
+ onChange={props.onInputChange}
placeholder={props.placeholder}
- ref={inputRef}
+ ref={props.inputRef}
type="number"
- value={valueInput}
+ value={props.inputValue}
/>
);
}
+
+const PureFilterInput = memo(FilterInput);
+
+export function FilterComponent(props: FilterComponentProps): ReactElement {
+ const [state, onInputChange, onFilterTypeClick] = useFilterState(() => ({
+ inputValue: toInputValue(props.initialFilterValue),
+ type: props.initialFilterType
+ }));
+ const [inputRef] = useStateChangeEffects(state, (a, b) => props.updateFilters?.(a, b), props.inputChangeDelay);
+
+ return (
+
+ );
+}
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx b/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx
index 2a96f8487f..3c3ba2bbd8 100644
--- a/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx
@@ -63,20 +63,20 @@ describe("Number Filter", () => {
render((new Big(100))} />);
expect(screen.getByRole("spinbutton")).toHaveValue(100);
});
- it("sync value and defaultValue when defaultValue changes from undefined to number", () => {
+ it("do not sync value and defaultValue when defaultValue changes from undefined to number", () => {
const { rerender } = render();
expect(screen.getByRole("spinbutton")).toHaveValue(null);
rerender((new Big(100))} />);
- expect(screen.getByRole("spinbutton")).toHaveValue(100);
+ expect(screen.getByRole("spinbutton")).toHaveValue(null);
});
- it("sync value and defaultValue when defaultValue changes from number to undefined", async () => {
+ it("do not sync value and defaultValue when defaultValue changes from number to undefined", async () => {
const { rerender } = render(
(new Big(100))} />
);
expect(screen.getByRole("spinbutton")).toHaveValue(100);
rerender();
await waitFor(() => {
- expect(screen.getByRole("spinbutton")).toHaveValue(null);
+ expect(screen.getByRole("spinbutton")).toHaveValue(100);
});
});
});
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/FilterComponent.spec.tsx b/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/FilterComponent.spec.tsx
index d5eeb90d27..58b55ea58f 100644
--- a/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/FilterComponent.spec.tsx
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/FilterComponent.spec.tsx
@@ -1,4 +1,4 @@
-import { render, shallow } from "enzyme";
+import { render, mount } from "enzyme";
import { createElement } from "react";
import { FilterComponent } from "../FilterComponent";
@@ -6,13 +6,15 @@ jest.useFakeTimers();
describe("Filter component", () => {
it("renders correctly", () => {
- const component = render();
+ const component = render();
expect(component).toMatchSnapshot();
});
it("renders correctly when not adjustable by user", () => {
- const component = render();
+ const component = render(
+
+ );
expect(component).toMatchSnapshot();
});
@@ -21,8 +23,8 @@ describe("Filter component", () => {
const component = render(
@@ -33,24 +35,35 @@ describe("Filter component", () => {
it("calls updateFilters when value changes", () => {
const updateFiltersHandler = jest.fn();
- const component = shallow(
-
+ const component = mount(
+
);
const input = component.find("input");
input.simulate("change", { target: { value: "test" } });
+ jest.advanceTimersByTime(500);
+
expect(updateFiltersHandler).toBeCalled();
});
it("debounces calls for updateFilters when value changes with numbers", () => {
const updateFiltersHandler = jest.fn();
- const component = shallow(
-
+ const component = mount(
+
);
- // Initial call with default filter
- expect(updateFiltersHandler).toBeCalledTimes(1);
+ expect(updateFiltersHandler).toBeCalledTimes(0);
const input = component.find("input");
input.simulate("change", { target: { value: "0" } });
@@ -59,22 +72,26 @@ describe("Filter component", () => {
input.simulate("change", { target: { value: "2" } });
jest.advanceTimersByTime(500);
- expect(updateFiltersHandler).toBeCalledTimes(2);
+ expect(updateFiltersHandler).toBeCalledTimes(1);
input.simulate("change", { target: { value: "3" } });
jest.advanceTimersByTime(500);
- expect(updateFiltersHandler).toBeCalledTimes(3);
+ expect(updateFiltersHandler).toBeCalledTimes(2);
});
it("debounces calls for updateFilters when value changes with decimals", () => {
const updateFiltersHandler = jest.fn();
- const component = shallow(
-
+ const component = mount(
+
);
- // Initial call with default filter
- expect(updateFiltersHandler).toBeCalledTimes(1);
+ expect(updateFiltersHandler).toBeCalledTimes(0);
const input = component.find("input");
input.simulate("change", { target: { value: "0.0" } });
@@ -83,22 +100,26 @@ describe("Filter component", () => {
input.simulate("change", { target: { value: "4" } });
jest.advanceTimersByTime(500);
- expect(updateFiltersHandler).toBeCalledTimes(2);
+ expect(updateFiltersHandler).toBeCalledTimes(1);
input.simulate("change", { target: { value: "6.8" } });
jest.advanceTimersByTime(500);
- expect(updateFiltersHandler).toBeCalledTimes(3);
+ expect(updateFiltersHandler).toBeCalledTimes(2);
});
it("debounces calls for updateFilters when value changes with invalid input", () => {
const updateFiltersHandler = jest.fn();
- const component = shallow(
-
+ const component = mount(
+
);
- // Initial call with default filter
- expect(updateFiltersHandler).toBeCalledTimes(1);
+ expect(updateFiltersHandler).toBeCalledTimes(0);
const input = component.find("input");
input.simulate("change", { target: { value: "test1" } });
@@ -107,13 +128,13 @@ describe("Filter component", () => {
input.simulate("change", { target: { value: "test3" } });
jest.advanceTimersByTime(500);
- // Consecutive invalid numbers wont call useState with empty value twice
- // this is why we expect func to be called 1 time
expect(updateFiltersHandler).toBeCalledTimes(1);
+ expect(updateFiltersHandler).toHaveBeenLastCalledWith(undefined, "equal");
input.simulate("change", { target: { value: "test4" } });
jest.advanceTimersByTime(500);
- expect(updateFiltersHandler).toBeCalledTimes(1);
+ expect(updateFiltersHandler).toBeCalledTimes(2);
+ expect(updateFiltersHandler).toHaveBeenLastCalledWith(undefined, "equal");
});
});
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/features/filter-state.ts b/packages/pluggableWidgets/datagrid-number-filter-web/src/features/filter-state.ts
new file mode 100644
index 0000000000..b805796935
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/features/filter-state.ts
@@ -0,0 +1,57 @@
+import { ChangeEventHandler, useState, useMemo, useRef, useEffect, MutableRefObject } from "react";
+import { FilterType } from "../../typings/FilterType";
+import { Big } from "big.js";
+import { debounce, useEventCallback } from "@mendix/pluggable-widgets-commons";
+import { toBig } from "../utils/value";
+
+type FilterState = {
+ type: FilterType;
+ inputValue: string;
+};
+
+type InputChangeHandler = ChangeEventHandler;
+
+type TypeClickHandler = (type: FilterType) => void;
+
+function updateState(key: K, value: V): (prev: S) => S {
+ return prev => (prev[key] !== value ? { ...prev, [key]: value } : prev);
+}
+
+export function useFilterState(initialState: () => FilterState): [FilterState, InputChangeHandler, TypeClickHandler] {
+ const [state, setState] = useState(initialState);
+ const [onInputChange, onTypeClick] = useMemo(() => {
+ const inputHandler: InputChangeHandler = event => setState(updateState("inputValue", event.target.value));
+ const clickHandler: TypeClickHandler = type => setState(updateState("type", type));
+
+ return [inputHandler, clickHandler];
+ }, []);
+
+ return [state, onInputChange, onTypeClick];
+}
+
+type ChangeDispatch = (value: Big | undefined, type: FilterType) => void;
+
+export function useStateChangeEffects(
+ state: FilterState,
+ dispatch: ChangeDispatch,
+ inputChangeDelay: number
+): [MutableRefObject] {
+ const stableDispatch = useEventCallback(dispatch);
+ const [stableDispatchDelayed] = useState(() => debounce(stableDispatch, inputChangeDelay));
+ const inputRef = useRef(null);
+ const prevStateRef = useRef(state);
+
+ useEffect(() => {
+ const { current: prevState } = prevStateRef;
+ if (state.type !== prevState.type) {
+ stableDispatch(toBig(state.inputValue), state.type);
+ inputRef.current?.focus();
+ } else if (state.inputValue !== prevState.inputValue) {
+ stableDispatchDelayed(toBig(state.inputValue), state.type);
+ }
+ prevStateRef.current = state;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [state]);
+
+ return [inputRef];
+}
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/package.xml b/packages/pluggableWidgets/datagrid-number-filter-web/src/package.xml
index 5d86993529..8dc3946227 100644
--- a/packages/pluggableWidgets/datagrid-number-filter-web/src/package.xml
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/package.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/utils/value.ts b/packages/pluggableWidgets/datagrid-number-filter-web/src/utils/value.ts
new file mode 100644
index 0000000000..bf9a2bb1f2
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/utils/value.ts
@@ -0,0 +1,13 @@
+import { Big } from "big.js";
+
+export function toBig(value: any): Big | undefined {
+ try {
+ return new Big(value);
+ } catch {
+ return undefined;
+ }
+}
+
+export function toInputValue(value: Big | undefined): string {
+ return value instanceof Big ? value.toString() : "";
+}
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/typings/FilterType.d.ts b/packages/pluggableWidgets/datagrid-number-filter-web/typings/FilterType.d.ts
new file mode 100644
index 0000000000..eb9fcd0c59
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/typings/FilterType.d.ts
@@ -0,0 +1 @@
+export { DefaultFilterEnum as FilterType } from "./DatagridNumberFilterProps";
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-text-filter-web/CHANGELOG.md
index 0f2c3a386a..3ddcf4fe54 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/CHANGELOG.md
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/CHANGELOG.md
@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased]
+### Fixed
+
+- We fixed an issue with widget rendering and performance.
+
+### Breaking changes
+
+- We introduce a breaking change that affects how widget is reacting on default value changes. Starting with this version, widget use the default value attribute only as an initial value, and any further changes to the default value attribute will be ignored.
+
## [2.3.2] - 2022-08-11
### Fixed
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/package.json b/packages/pluggableWidgets/datagrid-text-filter-web/package.json
index 8fabb2faaf..30e999840c 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/package.json
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/package.json
@@ -1,7 +1,7 @@
{
"name": "datagrid-text-filter-web",
"widgetName": "DatagridTextFilter",
- "version": "2.3.2",
+ "version": "2.4.0",
"description": "",
"copyright": "© Mendix Technology BV 2023. All rights reserved.",
"private": true,
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.editorPreview.tsx b/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.editorPreview.tsx
index 471fe5224b..af61bfa56d 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.editorPreview.tsx
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.editorPreview.tsx
@@ -8,13 +8,13 @@ export function preview(props: DatagridTextFilterPreviewProps): ReactElement {
);
}
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.tsx b/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.tsx
index 17db93ae59..133d354a46 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.tsx
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.tsx
@@ -77,12 +77,17 @@ export default function DatagridTextFilter(props: DatagridTextFilterContainerPro
return {errorMessage};
}
+ if (props.defaultValue?.status === "loading") {
+ return null;
+ }
+
return (
{
- const attributeCurrentValue = props.valueAttribute?.value || "";
- if (value !== attributeCurrentValue) {
- props.valueAttribute?.setValue(value);
- props.onChange?.execute();
- }
+ props.valueAttribute?.setValue(value);
+ props.onChange?.execute();
const conditions = attributes
?.map(attribute => getFilterCondition(attribute, value, type))
.filter((filter): filter is FilterCondition => filter !== undefined);
@@ -104,7 +106,6 @@ export default function DatagridTextFilter(props: DatagridTextFilterContainerPro
filterType: FilterType.STRING
});
}}
- value={defaultFilter?.value ?? props.defaultValue?.value}
/>
);
}}
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/components/FilterComponent.tsx b/packages/pluggableWidgets/datagrid-text-filter-web/src/components/FilterComponent.tsx
index b4c4f28604..6f545a1eb0 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/components/FilterComponent.tsx
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/components/FilterComponent.tsx
@@ -1,50 +1,37 @@
-import { createElement, CSSProperties, ReactElement, useCallback, useEffect, useRef, useState } from "react";
+import { createElement, CSSProperties, ReactElement, memo, useRef, ChangeEventHandler } from "react";
import { FilterSelector } from "@mendix/pluggable-widgets-commons/components/web";
-import { debounce } from "@mendix/pluggable-widgets-commons";
-
-import { DefaultFilterEnum } from "../../typings/DatagridTextFilterProps";
+import { useFilterState, useStateChangeEffects } from "../features/filter-state";
+import { FilterType } from "../../typings/FilterType";
import classNames from "classnames";
-interface FilterComponentProps {
+interface FilterProps {
adjustable: boolean;
+ initialFilterType: FilterType;
className?: string;
- defaultFilter: DefaultFilterEnum;
- delay: number;
id?: string;
placeholder?: string;
- tabIndex?: number;
screenReaderButtonCaption?: string;
screenReaderInputCaption?: string;
+ tabIndex?: number;
styles?: CSSProperties;
- updateFilters?: (value: string, type: DefaultFilterEnum) => void;
- value?: string;
}
-export function FilterComponent(props: FilterComponentProps): ReactElement {
- const [type, setType] = useState(props.defaultFilter);
- const [value, setValue] = useState("");
- const [valueInput, setValueInput] = useState("");
- const inputRef = useRef(null);
-
- useEffect(() => {
- setValueInput(props.value ?? "");
- setValue(props.value ?? "");
- }, [props.value]);
-
- useEffect(() => {
- props.updateFilters?.(value, type);
- }, [value, type]);
+interface FilterComponentProps extends FilterProps {
+ inputChangeDelay: number;
+ initialFilterValue?: string;
+ updateFilters?: (value: string | undefined, type: FilterType) => void;
+}
- const onChange = useCallback(
- debounce((value: string) => setValue(value), props.delay),
- [props.delay]
- );
+interface FilterInputProps extends FilterProps {
+ onFilterTypeClick: (type: FilterType) => void;
+ onInputChange: ChangeEventHandler;
+ inputValue: string;
+ inputRef?: React.ClassAttributes["ref"];
+ inputDisabled?: boolean;
+}
- const focusInput = useCallback(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, [inputRef]);
+function FilterInput(props: FilterInputProps): ReactElement {
+ const { current: initialFilterType } = useRef(props.initialFilterType);
return (
{
- setType(prev => {
- if (prev === type) {
- return prev;
- }
- focusInput();
- return type;
- });
- },
- [focusInput]
- )}
+ defaultFilter={initialFilterType}
+ onChange={props.onFilterTypeClick}
options={
[
{ value: "contains", label: "Contains" },
@@ -82,23 +58,49 @@ export function FilterComponent(props: FilterComponentProps): ReactElement {
{ value: "smallerEqual", label: "Smaller than or equal" },
{ value: "empty", label: "Empty" },
{ value: "notEmpty", label: "Not empty" }
- ] as Array<{ value: DefaultFilterEnum; label: string }>
+ ] as Array<{ value: FilterType; label: string }>
}
/>
)}
{
- setValueInput(e.target.value);
- onChange(e.target.value);
- }}
+ disabled={props.inputDisabled}
+ onChange={props.onInputChange}
placeholder={props.placeholder}
- ref={inputRef}
+ ref={props.inputRef}
type="text"
- value={valueInput}
+ value={props.inputValue}
/>
);
}
+
+const PureFilterInput = memo(FilterInput);
+
+export function FilterComponent(props: FilterComponentProps): ReactElement {
+ const [state, onInputChange, onFilterTypeClick] = useFilterState(() => ({
+ inputValue: props.initialFilterValue ?? "",
+ type: props.initialFilterType
+ }));
+ const [inputRef] = useStateChangeEffects(state, (a, b) => props.updateFilters?.(a, b), props.inputChangeDelay);
+
+ return (
+
+ );
+}
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx b/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx
index 6003901116..54ae5140e8 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx
@@ -30,7 +30,7 @@ describe("Text Filter", () => {
delete (global as any)["com.mendix.widgets.web.UUID"];
});
- describe("sync input value with defaultValue prop", () => {
+ describe("with defaultValue prop", () => {
beforeAll(() => {
(window as any)["com.mendix.widgets.web.filterable.filterContext"] = createContext({
filterDispatcher: jest.fn(),
@@ -38,7 +38,7 @@ describe("Text Filter", () => {
} as FilterContextValue);
});
- it("sync value when defaultValue changes from undefined to string", async () => {
+ it("don't sync value when defaultValue changes from undefined to string", async () => {
const { rerender } = render();
expect(screen.getByRole("textbox")).toHaveValue("");
@@ -46,10 +46,10 @@ describe("Text Filter", () => {
// rerender component with new `defaultValue`
const defaultValue = dynamicValue("xyz");
rerender();
- expect(screen.getByRole("textbox")).toHaveValue("xyz");
+ expect(screen.getByRole("textbox")).toHaveValue("");
});
- it("sync value when defaultValue changes from string to string", async () => {
+ it("don't sync value when defaultValue changes from string to string", async () => {
const { rerender } = render(
("abc")} />
);
@@ -59,10 +59,10 @@ describe("Text Filter", () => {
// rerender component with new `defaultValue`
const defaultValue = dynamicValue("xyz");
rerender();
- expect(screen.getByRole("textbox")).toHaveValue("xyz");
+ expect(screen.getByRole("textbox")).toHaveValue("abc");
});
- it("sync value when defaultValue changes from string to undefined", async () => {
+ it("don't sync value when defaultValue changes from string to undefined", async () => {
const { rerender } = render(
("abc")} />
);
@@ -71,7 +71,7 @@ describe("Text Filter", () => {
// rerender component with new `defaultValue`
rerender();
- expect(screen.getByRole("textbox")).toHaveValue("");
+ expect(screen.getByRole("textbox")).toHaveValue("abc");
});
});
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/FilterComponent.spec.tsx b/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/FilterComponent.spec.tsx
index b8d5a31fb6..a37bb67220 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/FilterComponent.spec.tsx
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/FilterComponent.spec.tsx
@@ -1,4 +1,4 @@
-import { render, shallow } from "enzyme";
+import { render, mount } from "enzyme";
import { createElement } from "react";
import { FilterComponent } from "../FilterComponent";
@@ -6,13 +6,15 @@ jest.useFakeTimers();
describe("Filter component", () => {
it("renders correctly", () => {
- const component = render();
+ const component = render();
expect(component).toMatchSnapshot();
});
it("renders correctly when not adjustable by user", () => {
- const component = render();
+ const component = render(
+
+ );
expect(component).toMatchSnapshot();
});
@@ -23,8 +25,8 @@ describe("Filter component", () => {
adjustable
screenReaderButtonCaption="my label"
screenReaderInputCaption="my label"
- defaultFilter="contains"
- delay={500}
+ initialFilterType="contains"
+ inputChangeDelay={500}
/>
);
@@ -33,37 +35,50 @@ describe("Filter component", () => {
it("calls updateFilters when value changes", () => {
const updateFiltersHandler = jest.fn();
- const component = shallow(
-
+ const component = mount(
+
);
const input = component.find("input");
input.simulate("change", { target: { value: "test" } });
-
- expect(updateFiltersHandler).toBeCalled();
+ expect(updateFiltersHandler).toBeCalledTimes(0);
+ jest.advanceTimersByTime(500);
+ expect(updateFiltersHandler).toBeCalledTimes(1);
});
it("debounces calls for updateFilters when value changes", () => {
const updateFiltersHandler = jest.fn();
- const component = shallow(
-
+ const component = mount(
+
);
- // Initial call with default filter
- expect(updateFiltersHandler).toBeCalledTimes(1);
+ expect(updateFiltersHandler).toBeCalledTimes(0);
const input = component.find("input");
input.simulate("change", { target: { value: "test" } });
jest.advanceTimersByTime(499);
+ expect(updateFiltersHandler).toBeCalledTimes(0);
+
input.simulate("change", { target: { value: "test2" } });
input.simulate("change", { target: { value: "test3" } });
jest.advanceTimersByTime(500);
-
- expect(updateFiltersHandler).toBeCalledTimes(2);
+ expect(updateFiltersHandler).toBeCalledTimes(1);
+ expect(updateFiltersHandler).toHaveBeenCalledWith("test3", "contains");
input.simulate("change", { target: { value: "test" } });
jest.advanceTimersByTime(500);
- expect(updateFiltersHandler).toBeCalledTimes(3);
+ expect(updateFiltersHandler).toBeCalledTimes(2);
+ expect(updateFiltersHandler).toHaveBeenCalledWith("test", "contains");
});
});
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/features/filter-state.ts b/packages/pluggableWidgets/datagrid-text-filter-web/src/features/filter-state.ts
new file mode 100644
index 0000000000..288864f2d6
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/features/filter-state.ts
@@ -0,0 +1,55 @@
+import { ChangeEventHandler, useState, useMemo, useRef, useEffect, MutableRefObject } from "react";
+import { FilterType } from "../../typings/FilterType";
+import { debounce, useEventCallback } from "@mendix/pluggable-widgets-commons";
+
+type FilterState = {
+ type: FilterType;
+ inputValue: string;
+};
+
+type InputChangeHandler = ChangeEventHandler;
+
+type TypeClickHandler = (type: FilterType) => void;
+
+function updateState(key: K, value: V): (prev: S) => S {
+ return prev => (prev[key] !== value ? { ...prev, [key]: value } : prev);
+}
+
+export function useFilterState(initialState: () => FilterState): [FilterState, InputChangeHandler, TypeClickHandler] {
+ const [state, setState] = useState(initialState);
+ const [onInputChange, onTypeClick] = useMemo(() => {
+ const inputHandler: InputChangeHandler = event => setState(updateState("inputValue", event.target.value));
+ const clickHandler: TypeClickHandler = type => setState(updateState("type", type));
+
+ return [inputHandler, clickHandler];
+ }, []);
+
+ return [state, onInputChange, onTypeClick];
+}
+
+type ChangeDispatch = (value: string | undefined, type: FilterType) => void;
+
+export function useStateChangeEffects(
+ state: FilterState,
+ dispatch: ChangeDispatch,
+ inputChangeDelay: number
+): [MutableRefObject] {
+ const stableDispatch = useEventCallback(dispatch);
+ const [stableDispatchDelayed] = useState(() => debounce(stableDispatch, inputChangeDelay));
+ const inputRef = useRef(null);
+ const prevStateRef = useRef(state);
+
+ useEffect(() => {
+ const { current: prevState } = prevStateRef;
+ if (state.type !== prevState.type) {
+ stableDispatch(state.inputValue, state.type);
+ inputRef.current?.focus();
+ } else if (state.inputValue !== prevState.inputValue) {
+ stableDispatchDelayed(state.inputValue, state.type);
+ }
+ prevStateRef.current = state;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [state]);
+
+ return [inputRef];
+}
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/package.xml b/packages/pluggableWidgets/datagrid-text-filter-web/src/package.xml
index 85477b056b..f08bb6cc35 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/package.xml
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/package.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/typings/FilterType.d.ts b/packages/pluggableWidgets/datagrid-text-filter-web/typings/FilterType.d.ts
new file mode 100644
index 0000000000..4005087572
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/typings/FilterType.d.ts
@@ -0,0 +1 @@
+export { DefaultFilterEnum as FilterType } from "./DatagridTextFilterProps";
\ No newline at end of file
diff --git a/packages/shared/pluggable-widgets-commons/src/functions.ts b/packages/shared/pluggable-widgets-commons/src/functions.ts
index 3d68e89a1b..a5ad4d3e66 100644
--- a/packages/shared/pluggable-widgets-commons/src/functions.ts
+++ b/packages/shared/pluggable-widgets-commons/src/functions.ts
@@ -1,3 +1,4 @@
+import { useState } from "react";
import { ActionValue, DynamicValue, EditableValue, ValueStatus } from "mendix";
export const executeAction = (action?: ActionValue): void => {
@@ -38,3 +39,55 @@ export const debounce = any>(func: F, waitFor: nu
return debounced as F;
};
+
+export function useId(name?: string): string {
+ const [id] = useState(() => {
+ const num = Math.random().toFixed(9).slice(2);
+ return name ? `${name}-${num}` : num;
+ });
+
+ return id;
+}
+
+const debugMsg = (...args: any[]): void => console.debug("[DEBUG]", ...args);
+
+function debugHeader(id: string): void {
+ debugMsg();
+ debugMsg(`Component:`, id);
+}
+
+function createInspect(id: string): (props: any) => void {
+ let prevProps: any = {};
+ return (currentProps: any) => {
+ debugHeader(id);
+ const keys = new Set([...Object.keys(prevProps), ...Object.keys(currentProps)]);
+ let changed = false;
+ for (const k of keys) {
+ if (prevProps[k] !== currentProps[k]) {
+ debugMsg(` > prop [${k}] changed`);
+ changed = true;
+ }
+ }
+ if (!changed) {
+ debugMsg(" > No prop changes");
+ }
+ prevProps = currentProps;
+ };
+}
+
+export function usePropInspect(id: string): (props: any) => void {
+ const [inspect] = useState(() => createInspect(id));
+ return inspect;
+}
+
+function createLog(id: string): (...args: string[]) => void {
+ return (...args: string[]) => {
+ debugHeader(id);
+ debugMsg(" >", ...args);
+ };
+}
+
+export function useLog(id: string): (...args: string[]) => void {
+ const [log] = useState(() => createLog(id));
+ return log;
+}