From 32465c18a476148c9e69abdb516a302834c3fd39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Thu, 27 Feb 2025 11:02:38 +0100 Subject: [PATCH] docs: update React grid data provider example (#4133) * docs: update React grid data provider example * fix mock not being used * make it work without a JPA repository * use new grid data provider hook * remove count implementation --- articles/components/grid/index.adoc | 8 ++ .../grid/react/grid-data-provider.tsx | 81 +++---------------- frontend/demo/services/GridPersonService.ts | 53 ++++++++++++ frontend/demo/services/mocks.ts | 3 +- .../component/grid/GridPersonRepository.java | 26 ++++++ .../component/grid/GridPersonService.java | 27 +++++++ 6 files changed, 126 insertions(+), 72 deletions(-) create mode 100644 frontend/demo/services/GridPersonService.ts create mode 100644 src/main/java/com/vaadin/demo/component/grid/GridPersonRepository.java create mode 100644 src/main/java/com/vaadin/demo/component/grid/GridPersonService.java diff --git a/articles/components/grid/index.adoc b/articles/components/grid/index.adoc index a576870ee8..6ea253aea0 100644 --- a/articles/components/grid/index.adoc +++ b/articles/components/grid/index.adoc @@ -314,6 +314,14 @@ ifdef::react[] ---- include::{root}/frontend/demo/component/grid/react/grid-data-provider.tsx[render,tags=snippet,indent=0,group=React] ---- +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/grid/GridPersonService.java[render,tags=snippet,indent=0,group=React] +---- +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/grid/GridPersonRepository.java[render,tags=snippet,indent=0,group=React] +---- endif::[] -- diff --git a/frontend/demo/component/grid/react/grid-data-provider.tsx b/frontend/demo/component/grid/react/grid-data-provider.tsx index f78fa71437..b7ee77fec0 100644 --- a/frontend/demo/component/grid/react/grid-data-provider.tsx +++ b/frontend/demo/component/grid/react/grid-data-provider.tsx @@ -1,88 +1,27 @@ import '@vaadin/icons'; import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line -import React, { useMemo } from 'react'; +import React from 'react'; import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line +import { useGridDataProvider } from '@vaadin/hilla-react-crud'; import { useSignal } from '@vaadin/hilla-react-signals'; -import { - Grid, - type GridDataProviderCallback, - type GridDataProviderParams, - type GridSorterDefinition, - type GridSorterDirection, -} from '@vaadin/react-components/Grid.js'; +import { Grid } from '@vaadin/react-components/Grid.js'; import { GridSortColumn } from '@vaadin/react-components/GridSortColumn.js'; import { Icon } from '@vaadin/react-components/Icon.js'; import { TextField } from '@vaadin/react-components/TextField.js'; import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js'; -import { getPeople } from 'Frontend/demo/domain/DataService'; -import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; - -function matchesTerm(value: string, searchTerm: string) { - return value.toLowerCase().includes(searchTerm.toLowerCase()); -} - -function compare(a: string, b: string, direction: GridSorterDirection) { - return direction === 'asc' ? a.localeCompare(b) : b.localeCompare(a); -} +import { GridPersonService } from 'Frontend/generated/endpoints'; // tag::snippet[] -async function fetchPeople(params: { - page: number; - pageSize: number; - searchTerm: string; - sortOrders: GridSorterDefinition[]; -}) { - const { page, pageSize, searchTerm, sortOrders } = params; - const { people } = await getPeople(); - let result = people.map((person) => ({ - ...person, - fullName: `${person.firstName} ${person.lastName}`, - })); - - // Filtering - if (searchTerm) { - result = result.filter( - (p) => matchesTerm(p.fullName, searchTerm) || matchesTerm(p.profession, searchTerm) - ); - } - - // Sorting - const sortBy = Object.fromEntries(sortOrders.map(({ path, direction }) => [path, direction])); - if (sortBy.fullName) { - result = result.sort((p1, p2) => compare(p1.fullName, p2.fullName, sortBy.fullName)); - } else if (sortBy.profession) { - result = result.sort((p1, p2) => compare(p1.profession, p2.profession, sortBy.profession)); - } - - // Pagination - const count = result.length; - const offset = page * pageSize; - result = result.slice(offset, offset + pageSize); - - return { people: result, count }; -} - function Example() { useSignals(); // hidden-source-line const searchTerm = useSignal(''); - const dataProvider = useMemo( - () => - async ( - params: GridDataProviderParams, - callback: GridDataProviderCallback - ) => { - const { page, pageSize, sortOrders } = params; - - const { people, count } = await fetchPeople({ - page, - pageSize, - sortOrders, - searchTerm: searchTerm.value, - }); - - callback(people, count); - }, + // Create a data provider that calls a backend service with a + // Spring Data pageable and the search term + const dataProvider = useGridDataProvider( + async (pageable) => GridPersonService.list(pageable, searchTerm.value), + // Providing the search term as a dependency will automatically + // refresh the data provider when the search term changes [searchTerm.value] ); diff --git a/frontend/demo/services/GridPersonService.ts b/frontend/demo/services/GridPersonService.ts new file mode 100644 index 0000000000..6e9603aacc --- /dev/null +++ b/frontend/demo/services/GridPersonService.ts @@ -0,0 +1,53 @@ +import { getPeople } from 'Frontend/demo/domain/DataService'; +import { CrudMockService } from 'Frontend/demo/services/CrudService'; +import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; +import type OrFilter from 'Frontend/generated/com/vaadin/hilla/crud/filter/OrFilter'; +import Matcher from 'Frontend/generated/com/vaadin/hilla/crud/filter/PropertyStringFilter/Matcher'; +import type Pageable from 'Frontend/generated/com/vaadin/hilla/mappedtypes/Pageable'; + +interface PersonWithFullName extends Person { + fullName: string; +} + +class GridPersonService { + private mockService?: CrudMockService; + + async list(pageable: Pageable, searchTerm: string): Promise { + await this.initMockService(); + + return this.mockService!.list(pageable, this.createFilter(searchTerm)); + } + + private createFilter(searchTerm: string): OrFilter { + return { + '@type': 'or', + children: [ + { + '@type': 'propertyString', + propertyId: 'fullName', + filterValue: searchTerm, + matcher: Matcher.CONTAINS, + }, + { + '@type': 'propertyString', + propertyId: 'profession', + filterValue: searchTerm, + matcher: Matcher.CONTAINS, + }, + ], + }; + } + + private async initMockService() { + if (this.mockService) { + return; + } + const data = (await getPeople()).people.map((person) => ({ + ...person, + fullName: `${person.firstName} ${person.lastName}`, + })); + this.mockService = new CrudMockService(data); + } +} + +export default new GridPersonService(); diff --git a/frontend/demo/services/mocks.ts b/frontend/demo/services/mocks.ts index 8dd97f0441..1e3d5f66cb 100644 --- a/frontend/demo/services/mocks.ts +++ b/frontend/demo/services/mocks.ts @@ -2,7 +2,8 @@ // During the build, the `Frontend/generated/endpoints` import is replaced with this module import DashboardService from 'Frontend/demo/services/DashboardService'; import EmployeeService from 'Frontend/demo/services/EmployeeService'; +import GridPersonService from 'Frontend/demo/services/GridPersonService'; import ProductService from 'Frontend/demo/services/ProductService'; export * from 'Frontend/generated/endpoints.js'; -export { DashboardService, EmployeeService, ProductService }; +export { DashboardService, EmployeeService, ProductService, GridPersonService }; diff --git a/src/main/java/com/vaadin/demo/component/grid/GridPersonRepository.java b/src/main/java/com/vaadin/demo/component/grid/GridPersonRepository.java new file mode 100644 index 0000000000..b1c38165c4 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/grid/GridPersonRepository.java @@ -0,0 +1,26 @@ +package com.vaadin.demo.component.grid; + +import com.vaadin.demo.domain.Person; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; + +import java.util.List; + +// hidden-source-line - We don't want to register an actual JPA repository here +// hidden-source-line - So we put the source code to show in a comment and declare a dummy interface that is hidden in the docs +// tag::snippet[] +/* // hidden-source-line +public interface GridPersonRepository extends JpaRepository { + List findByFullNameContainingIgnoreCaseOrProfessionContainingIgnoreCase( + String fullName, String profession, Pageable pageable); +} +*/ // hidden-source-line +// end::snippet[] + +@Component // hidden-source-line +public class GridPersonRepository { // hidden-source-line + List findByFullNameContainingIgnoreCaseOrProfessionContainingIgnoreCase(String fullName, String profession, Pageable pageable) { // hidden-source-line + return List.of(); // hidden-source-line + } // hidden-source-line +}// hidden-source-line diff --git a/src/main/java/com/vaadin/demo/component/grid/GridPersonService.java b/src/main/java/com/vaadin/demo/component/grid/GridPersonService.java new file mode 100644 index 0000000000..19ac3197ff --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/grid/GridPersonService.java @@ -0,0 +1,27 @@ +package com.vaadin.demo.component.grid; + +import com.vaadin.demo.domain.Person; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.hilla.BrowserCallable; +import org.jspecify.annotations.NonNull; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +// tag::snippet[] +@BrowserCallable +@AnonymousAllowed +public class GridPersonService { + private final GridPersonRepository personRepository; + + public GridPersonService(GridPersonRepository personRepository) { + this.personRepository = personRepository; + } + + public @NonNull List<@NonNull Person> list(Pageable pageable, String filter) { + // Implement your data fetching logic here + // For this example, we're using a Spring Data repository + return personRepository.findByFullNameContainingIgnoreCaseOrProfessionContainingIgnoreCase(filter, filter, pageable); + } +} +// end::snippet[]