From 4ae6aef71efb1405262681f717ef49ed03155128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Wed, 11 Nov 2026 16:15:12 +0200 Subject: [PATCH 1/2] docs: move the React how-tos to Building Apps Add three Building Apps how-tos for React views, each verified in a running application: - Work Offline, from the client caching guide: cache the application with @PWA, keep service data in local storage with a fallback only for network errors, and clear it at logout. - Upload and Download Files: the Upload component posts multipart data to a Spring controller, with the CSRF header and the size limits, and a link with the download attribute downloads the file. The old guide's approach, cancelling the Upload request and calling a service, stops after three uploads (vaadin/web-components#13387). - Test React Views, rewritten for Vitest browser mode with Playwright and React Testing Library, with vi.mock() for the generated clients. Remove the Hilla guides for client caching, uploads and downloads, testing, data grids and From Java to React, the FAQ, and the now empty Guides index, with redirects. Update the links to them. Co-Authored-By: Claude Opus 5.5 --- .../config/vocabularies/Docs/accept.txt | 1 + .../architecture/when-to-use-vaadin.adoc | 2 +- .../forms-data/add-grid/index.adoc | 2 +- articles/building-apps/react/index.adoc | 5 +- articles/building-apps/react/test-views.adoc | 208 ++++++++++ .../building-apps/react/upload-download.adoc | 94 +++++ .../building-apps/react/work-offline.adoc | 88 ++++ articles/building-apps/testing/index.adoc | 2 +- .../type-conversion.adoc | 2 +- articles/hilla/faq.adoc | 104 ----- articles/hilla/guides/client-caching.adoc | 103 ----- articles/hilla/guides/data-grids/data.sql | 50 --- articles/hilla/guides/data-grids/index.adoc | 211 ---------- articles/hilla/guides/from-java-to-react.adoc | 384 ------------------ articles/hilla/guides/index.adoc | 12 - articles/hilla/guides/testing.adoc | 298 -------------- articles/hilla/guides/uploads-downloads.adoc | 63 --- dspublisher/config/default.json | 19 +- frontend/demo/fusion/files/files-view.tsx | 46 +++ .../demo/fusion/forms/upload/download.tsx | 25 -- frontend/demo/fusion/forms/upload/upload.tsx | 23 -- frontend/demo/fusion/offline/cacheable.ts | 36 ++ frontend/demo/fusion/offline/tasks-view.tsx | 30 ++ frontend/demo/fusion/testing/todo-view.tsx | 35 ++ .../java/com/vaadin/demo/SecurityConfig.java | 10 - .../download/FileDownloadController.java | 28 -- .../demo/fusion/files/FileController.java | 63 +++ .../demo/fusion/offline/TaskService.java | 19 + .../demo/fusion/offline/package-info.java | 4 + .../demo/fusion/testing/TodoService.java | 15 + .../demo/fusion/upload/UploadService.java | 30 -- 31 files changed, 660 insertions(+), 1352 deletions(-) create mode 100644 articles/building-apps/react/test-views.adoc create mode 100644 articles/building-apps/react/upload-download.adoc create mode 100644 articles/building-apps/react/work-offline.adoc delete mode 100644 articles/hilla/faq.adoc delete mode 100644 articles/hilla/guides/client-caching.adoc delete mode 100644 articles/hilla/guides/data-grids/data.sql delete mode 100644 articles/hilla/guides/data-grids/index.adoc delete mode 100644 articles/hilla/guides/from-java-to-react.adoc delete mode 100644 articles/hilla/guides/index.adoc delete mode 100644 articles/hilla/guides/testing.adoc delete mode 100644 articles/hilla/guides/uploads-downloads.adoc create mode 100644 frontend/demo/fusion/files/files-view.tsx delete mode 100644 frontend/demo/fusion/forms/upload/download.tsx delete mode 100644 frontend/demo/fusion/forms/upload/upload.tsx create mode 100644 frontend/demo/fusion/offline/cacheable.ts create mode 100644 frontend/demo/fusion/offline/tasks-view.tsx create mode 100644 frontend/demo/fusion/testing/todo-view.tsx delete mode 100644 src/main/java/com/vaadin/demo/fusion/download/FileDownloadController.java create mode 100644 src/main/java/com/vaadin/demo/fusion/files/FileController.java create mode 100644 src/main/java/com/vaadin/demo/fusion/offline/TaskService.java create mode 100644 src/main/java/com/vaadin/demo/fusion/offline/package-info.java create mode 100644 src/main/java/com/vaadin/demo/fusion/testing/TodoService.java delete mode 100644 src/main/java/com/vaadin/demo/fusion/upload/UploadService.java diff --git a/.github/styles/config/vocabularies/Docs/accept.txt b/.github/styles/config/vocabularies/Docs/accept.txt index 2f16732c5d..fefebb54ce 100644 --- a/.github/styles/config/vocabularies/Docs/accept.txt +++ b/.github/styles/config/vocabularies/Docs/accept.txt @@ -272,6 +272,7 @@ Valo [vV]iewports? Viritin Vite +Vitest VMware Vue VU diff --git a/articles/building-apps/architecture/when-to-use-vaadin.adoc b/articles/building-apps/architecture/when-to-use-vaadin.adoc index a1067e3612..2c0ba0ba03 100644 --- a/articles/building-apps/architecture/when-to-use-vaadin.adoc +++ b/articles/building-apps/architecture/when-to-use-vaadin.adoc @@ -67,7 +67,7 @@ Vaadin recommends another tool, at least for part of the application, when one o *Search engine ranking.* Vaadin builds both Flow and React views in the browser with JavaScript, and doesn't render pages to HTML on the server for search engines. A Flow view also keeps a UI on the server for every visitor, crawlers included. Keep content pages on a content management system or a static site, and link from them to your application. -*Mobile-first or offline-first use.* Vaadin's layouts adapt to the screen size in the browser. However, every interaction that runs Java code adds the latency of the mobile network, and a Flow view stops working when the connection drops. React views can keep working offline, but you write the code that stores data in the browser and synchronizes it with the server, as described in <<{articles}/hilla/guides/client-caching#,Client-Side Data Caching>>. If most of the application has to work offline, most of it ends up as React views, and the benefits of writing the UI in Java no longer apply. +*Mobile-first or offline-first use.* Vaadin's layouts adapt to the screen size in the browser. However, every interaction that runs Java code adds the latency of the mobile network, and a Flow view stops working when the connection drops. React views can keep working offline, but you write the code that stores data in the browser and synchronizes it with the server, as described in <<{articles}/building-apps/react/work-offline#,Work Offline>>. If most of the application has to work offline, most of it ends up as React views, and the benefits of writing the UI in Java no longer apply. *A highly custom user interface.* Vaadin is designed for applications whose views follow one consistent design. The default theme styles the UI components without any CSS of your own, and you can <<{articles}/styling#,style>> them further, for example to match a brand. When a designer specifies each screen in detail, and the user interface is the product, a hand-written frontend gives more control. React views give that control within a view, and can still use Vaadin's UI components. diff --git a/articles/building-apps/forms-data/add-grid/index.adoc b/articles/building-apps/forms-data/add-grid/index.adoc index d66e6df22d..1addb57719 100644 --- a/articles/building-apps/forms-data/add-grid/index.adoc +++ b/articles/building-apps/forms-data/add-grid/index.adoc @@ -24,4 +24,4 @@ section_outline::[] For detailed information about binding data sets to UI components in Vaadin, see the <> reference guide. -The guides use Flow views. For grids in React views, see the <> reference guide. +The guides use Flow views. For grids in React views, see <<{articles}/components/auto-grid#,Auto Grid>>, which loads its data from a browser-callable service. diff --git a/articles/building-apps/react/index.adoc b/articles/building-apps/react/index.adoc index 9235bfa6ed..7091b7d4c2 100644 --- a/articles/building-apps/react/index.adoc +++ b/articles/building-apps/react/index.adoc @@ -71,5 +71,8 @@ This section covers: - <> -- link between views (including to and from Flow views), pass route and query parameters, and wrap views in shared layouts. - <> -- make a Java service browser-callable and call it from a React view in a type-safe way. - <> -- require authentication, restrict access by role, add login and logout, and protect the services behind your views. +- <> -- load the application without the server, and keep the data of a view available. +- <> -- upload files with the Upload component, and let users download them. +- <> -- test views in a browser with Vitest and React Testing Library, with mocked services. -For the reference documentation of React views, see <<{articles}/flow/react-views#,React Views>> in the Flow Reference. For form building, validation, and other in-depth React topics, see the <<{articles}/hilla/guides#,Hilla reference guides>>. +For the reference documentation of React views, including form binding and browser-callable services, see <<{articles}/flow/react-views#,React Views>> in the Flow Reference. diff --git a/articles/building-apps/react/test-views.adoc b/articles/building-apps/react/test-views.adoc new file mode 100644 index 0000000000..de40b38339 --- /dev/null +++ b/articles/building-apps/react/test-views.adoc @@ -0,0 +1,208 @@ +--- +title: Test React Views +page-title: How to test React views in a Vaadin application with Vitest +description: Test React views in a browser with Vitest and React Testing Library, without a running server. +meta-description: Test Vaadin React views with Vitest browser mode and React Testing Library - set up Vitest, render views, simulate users, and mock browser-callable services. +order: 70 +--- + + += Test React Views +:toclevels: 2 + +React views run in the browser, so you test them with JavaScript tools. This guide uses https://vitest.dev/guide/browser/[Vitest in browser mode], which runs the tests in a real browser, and https://testing-library.com/docs/react-testing-library/intro/[React Testing Library], which renders a view and works with it like a user. The tests replace the browser-callable services with mocks, so they don't need a running server. + +Test the services themselves with Java tests, as described in <<../testing#,Testing>>. To test the whole application in a browser, see <<{articles}/flow/testing/end-to-end#,End-to-End Testing>>. + + +== Setting Up Vitest + +Install Vitest, its Playwright provider, and React Testing Library, and then the Chromium browser that the tests run in: + +[source,bash] +---- +npm install --save-dev vitest @vitest/browser-playwright @testing-library/react @testing-library/dom @testing-library/user-event +---- + +[source,bash] +---- +npx playwright install chromium +---- + +Create a [filename]`vitest.config.ts` file in the project folder: + +[source,typescript] +.[filename]`vitest.config.ts` +---- +import { fileURLToPath } from 'node:url'; +import babel from '@rolldown/plugin-babel'; +import react from '@vitejs/plugin-react'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + react(), + babel({ // <1> + include: '**/*.tsx', + plugins: [['module:@preact/signals-react-transform', { mode: 'all' }]], + }), + ], + optimizeDeps: { + include: ['@preact/signals-react/runtime'], // <2> + }, + resolve: { + alias: { + Frontend: fileURLToPath(new URL('./src/main/frontend', import.meta.url)), // <3> + }, + }, + test: { + include: ['src/test/frontend/**/*.test.tsx'], + globals: true, // <4> + browser: { + enabled: true, + provider: playwright(), + headless: true, + instances: [{ browser: 'chromium' }], + }, + }, +}); +---- +<1> The signals transform makes a component render again when a signal that it reads changes, like in the application. Vaadin already adds these packages to [filename]`package.json`. +<2> The transform adds an import that Vite doesn't find in advance. Without this line, a run without a Vite cache, such as on a CI server, fails. +<3> Resolves the `Frontend/` imports of the views. +<4> Lets React Testing Library clean up the page after each test. + +Add a script that runs the tests to [filename]`package.json`: + +[source,json] +.[filename]`package.json` +---- +"scripts": { + "test": "vitest" +} +---- + +For type checking in your editor, add a [filename]`tsconfig.json` file to the folder of the tests. The [filename]`tsconfig.json` in the project folder covers only [filename]`src/main/frontend`, and Vaadin may replace it, so don't change it for the tests: + +[source,json] +.[filename]`src/test/frontend/tsconfig.json` +---- +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "types": ["@vitest/browser-playwright"] + }, + "include": ["**/*", "../../../types.d.ts"] +} +---- + +When a test fails, Vitest saves a screenshot in the [filename]`.vitest` folder. Add the folder to [filename]`.gitignore`. + + +== Generating the Service Clients + +The views import the TypeScript clients that Vaadin generates for the browser-callable services, so the tests need them too, even when they mock the services. Vaadin generates the clients when the application starts in development mode. Without starting it, for example on a CI server, generate them with Maven before running the tests: + +[source,bash] +---- +mvn compile vaadin:generate +---- + + +== Writing a Test + +The examples in this guide test the following view, which adds todos through a service: + +[source,tsx] +.[filename]`src/main/frontend/views/todo.tsx` +---- +include::{root}/frontend/demo/fusion/testing/todo-view.tsx[tags=snippet,indent=0] +---- + +[source,java] +.[filename]`TodoService.java` +---- +include::{root}/src/main/java/com/vaadin/demo/fusion/testing/TodoService.java[tags=snippet,indent=0] +---- + +Put the tests in [filename]`src/test/frontend`, not in the [filename]`views` folder, because the router treats every file there as a view. The following test renders the view and checks its heading: + +[source,tsx] +.[filename]`src/test/frontend/views/todo.test.tsx` +---- +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import TodoView from 'Frontend/views/todo'; + +describe('TodoView', () => { + it('shows the heading', () => { + render(); + expect(screen.getByText('Todos')).toBeInTheDocument(); + }); +}); +---- + +Run the tests: + +[source,bash] +---- +npm test +---- + +In a terminal, Vitest keeps running and runs the tests again when a file changes. To run them once, for example on a CI server, use `npx vitest run`. + + +== Mocking Services + +A view that calls a service in a test would send the call to the test server, which doesn't know the service. Replace the generated clients with mocks by calling `vi.mock()` at the top level of the test file: + +[source,tsx] +---- +import { TodoService } from 'Frontend/generated/endpoints'; +import { beforeEach, vi } from 'vitest'; + +vi.mock('Frontend/generated/endpoints', () => ({ + TodoService: { addTodo: vi.fn() }, +})); + +beforeEach(() => { + vi.clearAllMocks(); +}); +---- + +The mock replaces the module for every test in the file, and for the views that the tests render. It contains only the services and methods that you list. The `beforeEach()` hook clears the recorded calls before each test. + +You can't replace a single method with `vi.spyOn(TodoService, 'addTodo')`, because the generated clients are ES modules, which can't be changed. + + +== Testing User Interaction + +The `userEvent` object of React Testing Library types and clicks like a user. The following tests add a todo, and check that the view shows it and sends it to the service: + +[source,tsx] +---- +import userEvent from '@testing-library/user-event'; + +it('adds a todo', async () => { + const user = userEvent.setup(); + render(); + + await user.type(await screen.findByLabelText('New todo'), 'Buy milk'); + await user.click(screen.getByRole('button', { name: 'Add' })); + + expect(await screen.findByText('Buy milk')).toBeInTheDocument(); +}); + +it('saves the todo through the service', async () => { + const user = userEvent.setup(); + render(); + + await user.type(await screen.findByLabelText('New todo'), 'Buy milk'); + await user.click(screen.getByRole('button', { name: 'Add' })); + + expect(TodoService.addTodo).toHaveBeenCalledWith('Buy milk'); +}); +---- + +Vaadin components set up their labels after the first render, so find a field by its label with `findByLabelText()`, which waits for it, rather than with `getByLabelText()`. For the other queries and actions, see the https://testing-library.com/docs/react-testing-library/intro/[React Testing Library documentation]. diff --git a/articles/building-apps/react/upload-download.adoc b/articles/building-apps/react/upload-download.adoc new file mode 100644 index 0000000000..390e9a2f43 --- /dev/null +++ b/articles/building-apps/react/upload-download.adoc @@ -0,0 +1,94 @@ +--- +title: Upload and Download Files +page-title: How to upload and download files in Vaadin React views +description: Upload files from a React view with the Upload component, and let users download them, through a Spring controller. +meta-description: Upload and download files in Vaadin React views with the Upload component and a Spring controller, protected by Spring Security, with CSRF and size limits. +order: 60 +--- + + += Upload and Download Files +:toclevels: 2 + +This guide shows how to upload files from a React view with the <<{articles}/components/upload#,Upload>> component, and how to let users download them. Both go through a Spring controller: the Upload component sends each file in a request of its own, and a browser downloads a file from a URL. + + +== Creating the Controller + +Create a Spring controller with one method that receives an uploaded file, and one that sends a file back. The following controller keeps the files in memory and gives each one an ID: + +[source,java] +.[filename]`FileController.java` +---- +include::{root}/src/main/java/com/vaadin/demo/fusion/files/FileController.java[tags=snippet,indent=0] +---- + +The [methodname]`upload()` method returns the ID of the file as the response text. The [methodname]`download()` method sends the file with a `Content-Disposition` header, which makes the browser save it under its original name. [classname]`ContentDisposition` encodes names that contain other than ASCII characters. + +Keep the controller's path apart from the routes of your views, for example under `/api`. That way, the security rule for the controller doesn't apply to a view. + + +== Allowing Access to the Controller + +Vaadin's security configuration denies the requests that no rule allows, so add a rule for the controller. The following rule lets logged-in users upload and download files: + +[source,java] +---- +@Bean +SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(auth -> auth + .requestMatchers("/api/files/**").authenticated()); + http.with(VaadinSecurityConfigurer.vaadin(), configurer -> { + configurer.loginView("/login"); + }); + return http.build(); +} +---- + +For the other options, see <<{articles}/flow/security/spring/security-configuration#,Security Configuration>> in the reference. + + +== Uploading Files + +Point the `target` of the Upload component to the controller, and send the files as multipart form data. By default, the component sends the raw content of the file, which a [classname]`MultipartFile` parameter doesn't accept: + +[source,tsx] +---- +include::{root}/frontend/demo/fusion/files/files-view.tsx[tags=upload,indent=0] +---- + +Spring Security rejects a `POST` request without a CSRF token. Vaadin adds the token to the page in `` tags, and the `onUploadRequest` handler copies it to each upload request: + +[source,tsx] +---- +include::{root}/frontend/demo/fusion/files/files-view.tsx[tags=csrf,indent=0] +---- + +The handler reads the token for each request, because the token changes when the user logs in. + +When the upload succeeds, `xhr.responseText` holds the response of the controller, here the ID of the file. A relative `target`, without a leading slash, also works when the application has a context path. + + +=== Limiting the File Size + +Spring Boot accepts files of up to 1 MB by default. For a larger file, the server answers with status 413, and the Upload component shows that the file is too large. To accept larger files, set the limits in [filename]`application.properties`: + +[source,properties] +---- +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB +---- + +To reject a large file before it's uploaded, also set the `maxFileSize` property of the Upload component, in bytes. + + +== Downloading Files + +To let users download a file, link to the download URL of the controller, and add the `download` attribute to the link: + +[source,tsx] +---- +include::{root}/frontend/demo/fusion/files/files-view.tsx[tags=download,indent=0] +---- + +The router ignores links that have the `download` attribute, so the browser downloads the file instead of opening a view. diff --git a/articles/building-apps/react/work-offline.adoc b/articles/building-apps/react/work-offline.adoc new file mode 100644 index 0000000000..5224bedbfe --- /dev/null +++ b/articles/building-apps/react/work-offline.adoc @@ -0,0 +1,88 @@ +--- +title: Work Offline +page-title: How to make React views work offline in a Vaadin application +description: Keep React views and their data available when the browser can't reach the server. +meta-description: Make Vaadin React views work offline - cache the application with a service worker, keep service data in the browser, and clear it when the user logs out. +order: 50 +--- + + += Work Offline +:toclevels: 2 + +A React view runs in the browser, so it can keep working when the browser can't reach the server. This guide shows how to make the application load without the server, and how to keep the data of a view available. Flow views need the server, so they show an offline page instead. + +The guide covers reading data offline. A call that changes data fails while the server can't be reached. To let users make changes offline, the application has to store them in the browser and send them when the connection is back. + + +== Caching the Application + +Add the [annotationname]`@PWA` annotation to the application shell class. Vaadin then generates a service worker, which caches the application shell and the frontend files, including the React views: + +[source,java] +---- +@PWA(name = "Task Tracker", shortName = "Tasks") +public class Application implements AppShellConfigurator { + // ... +} +---- + +The browser installs the service worker the first time it opens the application. After that, the application loads from the cache when the server can't be reached, and React views open as usual. Try this with a production build: in development mode, the service worker doesn't cache the frontend files. + +The service worker doesn't cache the data that views get from services. For the other options of the annotation, see <<{articles}/flow/configuration/pwa#,Progressive Web Applications>>. + + +== Keeping Data in the Browser + +A service call fails when the server can't be reached. To show data anyway, store the result of each successful call in the browser, and use the stored result when a call fails. The following helper does this with local storage: + +[source,typescript] +.[filename]`cacheable.ts` +---- +include::{root}/frontend/demo/fusion/offline/cacheable.ts[tags=snippet,indent=0] +---- + +The helper uses the stored result only when the call doesn't reach the server. When the server answers with an error, such as access denied, the call throws an `EndpointError`, which the helper passes on. For more about errors, see <<{articles}/flow/react-views/browser-callable-services#errors,Errors>> in the reference. + +Wrap the service calls of a view with the helper: + +[source,tsx] +.[filename]`views/tasks.tsx` +---- +include::{root}/frontend/demo/fusion/offline/tasks-view.tsx[tags=snippet,indent=0] +---- + +When the browser opens the view without a connection to the server, the view shows the tasks that it stored the last time, or an empty list. + + +== Showing the Connection Status + +Vaadin shows a status message at the top of the page when a call can't reach the server. You don't need to add anything for it. To change its texts or its look, see <<{articles}/flow/advanced/loading-indicator#,Loading and Connection Indicator>>. + + +== Clearing the Data at Logout + +The data in local storage stays in the browser after the user logs out. Whoever uses the browser next can read it. Clear the data that may not stay before you log the user out: + +[source,tsx] +---- +import { Button } from '@vaadin/react-components'; +import { clearCache } from 'Frontend/cacheable'; +import { useAuth } from 'Frontend/security/auth'; + +export default function LogoutButton() { + const { logout } = useAuth(); + return ( + + ); +} +---- + +For how to add login and logout, see <>. diff --git a/articles/building-apps/testing/index.adoc b/articles/building-apps/testing/index.adoc index 8f2b52e335..61ba2ca08e 100644 --- a/articles/building-apps/testing/index.adoc +++ b/articles/building-apps/testing/index.adoc @@ -44,7 +44,7 @@ Each layer of your application is tested in its own way. The following table sho Service and browserless tests are fast and don't need a browser, so they can cover most of your application, including its use cases from start to finish. Keep end-to-end tests for what only a browser can show, as described in <<#browser-tests,Keep a Thin Layer of Browser Tests>>. -Browserless tests can't test <<../react#,React views>>, because React views run in the browser. Test the browser-callable services of a React view like any other application service, and the view itself with end-to-end tests, or with the JavaScript tools described in <<{articles}/hilla/guides/testing#,Testing>> in the Hilla reference. +Browserless tests can't test <<../react#,React views>>, because React views run in the browser. Test the browser-callable services of a React view like any other application service. Test the view itself with end-to-end tests, or with the JavaScript tools described in <<../react/test-views#,Test React Views>>. To run the tests on every change, see <<../production/ci#,Build on CI>>. diff --git a/articles/flow/react-views/browser-callable-services/type-conversion.adoc b/articles/flow/react-views/browser-callable-services/type-conversion.adoc index ace282c674..170808ae24 100644 --- a/articles/flow/react-views/browser-callable-services/type-conversion.adoc +++ b/articles/flow/react-views/browser-callable-services/type-conversion.adoc @@ -82,7 +82,7 @@ The Java types map to TypeScript types as follows: |`MultipartFile` |`File` -|Only as a method parameter. See <<{articles}/hilla/guides/uploads-downloads#,Uploads & Downloads>>. +|Only as a method parameter. A call with a `File` argument sends multipart form data. |=== Vaadin generates a TypeScript type for each bean, record, and enum that a service method uses, directly or through other types. The generated files are in [filename]`src/main/frontend/generated`, in directories named after the Java package. For example, the type of `com.example.application.Person` is in [filename]`com/example/application/Person.ts`. diff --git a/articles/hilla/faq.adoc b/articles/hilla/faq.adoc deleted file mode 100644 index 3d16d99f6b..0000000000 --- a/articles/hilla/faq.adoc +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: FAQ -page-title: Frequently asked questions about Hilla | Vaadin -description: "Frequently asked questions about the Hilla framework." -meta-description: Find answers to frequently asked questions about getting started with the Vaadin Hilla framework. -order: 15 ---- - -= FAQ - - -== What is Hilla? - -Hilla is a framework for building reactive web applications on Java backends. -It seamlessly integrates a reactive TypeScript frontend -- https://reactjs.org/[React], or https://lit.dev/[Lit] in the deprecated Hilla Lit version -- with a https://spring.io/projects/spring-boot[Spring Boot] backend. - -Hilla is designed to be simple enough for small utilities, but robust enough to build complex, enterprise-grade applications. - -The automatic type generation ensures that your frontend and backend always stay in sync and that you catch mistakes at build time, not runtime. - - -== What are the Benefits of Using Hilla? - -Hilla simplifies the process of creating business applications. It has many benefits: - -- Saves development time by simplifying communication between the frontend and Java backend, eliminating the need to create a REST API; -- Enforces end-to-end type-safety, identifying errors during the build process, when they are faster to fix, and ensuring they don't make it into production; -- Reduces the time to build a UI with more than 45 ready-made components plus a fully-customizable theme, all maintained by Vaadin; -- Simplifies security with browser-callable services that are secured by default and validation that ensures data integrity across the server and client; and -- Gets you started quickly with a zero-configuration toolchain for building both your frontend and backend. - - -== Hilla is Made for Whom? - -Hilla provides a simpler way to build complex business applications. A smaller team, or even a solo developer, can build the full stack, instead of having separate frontend and backend teams synchronizing their work. Hilla is also made for those who value open-source transparency together with the option of first-party support for their business. - - -== Who's Behind Hilla? - -Hilla is made and maintained by https://vaadin.com[Vaadin], the company behind the top Java web framework and corresponding tools and components for over 20 years. - - -== Is Hilla Open Source? - -Yes, Hilla is Apache 2.0 licensed. Hilla is hosted in a public https://github.com/vaadin/hilla[GitHub repository] where you can view, extend and modify the source code and report issues. - - -== Is there Enterprise Support for Hilla? - -Hilla is supported by Vaadin. All the commercial support, components, warranty, and services apply to Hilla. See the https://vaadin.com/pricing/hilla[Vaadin pricing page] for details. - - -== How is Hilla Different from React? - -Hilla isn't an alternative to React: it uses React for the frontend and puts the Java backend in the same project. Hilla includes everything you need to build an application in one package: UI components, routing, forms, security, build tooling. You can get first-party support for using Hilla from Vaadin, the company that built it. - - -== Can I Use Other React Components with Hilla? - -Yes, you can use any React components with Hilla, install them with npm and import them in your view or component modules in the same way as any of Vaadin's UI components. - - -== How is Hilla Different from Angular? - -Angular is a frontend framework for mobile and desktop applications with modules, dependency injection, and other features that enterprise application developers are used to. - -Hilla builds on web standards: its UI components are web components, used from React. -Unlike Angular, Hilla includes both the frontend and Java backend in the same project. Hilla includes everything you need to build an application in one package: UI components, routing, forms, security, build tooling. You can get first-party support for using Hilla from Vaadin, the company that built it. - - -== What's the Difference Between Hilla and Vue? - -Vue.js is a lightweight frontend framework with a framework-specific component model. - -Hilla builds on web standards: its UI components are web components, used from React. -Unlike Vue, Hilla includes both the frontend and Java backend in the same project. Hilla includes everything you need to build an application in one package: UI components, routing, forms, security, build tooling. You can get first-party support for using Hilla from Vaadin, the company that built it. - - -== How does Hilla Compare to JHipster? - -JHipster provides a set of tools for generating full-stack web applications and microservices by combining a Java Spring backend with a React/Angular/Vue frontend. - -Hilla is an opinionated full-stack framework that includes a Spring Boot backend and a React frontend. -It includes UI components, routing, forms, security, and build tooling. As the framework is updated, the Hilla development team ensures that the different parts of it remain interoperable, and offer documentation for upgrading when needed. - - -== Can Jakarta EE or Quarkus Replace Spring Boot? - -Neither Jakarta EE nor Quarkus support is on the roadmap. If you're interested in using either technology, show your interest in https://github.com/vaadin/hilla/issues/211[this GitHub issue]. If there is enough demand, that gives the development team some guidance to adjust the priorities. - - -== Does Hilla Work with Multi-Module Projects? - -Yes. You can use standard Maven modules in your application, or even external dependencies, as there is no need for the service and entity classes to be in the same project as the Hilla application. See <<{articles}/flow/react-views/browser-callable-services/code-generation#discovery,Code Generation>> for details. - - -== Does Kotlin, Scala, or Groovy Work with Hilla? - -Hilla is developed using Java as the reference language for the backend. Hilla parses `.class` files, which can be compiled from other languages as well, but support from some features may vary depending on the language. For example, in Java, an annotation is used to mark a field as not null, while in Kotlin that's part of the language syntax. - - -== Does Hilla Support a Microservice Architecture? - -Yes. Hilla only requires a thin service layer to communicate with the frontend. The rest of the backend can employ a microservice setup with which the browser-callable services would communicate. See the https://vaadin.com/blog/using-microservices-from-hilla/["Using microservices from Hilla"] article for more details. diff --git a/articles/hilla/guides/client-caching.adoc b/articles/hilla/guides/client-caching.adoc deleted file mode 100644 index 38516d1a98..0000000000 --- a/articles/hilla/guides/client-caching.adoc +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: Client-Side Data Caching -page-title: How to implement client caching in Vaadin Hilla -description: Understanding when and how to cache client-side data. -meta-description: Optimize your Hilla applications with client-side caching techniques. Read here to understand how to implement client caching in Vaadin Hilla. -order: 75 ---- -// TODO perhaps this information could be more tied to creating views? - -= Client-Side Data Caching - -Hilla caches client-side views out of the box, but not client-side data. -This can even lead to a situation where the client-side view is available offline, which isn't very useful, since there is no data. -To overcome this, you can use https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage[`localStorage`] to store the client-side data, so that the data is available even offline. - -== Cacheable Wrapper with Local Storage - -Assume you have a function to retrieve the data from the backend. -After the data is retrieved, you can save it into `localStorage` as a key-value pair. -It's also good to provide a default value, in case the data is available from neither the backend nor cache. - -*Example*: define a cacheable helper in [filename]`cacheable.ts` to cache client-side data. - -[source,typescript] ----- -export async function cacheable(fn: () => Promise, key: string, defaultValue: T) { - let result; - try { - // retrieve the data from backend. - result = await fn(); - // save the data to localStorage. - localStorage.setItem(key, JSON.stringify(result)); - } catch { - // if failed to retrieve the data from backend, try localStorage. - const cached = localStorage.getItem(key); - // use the cached data if available, otherwise the default value. - result = cached ? JSON.parse(cached) : defaultValue; - } - - return result; -} ----- - -Now you can use the cacheable wrapper in your client-side view. - -*Example*: use the cacheable wrapper. - -[source,typescript] ----- -// import the cacheable function from the path to cacheable.ts file (without the suffix) -import { cacheable } from 'path-to-cacheable'; - -// instead of always relying on the backend to retrieve the data. e.g. -// const stats = await getStats(); -// you can now wrap getStats into the cacheable helper. -const stats = await cacheable(() => getStats(), 'stats', {}); ----- - -== Clear the Local Storage - -It's good practice to clear the data stored in the `localStorage`, for example when a user logs out. -Suppose there is a `logout` event. -The above cacheable wrapper could be improved as: - -[source,typescript] ----- -// the name of the cache for offline usage -const CACHE_NAME = 'offline-cache'; - -window.addEventListener('logout', () => { - // clear the data from localStorage when a user logs out - clearCache(); -}); - -export async function cacheable(fn: () => Promise, key: string, defaultValue: T) { - let result; - try { - // retrieve the data from backend. - result = await fn(); - // save the data to localStorage. - const cache = getCache(); - cache[key] = result; - localStorage.setItem(CACHE_NAME, JSON.stringify(cache)); - } catch { - // if failed to retrieve the data from backend, try localStorage. - const cache = getCache(); - const cached = cache[key]; - // use the cached data if available, otherwise the default value. - result = cached === undefined ? defaultValue : cached; - } - - return result; -} - -function getCache(): any { - const cache = localStorage.getItem(CACHE_NAME) || '{}'; - return JSON.parse(cache); -} - -function clearCache() { - localStorage.removeItem(CACHE_NAME); -} ----- diff --git a/articles/hilla/guides/data-grids/data.sql b/articles/hilla/guides/data-grids/data.sql deleted file mode 100644 index bc5064016e..0000000000 --- a/articles/hilla/guides/data-grids/data.sql +++ /dev/null @@ -1,50 +0,0 @@ -INSERT INTO product (name, category, price, date_added) VALUES('Apple', 'Fruit', 1.99, '2019-01-01'); -INSERT INTO product (name, category, price, date_added) VALUES('Banana', 'Fruit', 2.99, '2019-01-02'); -INSERT INTO product (name, category, price, date_added) VALUES('Carrot', 'Vegetable', 3.99, '2019-01-03'); -INSERT INTO product (name, category, price, date_added) VALUES('Eggplant', 'Vegetable', 5.99, '2019-01-05'); -INSERT INTO product (name, category, price, date_added) VALUES('Orange', 'Fruit', 2.49, '2019-01-06'); -INSERT INTO product (name, category, price, date_added) VALUES('Broccoli', 'Vegetable', 4.29, '2019-01-07'); -INSERT INTO product (name, category, price, date_added) VALUES('Grapes', 'Fruit', 3.79, '2019-01-08'); -INSERT INTO product (name, category, price, date_added) VALUES('Cucumber', 'Vegetable', 2.89, '2019-01-09'); -INSERT INTO product (name, category, price, date_added) VALUES('Strawberry', 'Fruit', 4.99, '2019-01-10'); -INSERT INTO product (name, category, price, date_added) VALUES('Tomato', 'Vegetable', 2.19, '2019-01-11'); -INSERT INTO product (name, category, price, date_added) VALUES('Pineapple', 'Fruit', 3.69, '2019-01-12'); -INSERT INTO product (name, category, price, date_added) VALUES('Spinach', 'Vegetable', 2.79, '2019-01-13'); -INSERT INTO product (name, category, price, date_added) VALUES('Blueberry', 'Fruit', 6.49, '2019-01-14'); -INSERT INTO product (name, category, price, date_added) VALUES('Bell Pepper', 'Vegetable', 1.99, '2019-01-15'); -INSERT INTO product (name, category, price, date_added) VALUES('Watermelon', 'Fruit', 7.99, '2019-01-16'); -INSERT INTO product (name, category, price, date_added) VALUES('Zucchini', 'Vegetable', 2.49, '2019-01-17'); -INSERT INTO product (name, category, price, date_added) VALUES('Mango', 'Fruit', 4.79, '2019-01-18'); -INSERT INTO product (name, category, price, date_added) VALUES('Asparagus', 'Vegetable', 3.29, '2019-01-19'); -INSERT INTO product (name, category, price, date_added) VALUES('Cherry', 'Fruit', 3.99, '2019-01-20'); -INSERT INTO product (name, category, price, date_added) VALUES('Potato', 'Vegetable', 1.49, '2019-01-21'); -INSERT INTO product (name, category, price, date_added) VALUES('Pear', 'Fruit', 2.59, '2019-01-22'); -INSERT INTO product (name, category, price, date_added) VALUES('Cantaloupe', 'Fruit', 3.99, '2019-01-23'); -INSERT INTO product (name, category, price, date_added) VALUES('Cauliflower', 'Vegetable', 2.19, '2019-01-24'); -INSERT INTO product (name, category, price, date_added) VALUES('Lemon', 'Fruit', 1.79, '2019-01-25'); -INSERT INTO product (name, category, price, date_added) VALUES('Cabbage', 'Vegetable', 1.69, '2019-01-26'); -INSERT INTO product (name, category, price, date_added) VALUES('Raspberry', 'Fruit', 5.29, '2019-01-27'); -INSERT INTO product (name, category, price, date_added) VALUES('Onion', 'Vegetable', 1.29, '2019-01-28'); -INSERT INTO product (name, category, price, date_added) VALUES('Kiwi', 'Fruit', 2.69, '2019-01-29'); -INSERT INTO product (name, category, price, date_added) VALUES('Green Bean', 'Vegetable', 2.39, '2019-01-30'); -INSERT INTO product (name, category, price, date_added) VALUES('Blackberry', 'Fruit', 4.49, '2019-01-31'); -INSERT INTO product (name, category, price, date_added) VALUES('Sweet Potato', 'Vegetable', 1.99, '2019-02-01'); -INSERT INTO product (name, category, price, date_added) VALUES('Peach', 'Fruit', 3.49, '2019-02-02'); -INSERT INTO product (name, category, price, date_added) VALUES('Celery', 'Vegetable', 1.89, '2019-02-03'); -INSERT INTO product (name, category, price, date_added) VALUES('Grapefruit', 'Fruit', 2.99, '2019-02-04'); -INSERT INTO product (name, category, price, date_added) VALUES('Radish', 'Vegetable', 1.29, '2019-02-05'); -INSERT INTO product (name, category, price, date_added) VALUES('Apricot', 'Fruit', 3.99, '2019-02-06'); -INSERT INTO product (name, category, price, date_added) VALUES('Brussels Sprout', 'Vegetable', 2.49, '2019-02-07'); -INSERT INTO product (name, category, price, date_added) VALUES('Artichoke', 'Vegetable', 3.99, '2019-02-09'); -INSERT INTO product (name, category, price, date_added) VALUES('Lime', 'Fruit', 1.49, '2019-02-10'); -INSERT INTO product (name, category, price, date_added) VALUES('Beet', 'Vegetable', 1.99, '2019-02-11'); -INSERT INTO product (name, category, price, date_added) VALUES('Plum', 'Fruit', 2.99, '2019-02-12'); -INSERT INTO product (name, category, price, date_added) VALUES('Corn', 'Vegetable', 1.49, '2019-02-13'); -INSERT INTO product (name, category, price, date_added) VALUES('Pomegranate', 'Fruit', 3.99, '2019-02-14'); -INSERT INTO product (name, category, price, date_added) VALUES('Garlic', 'Vegetable', 1.29, '2019-02-15'); -INSERT INTO product (name, category, price, date_added) VALUES('Papaya', 'Fruit', 4.99, '2019-02-16'); -INSERT INTO product (name, category, price, date_added) VALUES('Green Onion', 'Vegetable', 1.49, '2019-02-17'); -INSERT INTO product (name, category, price, date_added) VALUES('Ginger', 'Vegetable', 1.99, '2019-02-19'); -INSERT INTO product (name, category, price, date_added) VALUES('Parsley', 'Vegetable', 1.29, '2019-02-20'); -INSERT INTO product (name, category, price, date_added) VALUES('Parsnip', 'Vegetable', 1.99, '2019-02-21'); -INSERT INTO product (name, category, price, date_added) VALUES('Peas', 'Vegetable', 1.99, '2019-02-24'); diff --git a/articles/hilla/guides/data-grids/index.adoc b/articles/hilla/guides/data-grids/index.adoc deleted file mode 100644 index 142713b578..0000000000 --- a/articles/hilla/guides/data-grids/index.adoc +++ /dev/null @@ -1,211 +0,0 @@ ---- -title: Data Grids -page-title: How to work with data grids in Hilla | Vaadin -description: Quickly create data grids using the Auto Grid feature. -meta-description: Learn to create and manage advanced data grids in applications using Vaadin's Hilla for data visualization. -order: 65 ---- - - -= Data Grids - -Auto Grid makes it easy to show data from a database in a grid. It works by connecting a JPA repository to a grid component -- and with minimal effort. The grid shows columns for all properties of the entity, and loads data in a lazy-loaded manner. It can also provide sorting and filtering. - -This guide shows how to set up a JPA entity and repository in the backend, how to expose the repository with a service, and how to connect that service to an Auto Grid in the frontend. - -This guide assumes a Vaadin project with React views. See <<{articles}/getting-started#,Getting Started>> for creating a project, and <<{articles}/flow/react-views#enabling,Enabling React Views>> for the dependency that browser-callable services and React views need. - - -== Setting up the Backend - -To use Auto Grid, you need to have JPA and a database in your project. If you haven't done this already, open the following section on how to configure JPA and an in-memory H2 database. Otherwise, you can skip it. - -.Database Setup -[%collapsible] -==== -First, add the following dependencies to the `pom.xml` file: - -[source,xml] -.pom.xml ----- - - org.springframework.boot - spring-boot-starter-data-jpa - - - com.h2database - h2 - runtime - ----- - -Next, add the following properties to the `src/main/resources/application.properties` file: - -[source,properties] -.application.properties ----- -spring.datasource.url=jdbc:h2:mem:testdb -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password=password -spring.jpa.hibernate.ddl-auto=update ----- - -==== - - -First, create a JPA entity class in the backend. For this guide, you'll use a simple entity that represents a product. Create a [filename]`Product.java` file next to [filename]`Application.java` with the following content: - -[source,java] -.`Product.java` ----- -package com.example.application; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; - -import java.time.LocalDate; - -@Entity -public class Product { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - private String name; - private String category; - private double price; - private LocalDate dateAdded; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } - - public double getPrice() { - return price; - } - - public void setPrice(double price) { - this.price = price; - } - - public LocalDate getDateAdded() { - return dateAdded; - } - - public void setDateAdded(LocalDate dateAdded) { - this.dateAdded = dateAdded; - } -} ----- - -Next, create a Spring JPA repository for the entity. Create a [filename]`ProductRepository.java` file next to [filename]`Product.java` with the following content: - -[source,java] -.`ProductRepository.java` ----- -package com.example.application; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; - -public interface ProductRepository extends JpaRepository, JpaSpecificationExecutor { -} ----- - -Finally, create a service to expose the repository to the frontend. Create a [filename]`ProductService.java` file next to `ProductRepository.java` with the following contents: - -[source,java] -.`ProductService.java` ----- -package com.example.application; - -import com.vaadin.flow.server.auth.AnonymousAllowed; -import com.vaadin.hilla.BrowserCallable; -import com.vaadin.hilla.crud.ListRepositoryService; - -@BrowserCallable -@AnonymousAllowed -public class ProductService extends ListRepositoryService { -} ----- - -This service is annotated with `@BrowserCallable` to allow it to be called from the frontend. It extends `ListRepositoryService`, which provides a default implementation for fetching paginated, sorted, and filtered data. By default, all services exposed to the frontend are secured and require authentication. To keep things simple, the service has the `@AnonymousAllowed` annotation to allow unauthenticated access to the service. - -To have some example product data, download this SQL script and put it in the `src/main/resources` folder: - -++++ -

-Download product data -

-++++ - - -== Creating the View - -Now that the backend is set up, you can implement the frontend part. Start by running the application, using the default Maven goal: - -[source,terminal] ----- -mvn ----- - -This takes a while to install all necessary dependencies, but once it is done, you should see the application in your browser. - -Next, create a view that renders the product data. Create a [filename]`products.tsx` file in the [filename]`frontend/views` folder with the following content: - -[source,tsx] -.ProductsView.tsx ----- -import React from 'react'; -import { AutoGrid } from '@vaadin/hilla-react-crud'; -import { ProductService } from 'Frontend/generated/endpoints'; -import ProductModel from 'Frontend/generated/com/example/application/ProductModel'; - -export function ProductsView() { - return ( -
- -
- ); -} ----- - -This view renders an Auto Grid component, configured to load data from the `ProductService` and using the `ProductModel` to define the columns. The `ProductModel` is generated from the `Product` entity in the backend that you created earlier. - -Save the file and open the URL, `http://localhost:8080/products` in your browser. You should see a grid like the following, showing product data from the database: - -[source,typescript] ----- -include::{root}/frontend/demo/component/auto-grid/react/auto-grid-basic.tsx[render,tags=snippet,indent=0,group=React] ----- - - -== Further Information - -For more information about the Auto Grid feature, see the <> documentation. diff --git a/articles/hilla/guides/from-java-to-react.adoc b/articles/hilla/guides/from-java-to-react.adoc deleted file mode 100644 index 1ea6518bf2..0000000000 --- a/articles/hilla/guides/from-java-to-react.adoc +++ /dev/null @@ -1,384 +0,0 @@ ---- -title: React for Java Developers -page-title: React for Java Developers using Vaadin -description: How React differs from Java, and how it's used in Vaadin. -meta-description: Learn about how React differs from Java, and how it's used in Vaadin. -order: 120 ---- - -= React Introduction for Java Developers - -React is a JavaScript library for building user interfaces, mainly single-page applications that require efficient, dynamic updates. Developed by Facebook, it emphasizes a component-based architecture, allowing complex UIs to be constructed from small, reusable pieces. - -React is a declarative library. This means the code you write describes how the UI must change according to the application state. You don't have to handle the details of updating the interface when a value changes: React takes care of it. This is in contrast to imperative programming in which you describe the steps to take to achieve a desired UI state. - -Here's an example of imperative programming that shows how to update a text using a button in Java and Vaadin: - -[source,java] ----- -public class CounterView extends VerticalLayout { - private int counter = 0; - - public CounterView() { - var counterField = new TextField("Counter"); - counterField.setValue(String.valueOf(counter)); - counterField.setReadOnly(true); - - var button = new Button("Increment", e -> { - counterField.setValue(String.valueOf(++counter)); - }); - - add(counterField, button); - } -} ----- - -The idea is to store the value in a field. The value is incremented and the field is updated each time the button is pressed. Again, this is an imperative approach. A server call is also needed to update the counter and the UI each time the button is pressed. - -A similar interface can be implemented on the client, using JavaScript: - -[source,html] ----- - - - - - Counter Example - - -
- - -
- - - - - ----- - -While everything is done on the client, it works the same as the Java example: the value is stored in a variable and, each time the button is pressed, the value is incremented and the field is updated. - -By contrast, React uses a declarative approach. This means that the UI changes when the state changes. - -In React, you can write code as simple as this: - -.TSX -[source,jsx] ----- -export default function CounterView() { - const count = useSignal(0); - - return ( - <> -

Count: {count.value}

- - - ); -} ----- - -In this example, there's a `count` variable that's updated when the button is pressed. The UI is updated to reflect the new value. Not only is the code shorter, but React optimizes UI updates, improving performance significantly. - -While this code is short and looks simple, it introduces some concepts that aren't present in Java or plain JavaScript. They are illustrated in the following section. - - -== Anatomy of a React Component - -A React Component is a function that returns a fragment of the UI. The same way as you create a View in Vaadin, you can create a Component in React and use it as a view, or as a building block for a more complex view. The example above is a component used as a view, although the same code could be used as a component in another view. - -A component can contain some state, which can be implemented in many ways, the more recent being React Hooks. - - -=== Hooks - -Hooks is a feature in React that allows you to use state and other React features in function components. They are functions that let you hook into React state and lifecycle features, enabling you to manage state and side effects in your components. - -Side effects are operations such as data fetching, subscriptions, or changing manually the DOM that occur outside of the function scope and can affect other components or the state of the application. - -In the previous example, a hook called `useSignal` is used to create a state variable. The `count` variable is a signal that updates the UI when its `value` changes. Another commonly used hook is `useState`, which is part of the core React library. - -At Vaadin, signals are the default way to manage state in React components. Signals are similar to standard React Hooks, but they can easily share a state between components and are optimized for performance. - - -=== Signals - -Signals are a state management concept used in React programming. They are similar to standard React Hooks but with some key differences. A signal is a function that returns a single value, and this value can be observed by multiple components. When the value of a signal changes, all components observing that signal are rendered again with the new value. - -This makes signals a powerful tool for sharing a state between components and managing complex state interactions. They are optimized for performance, ensuring that components only re-render when necessary. - -The basic usage of signals is to deal with its `value` property. When you update it, all places where the same signal is used are updated. - - -=== React UI Updates - -The structure of an HTML page is called DOM (i.e., Document Object Model). When a change happens in an interactive page, the DOM is updated to reflect the new state. This process is quite expensive, as the browser needs to recalculate the layout and repaint the screen. - -React uses a virtual DOM to optimize this process. When a component updates its state, React creates a new virtual DOM and compares it with the previous one. It then calculates the minimum number of changes needed to update the real DOM and applies them. - - -== TypeScript - -All Vaadin's React-based tools are implemented using TypeScript, which is a superset of JavaScript that adds static typing. - -React components are written in a format called TSX, which is a mix of regular TypeScript and JSX, a syntax extension for JavaScript that allows you to write HTML-like code in your JavaScript files. The previous example is written in TSX and returns the code component, directly. - -In Vaadin, React components can access server-side services written in Java and, thanks to code generation, retain the types and methods of these services. - -To see how this works, replicate the original Java example, where the `counter` value is stored on the server. Create a Spring Service annotated with `@BrowserCallable` that allows you to interact with the server from the client like so: - -[source,java] ----- -@BrowserCallable -@AnonymousAllowed -public class CounterService { - private int counter; - - public int getCounter() { - return counter; - } - - public int increment() { - return ++counter; - } -} ----- - -When running the application, a TypeScript file is generated with functions that map public methods. It will look similar to this: - -[source,typescript] ----- -async function getCounter(): Promise { - // call `getCounter` on the server and return the result -} -async function increment(): Promise { - // call `increment` on the server and return the result -} ----- - -This way, you can call the server methods from the client, and the TypeScript compiler checks if the method exists and if the parameters are correct. - -Learn more about <<{articles}/flow/react-views/browser-callable-services#,browser-callable services>>. - -You can rewrite the React component to use the generated TypeScript functions: - -.TSX -[source,jsx] ----- -export default function CounterView() { - const count = useSignal(0); // <1> - - // Gets the initial value from the server - useEffect(() => { - CounterService.getCounter().then((value) => { - count.value = value; // <2> - }); - }, []); - - // calls the server to perform the increment and get the updated value - function increment() { - CounterService.increment().then((newValue) => { - count.value = newValue; - }); - } - - return ( - <> -

Count: {count}

- - - ); -} ----- -<1> This is a hook: a signal is created with an initial value of 0 and this initialization is done only once, even if the whole component function is executed every time the component is rendered. -<2> This is a side effect: a service is called to get the initial value from the server. This is encapsulated in a `useEffect` hook to make sure it is executed only once. - -While this view looks the same as before, it interacts with the server and preserves the value when reloading the page. This basic example shares the same counter between all connected clients. - -React views in Vaadin can use the same Web Components as in Java: change `button` to `Button` in the example above, import it and you'll get a Vaadin button. You can try using a `TextField` and a `VerticalLayout` to achieve the same result as in the Java example. - -`useEffect` is a standard React Hook that allows you to run side effects in your components. In this case, you'd use it to fetch the counter value from the server when the component is mounted. Calling the function directly would execute it every time the component is rendered. This would happen because React runs the component function each time it needs to render it. Hooks are a way to avoid running the same code more than necessary. - - -== References in Java and React - -In Java, passing references to objects is a fundamental concept. You can pass an object reference to methods or constructors, allowing direct manipulation of the object. - -[source,java] ----- -public class Example { - public void modifyObject(MyObject obj) { - obj.setValue("new value"); - } -} - -MyObject obj = new MyObject(); -Example example = new Example(); -example.modifyObject(obj); ----- - -In React, data is passed to components via properties. Properties are immutable within the child component. This means that you can't change the value of a property inside a component. If you need to change the value, you should pass a function that updates the value in the parent component. In Java, you might use methods and constructors to pass data into objects and retrieve data via getters, while React components receive data through properties and use callbacks to communicate with parent components. - -.TSX -[source,jsx] ----- -type ChildComponentProps = { - count: number; - increment: () => void; -}; - -function ChildComponent({ count, increment }: ChildComponentProps) { - return ( - <> -

Count: {count}

- - - ); -}; - -export default function ParentComponent() { - const count = useSignal(0); - - // a callback function passed to the child component - const increment = () => { - count.value++; - } - - return ; -}; ----- - - -== Hierarchy in Java and React - -In Java, interfaces define a contract that classes can implement, ensuring certain methods are present. - -[source,java] ----- -public interface MyInterface { - void performAction(); -} - -public class MyComponent implements MyInterface { - public void performAction() { - // Implementation - } -} ----- - -React doesn't support interfaces in the same way. Instead, it relies on the structure of properties and the functional nature of components to enforce contracts, implicitly. - -.TSX -[source,jsx] ----- -type ChildComponentProps = { - action: () => void; -}; - -function ChildComponent({ action }: ChildComponentProps) { - useEffect(() => { - action(); - }, [action]); - - return
Child component content
; -}; - -export default function ParentComponent() { - return console.log("Action performed")} />; -}; ----- - - -== Routing - -Vaadin uses the React Router, by default. This is the most commonly used router in React applications. By default, this router is configured manually, but Vaadin can generate the routes based on the filesystem structure. This way, you can create a new view by creating a new file in the `views` folder. - -The filesystem can be used to organize views logically, similar to packages in Java. The main difference is that the structure is exposed to users in the form of URLs. - - -=== Useful Routing Hooks - -Below are some useful routing hooks with explanations and examples of how to use each. - -==== `useParams` - -File Router supports parameters in URLs. You can define a parameter in the file name by creating a file or folder with the name enclosed in curly braces. For example, a file named [filename]`views/user/{userId}.tsx` or [filename]`views/user/{userId}/@index.tsx` matches the URL `/user/123`, and the `userId` parameter is available in the component props. - -The parameter is accessible using the `useParams` hook from the `react-router` package. - -.TSX -[source,jsx] ----- -import { useParams } from 'react-router'; - -export default function UserView() { - const { userId } = useParams<{ userId: string }>(); - - return

User ID: {userId}

; -} ----- - - -==== `useNavigate` - -The `useNavigate` hook is used to navigate programmatically. It returns a function that can be called with a string to navigate to a new location. - -.TSX -[source,jsx] ----- -import { useSignal } from "@vaadin/hilla-react-signals"; -import { Button, NumberField } from "@vaadin/react-components"; -import { useNavigate } from "react-router"; - -export default function GameView() { - const secretNumber = 42; - const navigate = useNavigate(); - const id = useSignal(''); - - function checkNumber() { - if (Number(id.value) === secretNumber) { - navigate('/win'); - } else { - navigate('/lose'); - } - } - - return <> - id.value = e.detail.value} /> - - ; -} ----- - - -==== `useLocation` - -The `useLocation` hook returns the current location object. You can use it to react to location changes. - -.TSX -[source,jsx] ----- -import { useLocation } from 'react-router'; - -export default function LocationView() { - const location = useLocation(); - - return

Current location: {location.pathname}

; -} ----- - -Learn more about <<{articles}/flow/react-views/routing#,the file-based router>>. - - -=== Conclusion - -To summarize, the main mindset change coming from Java to React is that you can't update the UI, manually. You update the state, and React updates the UI. While this is a great simplification, it requires giving up some habits that are common in imperative programming as is the case with Java. - -Using React instead of Java allows for better performance and flexibility, as you get access to the full power of JavaScript and the browser APIs. That power comes at the expense of losing the automatic server-side updates that Vaadin provides. diff --git a/articles/hilla/guides/index.adoc b/articles/hilla/guides/index.adoc deleted file mode 100644 index 8f895211b0..0000000000 --- a/articles/hilla/guides/index.adoc +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Guides -page-title: How to get started with Hilla | Vaadin -meta-description: Explore a range of guides to enhance your development experience with Vaadin's Hilla. -order: 20 ---- - -= Hilla Guides - -These guides cover what's specific to Hilla when building an application. Topics that aren't specific to Hilla are documented once for all of Vaadin: see the <<{articles}/upgrading#, Upgrading Guide>> for moving an application to a new Vaadin version, and <<{articles}/flow/production#, Deploying to Production>> for production builds and deployment. - -section_outline::[] diff --git a/articles/hilla/guides/testing.adoc b/articles/hilla/guides/testing.adoc deleted file mode 100644 index 4967e2b863..0000000000 --- a/articles/hilla/guides/testing.adoc +++ /dev/null @@ -1,298 +0,0 @@ ---- -title: Testing -page-title: How to test Hilla applications effectively | Vaadin -description: How to test views in Hilla applications. -meta-description: Discover strategies and tools for testing Hilla applications with confidence. -order: 105 ---- - - -= Testing - -This guide explains the basics of testing views in Hilla. It uses the https://vitest.dev/[Vitest] library for testing and setting up a configuration that runs tests in a browser to produce accurate results. It also uses the https://testing-library.com/docs/react-testing-library/intro/[React Testing Library] to render and interact with React components during tests. - - -== Setting Up a Test Environment - -Before starting to test, make sure your Vaadin project is initialized, and the default Vite configuration exists. You can do this by starting the application once by running the default Maven goal: - -[source,terminal] ----- -mvn ----- - -Now, install the necessary testing libraries like so: - -[source,terminal] ----- -npm install -D vitest @vitest/browser webdriverio pretty-format @testing-library/react @testing-library/user-event ----- - -Next, create a test configuration file named, [filename]`vitest.config.ts` in the project root with the following content: - -[source,typescript] ----- -import type { UserConfigFn } from 'vite'; -import { overrideVaadinConfig } from './vite.generated'; - -const customConfig: UserConfigFn = (env) => ({ - plugins: [], - test: { - include: ['./tests/**/*.{test,spec}.ts?(x)'], - globals: true, - browser: { - enabled: true, - name: 'chrome', - } - }, -}); - -export default overrideVaadinConfig(customConfig); ----- - -Finally, add an `npm` script to [filename]`package.json` to run the tests and open the Vitest UI: - -[source,json] ----- -"scripts": { - "test": "vitest" -} ----- - -You can now run the tests by executing the following from the command-line: - -[source,terminal] ----- -npm test ----- - -At this point, this command fails because there aren't any tests, yet. That's covered in the next section. - - -== Writing Tests - -As an example, to see how to test views in Hilla, use a simple Todo view. To start, create a basic view in [filename]`frontend/views/TodoView.tsx` like this: - -[source,tsx] ----- -export function TodoView() { - return ( -
-

My Todos

-
- ); -} ----- - -Now, create a basic test for this view in a file named, [filename]`frontend/tests/TodoView.test.tsx` and copy the following into it: - -[source,tsx] ----- -import { describe, it, expect } from 'vitest'; -import { TodoView } from 'Frontend/views/TodoView'; -import { render, screen } from '@testing-library/react'; - -describe('TodoView', () => { - it('should render', () => { - render(); - expect(screen.getByText('My Todos')).to.exist; - }); -}); ----- - -This test file contains a single test that renders the view and verifies that the `My Todos` text is present in the DOM. - -Start the test by executing the following again from the command-line: - -[source,terminal] ----- -npm test ----- - -This opens a browser window and displays the Vitest UI, which can be used for running and monitoring tests. It should run the newly added test and report that it passed. - - -== Testing User Interaction - -Now you can explore how to add a feature to the view and test it with user interactions. To do this, add an input field and a button to enter new tasks to do, along with a list to render them. Here's the updated `TodoView` component: - -[source,tsx] ----- -import { Button } from '@vaadin/react-components/Button.js'; -import { - TextField, - TextFieldValueChangedEvent, -} from '@vaadin/react-components/TextField.js'; -import { useSignal } from '@vaadin/hilla-react-signals'; - -export function TodoView() { - const newTodo = useSignal(''); - const todos = useSignal>([]); - - return ( -
-

My Todos

-
- - newTodo.value = e.detail.value - } - /> - -
-
    - {todos.value.map((todo, idx) => ( -
  • {todo}
  • - ))} -
-
- ); -} ----- - -Next, add a test that interacts with the view. For testing interactions, import `userEvent` from `@testing-library/user-event` like so: - -[source,tsx] ----- -import { userEvent } from '@testing-library/user-event'; - -describe('TodoView', () => { - ... - - it('should add a todo', async () => { - render(); - - // Change the value of the text field - const textField = screen.getByLabelText('New todo'); - await userEvent.click(textField); - await userEvent.type(textField, 'Read testing guide'); - - // Click the add todo button - const button = screen.getByText('Add todo'); - await userEvent.click(button); - - // Rerender and check that the todo is shown - expect(screen.getByText('Read testing guide')).to.exist; - }); -}); ----- - -This new test first finds the text field and changes its value using `userEvent.type`. Next, it finds the button and clicks it using `userEvent.click`. Finally, it verifies that the new todo is rendered in the list. - -Save the test so Vitest can run it in the browser, verifying that the interaction works as expected. - - -== Testing Backend Calls - -Views often need to interact with backend services, which should be tested, as well. In this section, you'll create a backend service to store todos and then verify that it's called correctly from the view. First, create a dummy service called, [filename]`TodoService.java` next to [filename]`Application.java`: - -[source,java] ----- -package com.example.application; - -import com.vaadin.flow.server.auth.AnonymousAllowed; -import com.vaadin.hilla.BrowserCallable; - -@BrowserCallable -@AnonymousAllowed -public class TodoService { - public void addTodo(String todo) { - System.out.println("addTodo: " + todo); - } -} ----- - -The service has a single method, `addTodo` that only prints the given todo to the console. - -From the CLI, run the following command to generate the TypeScript client for the service: - -[source,terminal] ----- -mvn clean compile hilla:generate ----- - -Now, update the click handler of the button in `TodoView` to call the service: - -[source,tsx] ----- -/* Add new import for generated service client */ -import { TodoService } from 'Frontend/generated/endpoints'; - -... - - ----- - -Next, add a test to verify that the service is called correctly. Set up a test environment that stubs the service to prevent actual backend calls and allows you to monitor calls: - -[source,tsx] ----- -/* Update imports from vitest */ -import { afterEach, beforeEach, describe, expect, it, type SpyInstance, vi } from 'vitest'; - -/* Add new import for generated service client */ -import { TodoService } from 'Frontend/generated/endpoints'; - -describe('TodoView', () => { - /* Add test setup and teardown */ - let addTodoSpy: SpyInstance; - - beforeEach(() => { - addTodoSpy = vi.spyOn(TodoService, 'addTodo'); - addTodoSpy.mockReturnValue(Promise.resolve()); - }); - - afterEach(() => { - addTodoSpy.mockRestore(); - }); - - ... - - it('should call service when adding todo', async () => { - render(); - - const textField = screen.getByLabelText('New todo'); - await userEvent.click(textField); - await userEvent.type(textField, 'Read testing guide'); - - const button = screen.getByText('Add todo'); - await userEvent.click(button); - - expect(addTodoSpy).toHaveBeenCalledWith('Read testing guide'); - }); -}); - ----- - -In the `beforeEach` hook, this test first creates a spy for the `addTodo` method of the service. Then, this stubs the spy to return a resolved promise to prevent the actual backend call. Finally, it restores the original method in the `afterEach` hook to prevent the stub from leaking to other tests. - -The actual test does the same user interaction as before, but this time it's verifying that the service is called with the correct argument. - -Save the test so Vitest can run it in the browser, verifying that the service is called correctly. - - -== Further Information - -For more information about Vitest and React Testing Library, refer to the official documentation: - -- https://vitest.dev/guide/[Vitest Documentation] -- https://vitest.dev/api/[Vitest API Documentation] -- https://testing-library.com/docs/react-testing-library/intro/[React Testing Library Documentation] -- https://testing-library.com/docs/react-testing-library/api/[React Testing Library API Documentation] diff --git a/articles/hilla/guides/uploads-downloads.adoc b/articles/hilla/guides/uploads-downloads.adoc deleted file mode 100644 index 21cbaca6c8..0000000000 --- a/articles/hilla/guides/uploads-downloads.adoc +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Uploading pass:[&] Downloading -page-title: How to implement file uploading and downloading in Hilla -description: How to implement file uploading and downloading in Hilla React applications. -meta-description: Learn how to implement file uploading and downloading in Vaadin Hilla. -order: 73 ---- - - -= Uploading & Downloading Files - -Transferring files between the client and server is a common requirement in web applications. This page demonstrates how to implement file uploading and downloading in Hilla React applications. - - -== Uploading Files - -Hilla supports the https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/multipart/MultipartFile.html[MultipartFile] class from Spring to handle file uploads. It maps to the standard https://developer.mozilla.org/en-US/docs/Web/API/File[File] interface in the generated TypeScript code. - -The following example demonstrates how to implement such a service and connect it to the <> component in a view: - -[.example] --- -[source,tsx] -.`upload.tsx` ----- -include::{root}/frontend/demo/fusion/forms/upload/upload.tsx[tags=snippet,indent=0] ----- -[source,java] -.`UploadService.java` ----- -include::{root}/src/main/java/com/vaadin/demo/fusion/upload/UploadService.java[tags=snippet,indent=0] ----- --- - - -== Downloading Files - -Browser-callable services only respond to `POST` requests and don't support file downloads. Since Hilla applications use Spring, though, you can leverage Spring's capabilities to implement file downloading. - -Select the server mapping that best suits your application. Then create an ad-hoc Spring controller to handle file downloads. - -Next, configure security using the usual Spring Security configuration semantics. - -In the following example, the view downloads a file and passes an ID as a parameter. The controller generates the file and sends it to the client. The security is configured so that only authenticated users can download files. - -[.example] --- -[source,tsx] -.`download.tsx` ----- -include::{root}/frontend/demo/fusion/forms/upload/download.tsx[tags=snippet,indent=0] ----- -[source,java] -.`FileDownloadController.java` ----- -include::{root}/src/main/java/com/vaadin/demo/fusion/download/FileDownloadController.java[tags=snippet,indent=0] ----- -[source,java] -.`SecurityConfig.java` ----- -include::{root}/src/main/java/com/vaadin/demo/SecurityConfig.java[tags=download,indent=0] ----- --- diff --git a/dspublisher/config/default.json b/dspublisher/config/default.json index fce0487a43..5113ddc689 100644 --- a/dspublisher/config/default.json +++ b/dspublisher/config/default.json @@ -40,8 +40,12 @@ "/flow/testing/browserless/migration": "/building-apps/testing/browserless/migrate-ui-unit-tests", "/flow/testing/browserless/non-spring": "/building-apps/testing/browserless/setup-without-spring", "/flow/testing/browserless/optimizing-tests": "/building-apps/testing/browserless/speed-up-tests", + "/hilla/faq": "/building-apps/react", + "/hilla/guides": "/building-apps/react", "/hilla/guides/browser-callable-services": "/flow/react-views/browser-callable-services", + "/hilla/guides/client-caching": "/building-apps/react/work-offline", "/hilla/guides/client-middleware": "/flow/react-views/browser-callable-services/client#middleware", + "/hilla/guides/data-grids": "/components/auto-grid", "/hilla/guides/endpoints": "/flow/react-views/browser-callable-services", "/hilla/guides/event-handling": "/flow/react-views/component-events", "/hilla/guides/flow-component-in-hilla": "/flow/react-views/flow-interop#flow-components", @@ -51,10 +55,11 @@ "/hilla/guides/forms/binder-load": "/flow/react-views/forms", "/hilla/guides/forms/binder-validation": "/flow/react-views/forms/validation", "/hilla/guides/forms/binding-arrays": "/flow/react-views/forms/nested-objects-arrays", - "/hilla/guides/forms/form-images": "/hilla/guides/uploads-downloads", + "/hilla/guides/forms/form-images": "/building-apps/react/upload-download", "/hilla/guides/forms/form-status-tracking": "/flow/react-views/forms/form-state", "/hilla/guides/forms/reference": "/flow/react-views/forms/api", "/hilla/guides/forms/vaadin-components": "/flow/react-views/forms/field-components", + "/hilla/guides/from-java-to-react": "/building-apps/react", "/hilla/guides/full-stack-signals": "/flow/ui-state/full-stack-signals", "/hilla/guides/i18n": "/flow/i18n/react-views", "/hilla/guides/production": "/flow/production", @@ -82,18 +87,20 @@ "/hilla/guides/security/spring-stateless": "/flow/security/react-views/stateless-authentication", "/hilla/guides/security/vulnerabilities": "/flow/security/protections/vulnerabilities", "/hilla/guides/styling": "/styling/stylesheets#react-views", + "/hilla/guides/testing": "/building-apps/react/test-views", "/hilla/guides/upgrading": "/upgrading", + "/hilla/guides/uploads-downloads": "/building-apps/react/upload-download", "/hilla/lit": "/upgrading#vaadin-router-removal", "/hilla/lit/components": "/components", "/hilla/lit/components/3rd-party": "/upgrading#vaadin-router-removal", "/hilla/lit/components/create": "/upgrading#vaadin-router-removal", "/hilla/lit/components/type-definitions": "/upgrading#vaadin-router-removal", "/hilla/lit/components/using-vaadin-components": "/components", - "/hilla/lit/faq": "/hilla/faq", - "/hilla/lit/guides": "/hilla/guides", + "/hilla/lit/faq": "/building-apps/react", + "/hilla/lit/guides": "/building-apps/react", "/hilla/lit/guides/architecture": "/building-apps/react", "/hilla/lit/guides/browser-callable-services": "/flow/react-views/browser-callable-services", - "/hilla/lit/guides/client-caching": "/hilla/guides/client-caching", + "/hilla/lit/guides/client-caching": "/building-apps/react/work-offline", "/hilla/lit/guides/client-middleware": "/flow/react-views/browser-callable-services/client#middleware", "/hilla/lit/guides/endpoints": "/flow/react-views/browser-callable-services", "/hilla/lit/guides/event-handling": "/flow/react-views/component-events", @@ -102,7 +109,7 @@ "/hilla/lit/guides/forms/binder-load": "/flow/react-views/forms", "/hilla/lit/guides/forms/binder-validation": "/flow/react-views/forms/validation", "/hilla/lit/guides/forms/binding-arrays": "/flow/react-views/forms/nested-objects-arrays", - "/hilla/lit/guides/forms/form-images": "/hilla/guides/uploads-downloads", + "/hilla/lit/guides/forms/form-images": "/building-apps/react/upload-download", "/hilla/lit/guides/forms/form-status-tracking": "/flow/react-views/forms/form-state", "/hilla/lit/guides/forms/reference": "/flow/react-views/forms/api", "/hilla/lit/guides/forms/vaadin-components": "/flow/react-views/forms/field-components", @@ -145,7 +152,7 @@ "/hilla/lit/reference/typescript-client": "/flow/react-views/browser-callable-services/client", "/hilla/lit/start": "/getting-started", "/hilla/lit/start/basics": "/getting-started/tutorial", - "/hilla/lit/start/faq": "/hilla/faq", + "/hilla/lit/start/faq": "/building-apps/react", "/hilla/lit/start/quick": "/getting-started/quick-start", "/hilla/reference": "/flow/react-views", "/hilla/reference/configuration": "/flow/react-views/browser-callable-services/code-generation", diff --git a/frontend/demo/fusion/files/files-view.tsx b/frontend/demo/fusion/files/files-view.tsx new file mode 100644 index 0000000000..f6d3466bae --- /dev/null +++ b/frontend/demo/fusion/files/files-view.tsx @@ -0,0 +1,46 @@ +import { useSignal } from '@vaadin/hilla-react-signals'; +import { Upload } from '@vaadin/react-components'; + +// tag::csrf[] +// Adds the CSRF token that Spring Security expects +function addCsrfHeader(xhr: XMLHttpRequest) { + const header = document.querySelector('meta[name="_csrf_header"]')?.content; + const token = document.querySelector('meta[name="_csrf"]')?.content; + if (header && token) { + xhr.setRequestHeader(header, token); + } +} +// end::csrf[] + +type StoredFile = { id: string; name: string }; + +export default function FilesView() { + const files = useSignal([]); + + return ( + <> + {/* tag::upload[] */} + addCsrfHeader(event.detail.xhr)} + onUploadSuccess={(event) => { + const { file, xhr } = event.detail; + files.value = [...files.value, { id: xhr.responseText, name: file.name }]; + }} + /> + {/* end::upload[] */} + {/* tag::download[] */} + + {/* end::download[] */} + + ); +} diff --git a/frontend/demo/fusion/forms/upload/download.tsx b/frontend/demo/fusion/forms/upload/download.tsx deleted file mode 100644 index cbde1be90a..0000000000 --- a/frontend/demo/fusion/forms/upload/download.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useSignal } from '@vaadin/hilla-react-signals'; -import { TextField } from '@vaadin/react-components'; - -export default function DownloadView() { - // tag::snippet[] - const id = useSignal(''); - - return ( -
- { - id.value = e.detail.value; - }} - /> - {id.value && ( - - Download - - )} -
- ); - // end::snippet[] -} diff --git a/frontend/demo/fusion/forms/upload/upload.tsx b/frontend/demo/fusion/forms/upload/upload.tsx deleted file mode 100644 index 78a108fdc0..0000000000 --- a/frontend/demo/fusion/forms/upload/upload.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Notification, Upload, type UploadRequestEvent } from '@vaadin/react-components'; -import { UploadService } from 'Frontend/generated/endpoints'; - -export default function UploadView() { - // tag::snippet[] - const handleUploadRequest = async (e: UploadRequestEvent) => { - e.preventDefault(); - - const uploadRef = e.target; - - const fileId = await UploadService.uploadFile(e.detail.file); - uploadRef.files = uploadRef.files.map((file) => { - file.status = ''; - file.complete = true; - return file; - }); - - Notification.show(`Received file: ${fileId}`); - }; - - return ; - // end::snippet[] -} diff --git a/frontend/demo/fusion/offline/cacheable.ts b/frontend/demo/fusion/offline/cacheable.ts new file mode 100644 index 0000000000..fc59a257e3 --- /dev/null +++ b/frontend/demo/fusion/offline/cacheable.ts @@ -0,0 +1,36 @@ +// tag::snippet[] +import { EndpointError } from '@vaadin/hilla-frontend'; + +const CACHE_KEY = 'offline-cache'; + +function readCache(): Record { + return JSON.parse(localStorage.getItem(CACHE_KEY) ?? '{}') as Record; +} + +/** + * Calls a service and stores the result in the browser. When the server + * can't be reached, returns the stored result, or the default value. + */ +export async function cacheable( + key: string, + call: () => Promise, + defaultValue: T +): Promise { + try { + const result = await call(); + localStorage.setItem(CACHE_KEY, JSON.stringify({ ...readCache(), [key]: result })); + return result; + } catch (error) { + if (error instanceof EndpointError) { + // The server answered with an error, such as access denied + throw error; + } + const cached = readCache()[key]; + return cached === undefined ? defaultValue : (cached as T); + } +} + +export function clearCache(): void { + localStorage.removeItem(CACHE_KEY); +} +// end::snippet[] diff --git a/frontend/demo/fusion/offline/tasks-view.tsx b/frontend/demo/fusion/offline/tasks-view.tsx new file mode 100644 index 0000000000..7e22e3e8f8 --- /dev/null +++ b/frontend/demo/fusion/offline/tasks-view.tsx @@ -0,0 +1,30 @@ +import { useEffect } from 'react'; +import { useSignal } from '@vaadin/hilla-react-signals'; +import type Task from 'Frontend/generated/com/vaadin/demo/fusion/offline/TaskService/Task'; +import { TaskService } from 'Frontend/generated/endpoints'; +import { cacheable } from './cacheable'; + +// tag::snippet[] +export default function TasksView() { + const tasks = useSignal([]); + + useEffect(() => { + cacheable('tasks', TaskService.findTasks, []).then( + (result) => { + tasks.value = result; + }, + (error: unknown) => { + console.error('Loading the tasks failed', error); + } + ); + }, []); + + return ( +
    + {tasks.value.map((task) => ( +
  • {task.title}
  • + ))} +
+ ); +} +// end::snippet[] diff --git a/frontend/demo/fusion/testing/todo-view.tsx b/frontend/demo/fusion/testing/todo-view.tsx new file mode 100644 index 0000000000..4c63644f9c --- /dev/null +++ b/frontend/demo/fusion/testing/todo-view.tsx @@ -0,0 +1,35 @@ +import { useSignal } from '@vaadin/hilla-react-signals'; +import { Button, TextField } from '@vaadin/react-components'; +import { TodoService } from 'Frontend/generated/endpoints'; + +// tag::snippet[] +export default function TodoView() { + const todos = useSignal([]); + const newTodo = useSignal(''); + + async function addTodo() { + await TodoService.addTodo(newTodo.value); + todos.value = [...todos.value, newTodo.value]; + newTodo.value = ''; + } + + return ( +
+

Todos

+ { + newTodo.value = event.detail.value; + }} + /> + +
    + {todos.value.map((todo) => ( +
  • {todo}
  • + ))} +
+
+ ); +} +// end::snippet[] diff --git a/src/main/java/com/vaadin/demo/SecurityConfig.java b/src/main/java/com/vaadin/demo/SecurityConfig.java index 3bc9b6651e..bee6fb4bbc 100644 --- a/src/main/java/com/vaadin/demo/SecurityConfig.java +++ b/src/main/java/com/vaadin/demo/SecurityConfig.java @@ -15,15 +15,5 @@ public class SecurityConfig { SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.csrf(AbstractHttpConfigurer::disable).build(); - - // @formatter:off hidden-source-line - /* Disable on docs app, but leave in place to be used as snippet // hidden-source-line - // tag::download[] - // Restrict access to FileDownloadController to authenticated users - http.authorizeHttpRequests(authorize -> authorize - .requestMatchers("/download/**").authenticated()); - // end::download[] - */ // hidden-source-line - // @formatter:on hidden-source-line } } diff --git a/src/main/java/com/vaadin/demo/fusion/download/FileDownloadController.java b/src/main/java/com/vaadin/demo/fusion/download/FileDownloadController.java deleted file mode 100644 index 099c446f6a..0000000000 --- a/src/main/java/com/vaadin/demo/fusion/download/FileDownloadController.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.vaadin.demo.fusion.download; - -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class FileDownloadController { - - // tag::snippet[] - @RequestMapping(value = "/download/{id}", method = RequestMethod.GET) - public ResponseEntity downloadFile( - @PathVariable("id") String id) { - String content = "File content for ID: " + id; - Resource file = new ByteArrayResource(content.getBytes()); - - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=\"file_" + id + ".txt\"") - .body(file); - } - // end::snippet[] -} diff --git a/src/main/java/com/vaadin/demo/fusion/files/FileController.java b/src/main/java/com/vaadin/demo/fusion/files/FileController.java new file mode 100644 index 0000000000..1f234199c2 --- /dev/null +++ b/src/main/java/com/vaadin/demo/fusion/files/FileController.java @@ -0,0 +1,63 @@ +package com.vaadin.demo.fusion.files; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +// tag::snippet[] +@Profile("this-is-just-a-demo-class") // hidden-source-line +@RestController +@RequestMapping("/api/files") +public class FileController { + + record StoredFile(String name, MediaType type, byte[] content) { + } + + // Keeps the files in memory. Use a database or a file store instead. + private final Map files = new ConcurrentHashMap<>(); + + // tag::upload[] + @PostMapping + public String upload(@RequestParam("file") MultipartFile file) + throws IOException { + var type = file.getContentType() == null + ? MediaType.APPLICATION_OCTET_STREAM + : MediaType.parseMediaType(file.getContentType()); + var id = UUID.randomUUID().toString(); + files.put(id, new StoredFile(file.getOriginalFilename(), type, + file.getBytes())); + return id; + } + // end::upload[] + + // tag::download[] + @GetMapping("/{id}") + public ResponseEntity download(@PathVariable String id) { + var file = files.get(id); + if (file == null) { + return ResponseEntity.notFound().build(); + } + var disposition = ContentDisposition.attachment() + .filename(file.name(), StandardCharsets.UTF_8).build(); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString()) + .contentType(file.type()).body(file.content()); + } + // end::download[] +} +// end::snippet[] diff --git a/src/main/java/com/vaadin/demo/fusion/offline/TaskService.java b/src/main/java/com/vaadin/demo/fusion/offline/TaskService.java new file mode 100644 index 0000000000..9412905018 --- /dev/null +++ b/src/main/java/com/vaadin/demo/fusion/offline/TaskService.java @@ -0,0 +1,19 @@ +package com.vaadin.demo.fusion.offline; + +import java.util.List; + +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.hilla.BrowserCallable; + +@BrowserCallable +@AnonymousAllowed +public class TaskService { + + public record Task(String title, boolean done) { + } + + public List findTasks() { + return List.of(new Task("Write the report", false), + new Task("Book a meeting room", true)); + } +} diff --git a/src/main/java/com/vaadin/demo/fusion/offline/package-info.java b/src/main/java/com/vaadin/demo/fusion/offline/package-info.java new file mode 100644 index 0000000000..2e407664d2 --- /dev/null +++ b/src/main/java/com/vaadin/demo/fusion/offline/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package com.vaadin.demo.fusion.offline; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/com/vaadin/demo/fusion/testing/TodoService.java b/src/main/java/com/vaadin/demo/fusion/testing/TodoService.java new file mode 100644 index 0000000000..f02455692d --- /dev/null +++ b/src/main/java/com/vaadin/demo/fusion/testing/TodoService.java @@ -0,0 +1,15 @@ +package com.vaadin.demo.fusion.testing; + +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.hilla.BrowserCallable; + +// tag::snippet[] +@BrowserCallable +@AnonymousAllowed +public class TodoService { + + public void addTodo(String text) { + // Store the todo, for example in a database + } +} +// end::snippet[] diff --git a/src/main/java/com/vaadin/demo/fusion/upload/UploadService.java b/src/main/java/com/vaadin/demo/fusion/upload/UploadService.java deleted file mode 100644 index 1e7b4f21e9..0000000000 --- a/src/main/java/com/vaadin/demo/fusion/upload/UploadService.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.vaadin.demo.fusion.upload; - -import java.util.UUID; - -import org.jspecify.annotations.NonNull; -import org.springframework.web.multipart.MultipartFile; - -import com.vaadin.flow.server.auth.AnonymousAllowed; -import com.vaadin.hilla.BrowserCallable; - -@BrowserCallable -@AnonymousAllowed -public class UploadService { - - /** - * Simulates uploading a file. - * - * @param file - * the uploaded file - * @return an identifier for the uploaded file - */ - // tag::snippet[] - @NonNull - public String uploadFile(@NonNull MultipartFile file) { - // handle the file as needed - return file.getOriginalFilename() + '_' + file.getSize() + '_' - + UUID.randomUUID(); - } - // end::snippet[] -} From 05dce3fe7bd18b616c49b44df00507d0260d1da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Wed, 11 Nov 2026 19:45:34 +0200 Subject: [PATCH 2/2] docs: show the whole files view in the upload guide The upload and download snippets used a signal that the page didn't show. Show the whole view, including the signal and its type, and keep the download snippet as a focused excerpt. Also drop the sentence about context paths, which wasn't tested. Co-Authored-By: Claude Opus 5.5 --- articles/building-apps/react/upload-download.adoc | 9 +++++---- frontend/demo/fusion/files/files-view.tsx | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/articles/building-apps/react/upload-download.adoc b/articles/building-apps/react/upload-download.adoc index 390e9a2f43..c68b3c1e9e 100644 --- a/articles/building-apps/react/upload-download.adoc +++ b/articles/building-apps/react/upload-download.adoc @@ -50,11 +50,12 @@ For the other options, see <<{articles}/flow/security/spring/security-configurat == Uploading Files -Point the `target` of the Upload component to the controller, and send the files as multipart form data. By default, the component sends the raw content of the file, which a [classname]`MultipartFile` parameter doesn't accept: +Point the `target` of the Upload component to the controller, and send the files as multipart form data. By default, the component sends the raw content of the file, which a [classname]`MultipartFile` parameter doesn't accept. The following view uploads files, and lists the uploaded files with links for downloading them: [source,tsx] +.[filename]`views/files.tsx` ---- -include::{root}/frontend/demo/fusion/files/files-view.tsx[tags=upload,indent=0] +include::{root}/frontend/demo/fusion/files/files-view.tsx[tags=view,indent=0] ---- Spring Security rejects a `POST` request without a CSRF token. Vaadin adds the token to the page in `` tags, and the `onUploadRequest` handler copies it to each upload request: @@ -66,7 +67,7 @@ include::{root}/frontend/demo/fusion/files/files-view.tsx[tags=csrf,indent=0] The handler reads the token for each request, because the token changes when the user logs in. -When the upload succeeds, `xhr.responseText` holds the response of the controller, here the ID of the file. A relative `target`, without a leading slash, also works when the application has a context path. +When the upload succeeds, `xhr.responseText` holds the response of the controller, here the ID of the file. The view keeps the ID and the name of each uploaded file in a signal. === Limiting the File Size @@ -84,7 +85,7 @@ To reject a large file before it's uploaded, also set the `maxFileSize` property == Downloading Files -To let users download a file, link to the download URL of the controller, and add the `download` attribute to the link: +To let users download a file, link to the download URL of the controller, and add the `download` attribute to the link. The view above does this for each uploaded file: [source,tsx] ---- diff --git a/frontend/demo/fusion/files/files-view.tsx b/frontend/demo/fusion/files/files-view.tsx index f6d3466bae..fd24e18ee2 100644 --- a/frontend/demo/fusion/files/files-view.tsx +++ b/frontend/demo/fusion/files/files-view.tsx @@ -12,6 +12,7 @@ function addCsrfHeader(xhr: XMLHttpRequest) { } // end::csrf[] +// tag::view[] type StoredFile = { id: string; name: string }; export default function FilesView() { @@ -19,7 +20,6 @@ export default function FilesView() { return ( <> - {/* tag::upload[] */} - {/* end::upload[] */} {/* tag::download[] */}
    {files.value.map((file) => ( @@ -44,3 +43,4 @@ export default function FilesView() { ); } +// end::view[]