Skip to content

Commit

Permalink
feat: Add a dependency list to useGridDataProvider and useComboBoxDat…
Browse files Browse the repository at this point in the history
…aProvider (#3273)

This is to support using state and signals in the fetch callback
  • Loading branch information
Artur- authored Feb 24, 2025
1 parent f1dc8cb commit 77b9667
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useComboBoxDataProvider } from '@vaadin/hilla-react-crud';
import { useSignal } from '@vaadin/hilla-react-signals';
import { Button, ComboBox } from '@vaadin/react-components';
import { useState } from 'react';
import { PersonCustomService } from 'Frontend/generated/endpoints';
Expand All @@ -12,10 +13,35 @@ export default function ComboBoxUseComboBoxDataProviderHook(): React.JSX.Element
const dataProvider = useComboBoxDataProvider(PersonCustomService.listPersonsLazyWithFilter);
const dataProviderLastName = useComboBoxDataProvider(PersonCustomService.listPersonsLazyWithFilter, { sort });

const filterSignal = useSignal('');
const dataProviderLastNameFiltered = useComboBoxDataProvider(
async (pageable, filter) => PersonCustomService.listPersonsLazyWithFilter(pageable, filterSignal.value + filter),
{ sort },
[filterSignal.value],
);

return (
<div className="p-m flex flex-col gap-m">
<ComboBox label="Default sort" dataProvider={dataProvider} itemLabelPath="lastName" />
<ComboBox label="Sorted using last name" dataProvider={dataProviderLastName} itemLabelPath="lastName" />
<ComboBox id="defaultSort" label="Default sort" dataProvider={dataProvider} itemLabelPath="lastName" />
<ComboBox
id="sortLastName"
label="Sorted using last name"
dataProvider={dataProviderLastName}
itemLabelPath="lastName"
/>
<input
id="filter"
type="text"
onInput={(e) => {
filterSignal.value = (e.target as HTMLInputElement).value;
}}
/>
<ComboBox
id="prependFilter"
label="Prepending filter with"
dataProvider={dataProviderLastNameFiltered}
itemLabelPath="lastName"
/>
<Button
onClick={() => {
dataProvider.refresh();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
import { useGridDataProvider } from '@vaadin/hilla-react-crud';
import { useSignal } from '@vaadin/hilla-react-signals';
import { Button } from '@vaadin/react-components';
import { Grid } from '@vaadin/react-components/Grid';
import { GridSortColumn } from '@vaadin/react-components/GridSortColumn';
import { useState } from 'react';
import type Pageable from 'Frontend/generated/com/vaadin/hilla/mappedtypes/Pageable';
import { PersonCustomService } from 'Frontend/generated/endpoints';

export default function GridUseGridDataProviderHook(): React.JSX.Element {
const dataProvider = useGridDataProvider(PersonCustomService.listPersonsLazy);
const filterSignal = useSignal('');
const fetch = async (pageable: Pageable) =>
PersonCustomService.listPersonsLazyWithFilter(pageable, filterSignal.value);
const dataProviderWithFilter = useGridDataProvider(fetch, [filterSignal.value]);

const [state, setState] = useState(0);

return (
<div className="p-m flex flex-col gap-m">
<Grid pageSize={10} dataProvider={dataProvider}>
<GridSortColumn path="firstName" />
<GridSortColumn path="lastName" />
<GridSortColumn path="gender" />
</Grid>
<Button onClick={() => dataProvider.refresh()}>Refresh</Button>
</div>
<>
<div className="p-m flex flex-col gap-m"></div>
<div>
<input
type="text"
onInput={(e) => {
const filterString = (e.target as HTMLInputElement).value;
setState(state + 1);
filterSignal.value = filterString;
}}
/>
<div>Filter length useState is {state}</div>
<div>Filter value is {filterSignal.value}</div>
<Grid pageSize={10} dataProvider={dataProviderWithFilter}>
<GridSortColumn path="firstName" />
<GridSortColumn path="lastName" />
<GridSortColumn path="gender" />
</Grid>
<Button onClick={() => dataProviderWithFilter.refresh()}>Refresh</Button>
</div>
</>
);
}
6 changes: 6 additions & 0 deletions packages/java/tests/spring/react-grid-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
<artifactId>vaadin-time-picker-testbench</artifactId>
<version>${vaadin.components.version}</version>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-combo-box-testbench</artifactId>
<version>${vaadin.components.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.vaadin.hilla.test.reactgrid;

import java.util.List;
import java.util.Map;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import com.vaadin.flow.component.button.testbench.ButtonElement;
import com.vaadin.flow.component.combobox.testbench.ComboBoxElement;
import com.vaadin.flow.testutil.ChromeBrowserTest;
import com.vaadin.testbench.TestBenchElement;

public class ComboBoxUseComboBoxDataProviderHookIT extends ChromeBrowserTest {

@Override
protected String getTestPath() {
return getRootURL() + "/"
+ getClass().getSimpleName().replace("IT", "");

}

@Override
@Before
public void setup() throws Exception {
super.setup();
getDriver().get(getTestPath());
}

@After
public void checkBrowserLogs() {
checkLogsForErrors();
}

@Test
public void defaultSort_dataShown() {
ComboBoxElement comboBox = $(ComboBoxElement.class).id("defaultSort");
List<String> options = getOptions(comboBox);
Assert.assertEquals("Johnson", options.get(0));
Assert.assertEquals("Lewis", options.get(9));

}

@Test
public void sortUsingLastname_dataShown() {
ComboBoxElement comboBox = $(ComboBoxElement.class).id("sortLastName");
List<String> options = getOptions(comboBox);
Assert.assertEquals("Adams", options.get(0));
Assert.assertEquals("Evans", options.get(9));
}

@Test
public void filteringUsingSignalWorks() {
ComboBoxElement comboBox = $(ComboBoxElement.class).id("prependFilter");
List<String> options = getOptions(comboBox);
Assert.assertEquals("Adams", options.get(0));
Assert.assertEquals("Evans", options.get(9));
comboBox.closePopup();

TestBenchElement filterInput = $("input").id("filter");
filterInput.sendKeys("c");
options = getOptions(comboBox);
Assert.assertEquals("Baker", options.get(0)); // Zack
Assert.assertEquals("Johnson", options.get(9)); // Alice
}

private List<String> getOptions(ComboBoxElement comboBox) {
comboBox.openPopup();
return waitUntil(driver -> {
List<String> opt = comboBox.getOptions();
if (opt.isEmpty()) {
return null;
}
return opt;
});
}

private void setFilter(String string) {
TestBenchElement filterInput = $("input").first();
filterInput.clear();
filterInput.sendKeys(string);
filterInput.dispatchEvent("input", Map.of("bubbles", true));
}

private void refresh() {
var refreshButton = $(ButtonElement.class).all().stream()
.filter(button -> button.getText().equals("Refresh"))
.findFirst();
if (refreshButton.isPresent()) {
refreshButton.get().click();
} else {
Assert.fail("Refresh button not found");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.vaadin.hilla.test.reactgrid;

import java.util.Map;

import org.junit.Assert;
import org.junit.Test;

import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.testbench.ButtonElement;
import com.vaadin.testbench.TestBenchElement;

public class GridUseGridDataProviderHookIT extends AbstractGridTest {

Expand Down Expand Up @@ -34,6 +38,33 @@ public void sortingWorks_and_refreshingDataProvider_keepsTheAppliedSort() {
assertFirstName(3, "Xander");
}

@Test
public void filteringUsingSignalWorks() {
assertFirstName(0, "Alice");
assertFirstName(1, "Bob");
assertFirstName(2, "Charlie");
assertFirstName(3, "David");

setFilter("al");
assertFirstName(0, "Alice");
assertFirstName(1, "Edward"); // Gonazlez matches
assertFirstName(2, "Kathy"); // Walker matches
assertFirstName(3, "Laura"); // Hall matches
setFilter("");
assertFirstName(0, "Alice");
assertFirstName(1, "Bob");
assertFirstName(2, "Charlie");
assertFirstName(3, "David");

}

private void setFilter(String string) {
TestBenchElement filterInput = $("input").first();
filterInput.clear();
filterInput.sendKeys(string);
filterInput.dispatchEvent("input", Map.of("bubbles", true));
}

private void refresh() {
var refreshButton = $(ButtonElement.class).all().stream()
.filter(button -> button.getText().equals("Refresh"))
Expand Down
46 changes: 34 additions & 12 deletions packages/ts/react-crud/src/data-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
ComboBoxDataProviderParams,
} from '@vaadin/react-components';
import type { GridDataProvider, GridDataProviderCallback, GridDataProviderParams } from '@vaadin/react-components/Grid';
import { useMemo, useState } from 'react';
import { useMemo, useState, type DependencyList } from 'react';
import type { CountService, ListService } from './crud';
import type FilterUnion from './types/com/vaadin/hilla/crud/filter/FilterUnion';
import type Pageable from './types/com/vaadin/hilla/mappedtypes/Pageable';
Expand Down Expand Up @@ -126,14 +126,14 @@ export abstract class DataProvider<TItem> {
}

export abstract class AbstractComboBoxDataProvider<TItem> {
protected readonly list: ComboBoxFetchCallback<TItem>;
protected readonly list: ComboBoxFetchMethod<TItem>;
protected readonly loadTotalCount?: boolean;

protected sort: Sort | undefined;
protected totalCount: number | undefined;
protected filteredCount: number | undefined;

constructor(list: ComboBoxFetchCallback<TItem>, sort: Sort | undefined) {
constructor(list: ComboBoxFetchMethod<TItem>, sort: Sort | undefined) {
this.list = list;
this.sort = sort;
}
Expand Down Expand Up @@ -289,15 +289,25 @@ export type UseGridDataProviderResult<TItem> = GridDataProvider<TItem> & {
refresh(): void;
};

export type GridFetchCallback<TItem> = (pageable: Pageable) => Promise<TItem[]>;

export function useGridDataProvider<TItem>(list: GridFetchCallback<TItem>): UseGridDataProviderResult<TItem> {
export type GridFetchMethod<TItem> = (pageable: Pageable) => Promise<TItem[]>;

/**
* Creates a data provider for a grid component that fetches data using the provided fetch callback.
*
* @param fetch - the callback that fetches the data for the grid. The callback should return a promise that resolves to an array of items.
* @param dependencies - A list of all reactive values referenced inside of the fetch callback. A change to any of the listed values will cause the grid to refresh its data.
* @returns a data provider that can be assigned to a <Grid> component usings its dataProvider property and additionally contains a refresh method that can be called to force a reload of the grid data.
*/
export function useGridDataProvider<TItem>(
fetch: GridFetchMethod<TItem>,
dependencies?: DependencyList,
): UseGridDataProviderResult<TItem> {
const result = useDataProvider(
useMemo(
() => ({
list: async (pageable: Pageable) => list(pageable),
list: async (pageable: Pageable) => fetch(pageable),
}),
[],
dependencies ?? [],
),
);
const dataProvider: UseGridDataProviderResult<TItem> = result.dataProvider as UseGridDataProviderResult<TItem>;
Expand All @@ -309,10 +319,10 @@ export type UseComboBoxDataProviderResult<TItem> = ComboBoxDataProvider<TItem> &
refresh(): void;
};

export type ComboBoxFetchCallback<TItem> = (pageable: Pageable, filterString: string) => Promise<TItem[]>;
export type ComboBoxFetchMethod<TItem> = (pageable: Pageable, filterString: string) => Promise<TItem[]>;

function createComboBoxDataProvider<TItem>(
list: ComboBoxFetchCallback<TItem>,
list: ComboBoxFetchMethod<TItem>,
sort: Sort | undefined,
): AbstractComboBoxDataProvider<TItem> {
return new InfiniteComboBoxDataProvider(list, sort);
Expand All @@ -322,12 +332,24 @@ type ComboboxDataProviderOptions = {
sort?: Sort;
};

/**
* Creates a data provider for a combo box component that fetches data using the provided fetch callback.
*
* @param fetch - the method that fetches the data for the grid. The method should return a promise that resolves to an array of items.
* @param dependencies - A list of all reactive values referenced inside of the fetch callback. A change to any of the listed values will cause the combo box to refresh its data.
* @returns a data provider that can be assigned to a <ComboBox> component usings its dataProvider property and additionally contains a refresh method that can be called to force a reload of the combo box data.
*/
export function useComboBoxDataProvider<TItem>(
list: ComboBoxFetchCallback<TItem>,
fetch: ComboBoxFetchMethod<TItem>,
options?: ComboboxDataProviderOptions,
dependencies?: DependencyList,
): UseComboBoxDataProviderResult<TItem> {
const [refreshCounter, setRefreshCounter] = useState(0);
const dataProvider = useMemo(() => createComboBoxDataProvider(list, options?.sort), [list, options?.sort]);

const dataProvider = useMemo(
() => createComboBoxDataProvider(fetch, options?.sort),
[options?.sort, ...(dependencies ?? [])],
);

// Create a new data provider function reference when the refresh counter is incremented.
// This effectively forces the combo box to reload
Expand Down
Loading

0 comments on commit 77b9667

Please sign in to comment.