Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a dependency list to useGridDataProvider and useComboBoxDataProvider #3273

Merged
merged 2 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
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