diff --git a/articles/building-apps/react/add-login.adoc b/articles/building-apps/react/add-login.adoc new file mode 100644 index 0000000000..c28e8486bb --- /dev/null +++ b/articles/building-apps/react/add-login.adoc @@ -0,0 +1,184 @@ +--- +title: Hilla +page-title: How to add a Hilla login view to a Vaadin application +meta-description: Learn how to create a secure login view using Hilla and React in Vaadin. This guide covers Spring Security integration, authentication setup, and best practices. +order: 10 +--- + + += Hilla Login +:toclevels: 2 + +In this guide, you'll learn how to create a login view using Hilla and React. + +.Never Use Hard-Coded Credentials In Production +[WARNING] +In-memory authentication is convenient for development, but production applications must use a more secure approach, such as *JDBC authentication*, *LDAP authentication*, or *OAuth 2.0*. Refer to the https://docs.spring.io/spring-security/reference/servlet/authentication/index.html[Spring Security Reference Manual] for more details. + + +== Client Configuration + +Vaadin includes a client-side security extension that integrates with Spring Security on the server. To configure it, follow these steps: + +1. Create a *user information endpoint* on the server. +2. Set up a *React context* in the frontend. +3. Enable the context in the *`App` component*. + + +=== Create a User Information Endpoint + +Since authentication happens on the server, you need a way to pass user information to the client. To achieve this, create a [annotationname]`@BrowserCallable` service that returns user details: + +// This assumes that the concept of browser callable services has been explained earlier. + +[source,java] +---- +@BrowserCallable +public class UserInfoService { + + @PermitAll // <1> + public @NonNull UserInfo getUserInfo() { + var auth = SecurityContextHolder.getContext().getAuthentication(); // <2> + var authorities = auth.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .toList(); + return new UserInfo(auth.getName(), authorities); // <3> + } +} +---- +<1> Allows *all authenticated users* to access [methodname]`getUserInfo()`. +<2> Retrieves user details from Spring Security. +<3> Returns user information to the client. + +Vaadin does not provide a built-in user information type, so you need to define your own: + +[source,java] +---- +public record UserInfo( + @NonNull String name, + @NonNull Collection authorities +) { +} +---- + +You can include *additional user details* (e.g., email, avatar) in this type and access them in React. + + +=== Setup the Authentication Context + +Now, configure a *React context* to store authentication details, using Vaadin's helper `configureAuth()`: + +.frontend/security/auth.ts +[source,typescript] +---- +import { configureAuth } from '@vaadin/hilla-react-auth'; +import { UserInfoService } from "Frontend/generated/endpoints"; + +const auth = configureAuth(UserInfoService.getUserInfo) // <1> +export const useAuth = auth.useAuth // <2> +export const AuthProvider = auth.AuthProvider // <3> +---- +<1> Uses `UserInfoService` to fetch user details from the server. +<2> `useAuth` is a *React hook* that provides authentication state within views. +<3> `AuthProvider` is a *React context provider*. + + +=== Enable the Authentication Context + +Next, wrap the root component with ``. First, *move `index.tsx` from `src/main/frontend/generated` to `src/main/frontend`*. Then, modify it as follows: + +.frontend/index.tsx +[source,tsx] +---- +import { createElement } from 'react'; +import { createRoot } from 'react-dom/client'; +import { RouterProvider } from 'react-router'; +import { router } from 'Frontend/generated/routes.js'; +// tag::snippet[] +import { AuthProvider } from "Frontend/security/auth"; +// end::snippet[] + +function App() { + return ( +// tag::snippet[] + +{/* end::snippet[] */} + +{/* tag::snippet[] */} + +// end::snippet[] + ); +} +... +---- + + +== The Login View + +The login view is a standard <<../../views/add-view/hilla#,Hilla view>>. The easiest way to implement one is by using the <<{articles}/components/login#,LoginForm>> component: + +.frontend/views/login.tsx +[source,tsx] +---- +import { LoginForm } from "@vaadin/react-components"; +import { ViewConfig } from "@vaadin/hilla-file-router/types.js"; +import { useSearchParams } from "react-router"; + +export const config: ViewConfig = { + skipLayouts: true, // <1> + menu: { + exclude: true // <2> + } +} + +export default function LoginView() { + const [searchParams] = useSearchParams() + const hasError = searchParams.has("error"); // <3> + + return ( +
+ {/* <4> */} +
+ ) +} +---- +<1> Disables auto layout to prevent the login view from being embedded in a <<../../views/add-router-layout#,router layout>>. +<2> Excludes the login view from the navigation menu. +<3> Detects if the `?error` query parameter is present. +<4> Instructs the login form to send a `POST` request to `/login` for authentication. + +Spring Security's *form login* mechanism automatically processes authentication requests sent to `/login`. When authentication fails, the user is redirected back to the login page with `?error`, which the login view handles. + + +== Server Configuration + +To instruct Spring Security to use your login view, modify your security configuration: + +.`SecurityConfig.java` +[source,java] +---- +@EnableWebSecurity +@Configuration +class SecurityConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // Configure Vaadin's security using VaadinSecurityConfigurer + http.with(VaadinSecurityConfigurer.vaadin(), configurer -> { +// tag::snippet[] + configurer.loginView("/login"); +// end::snippet[] + }); + + return http.build(); + } + ... +} +---- + +Now, when a user tires to access a protected view, they'll be redirected to the login page. + +[IMPORTANT] +By default, Vaadin *permits access to Hilla views and router layouts*, and *denies access to Flow views and router layouts*. This is covered in more detail in the <<../protect-views#,Protect Views>> guide. + diff --git a/articles/building-apps/react/add-logout.adoc b/articles/building-apps/react/add-logout.adoc new file mode 100644 index 0000000000..ab45443c8f --- /dev/null +++ b/articles/building-apps/react/add-logout.adoc @@ -0,0 +1,80 @@ +--- +title: Hilla +page-title: How to logout from a Vaadin Hilla application +meta-description: Learn to implement logout functionality with Hilla views using the useAuth hook. This guide covers logout handling, redirect, and best practices. +order: 10 +--- + + += Hilla Logout +:toclevels: 2 + +In this guide, you'll learn how to implement logout functionality in a Vaadin application with Hilla views. + + +== Logging Out + +Vaadin provides a React hook `useAuth`, which includes a `logout()` function. Calling this function *logs out the user* and *redirects* them to a preconfigured *logout success URL*. + +[NOTE] +You configured the `useAuth` hook in the <<../add-login/hilla#,Add Login>> guide. + +You typically call `logout()` from a *button* or *menu item* click listener. Here's how to add a logout button to a view: + +[source,tsx] +---- +import { Button } from '@vaadin/react-components'; +import { useAuth } from "Frontend/security/auth"; + +export default function LogoutView() { + const { logout } = useAuth(); + return ( +
+ +
+ ); +} +---- + + +== Configuring the Logout Success URL + +By default, users are redirected to the root URL (`/`) after logging out. To change this, *specify a custom logout success URL* in your security configuration: + +.`SecurityConfig.java` +[source,java] +---- +@EnableWebSecurity +@Configuration +class SecurityConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // Configure Vaadin's security using VaadinSecurityConfigurer + http.with(VaadinSecurityConfigurer.vaadin(), configurer -> { +// tag::snippet[] + configurer.loginView("/login", "/logged-out.html"); // <1> +// end::snippet[] + }); + return http.build(); + } + ... +} +---- + +<1> Sets `/logged-out.html` as the *logout success URL*. + +If your application runs at `\https://example.com`, users will be redirected to `\https://example.com/logged-out.html` after logging out. + + +=== Absolute vs. Relative URLs + +The logout success URL can be either absolute or relative. + +* *Absolute URLs* -- Start with `https://` or `http://` (e.g., `\https://example.com/logged-out`). +* *Relative URLs* -- Start with `/` (e.g., `/logged-out.html`). + +.Relative logout URLs must include the context path +[IMPORTANT] +If your application is deployed at `\https://example.com/app`, the logout URL should be `/app/logged-out.html`. + diff --git a/articles/building-apps/react/add-router-layout.adoc b/articles/building-apps/react/add-router-layout.adoc new file mode 100644 index 0000000000..e1251af593 --- /dev/null +++ b/articles/building-apps/react/add-router-layout.adoc @@ -0,0 +1,171 @@ +--- +title: Hilla +page-title: How to use router layouts in Hilla | Vaadin +meta-description: Learn how to create and customize router layouts in Hilla applications, including main layouts and nested layouts. +order: 10 +--- + + += Router Layouts in Hilla +:toclevels: 2 + +In this guide, you'll learn how to create and customize router layouts in Hilla views. Router layouts are special view components that wrap around other views, providing common UI elements like navigation bars, menus, and footers. + + +== Router Layouts in Hilla + +Router layouts in Hilla are React components that wrap other views. Router layouts do not create separate navigable routes, but they wrap views that are mapped to the actual routes. Common use cases for layouts are for providing the application's shell (e.g. including common UI elements like navigation bar, side menu, footer, etc.), or as a nested layout that wraps specific set of views with additional UI elements. + + +== Creating a Router Layout + +To create a router layout, create a file named `@layout.tsx` in any directory under the `views`. The Hilla router, by convention, wraps all the views in the respective directory and its subdirectories with that layout. The layout must render the `` component where child views should appear. + +Here's an example of a basic router layout created directly under the `views` directory that wraps all views in the application, as it is located in the root of `views` directory: + +[source,tsx] +.frontend/views/@layout.tsx +---- +// tag::snippet[] +import { Outlet } from 'react-router'; +// end::snippet[] + +export default function MainLayout() { + return ( +
+
+

My Application

+
+ +
+ {/* tag::snippet[] */} + + {/* end::snippet[] */} +
+ +
+

© 2025 My Company

+
+
+ ); +} +---- + +In this example, the `MainLayout` component wraps all views in the application with a common header and footer. The `` component acts as a placeholder of where the child views should render. Having `@layout.tsx` files is not limited to the root directory, you can create them in any subdirectory to create nested layouts. + +Here's an example of a layout that wraps the views defined in the `customers` directory and any possible subdirectories: + +[source,tsx] +.frontend/views/customers/@layout.tsx +---- +import { Outlet } from 'react-router'; + +export default function CustomersLayout() { + return ( +
+
+

Customers

+
+ +
+ +
+
+ ); +} +---- + +Depending on where a view file is located, by default it is wrapped and rendered within the available `@layout.tsx` of that directory, and also all the parent layouts of that directory. + +For example: + +[source] +---- +views +├── customers +│ ├── {id} +│ │ ├── @index.tsx +│ │ └── edit.tsx +│ ├── @index.tsx +│ └── @layout.tsx <1> +├── @index.tsx +└── @layout.tsx <2> +---- +<1> The layout file that wraps all views in the `customers` directory and its subdirectories +<2> The layout file that wraps all views in the application + + +== Skipping Layouts + +There are certain views and routes that should not be rendered inside any layouts. A `login` view is common example of such a view that should escape being rendered within the application layout. You can skip the layouts that are applied to views using the `ViewConfig` configuration object. Export this object from your view file to instruct the router not to wrap this view inside any layout available in the directory structure: + +[source,tsx] +.frontend/views/login.tsx +---- +// tag::snippet[] +import { ViewConfig } from '@vaadin/hilla-file-router/types.js'; +// end::snippet[] + +export default function LoginView() { + return ( +
Login form here
+ ); +} + +export const config: ViewConfig = { +// tag::snippet[] + skipLayouts: true, // <1> +// end::snippet[] +}; +---- +<1> Instruct the router to skip all the layouts for this view + + +== Creating Dynamic Menus + +The Hilla router provides utilities to create navigation menus based on your route structure. Use the `createMenuItems()` utility to automatically generate menu items: + +[source,tsx] +.frontend/views/@layout.tsx +---- +// tag::snippet[] +import { createMenuItems } from '@vaadin/hilla-file-router/runtime.js'; +// end::snippet[] +import { useLocation, useNavigate } from 'react-router'; +import { SideNav } from '@vaadin/react-components/SideNav.js'; +import { SideNavItem } from '@vaadin/react-components/SideNavItem.js'; +import { Icon } from '@vaadin/react-components/Icon.js'; + +export default function MainMenu() { + const navigate = useNavigate(); + const location = useLocation(); + + return ( + path && navigate(path)} + location={location} + > + {createMenuItems().map(({ to, icon, title }) => ( // <1> + + {icon && } + {title} + + ))} + + ); +} +---- +<1> Iterate over the list of available routes returned by `createMenuItems()` and create a menu item for each route + +[NOTE] +The `createMenuItems()` utility returns all routes available in the application, including the routes from Flow views. + + +== Best Practices + +When working with router layouts in Hilla, follow these best practices: + +1. Use `@layout.tsx` naming convention for layout files +2. Always render the `` component where child views should appear +3. Consider skipping layouts for authentication views + diff --git a/articles/building-apps/react/add-service.adoc b/articles/building-apps/react/add-service.adoc new file mode 100644 index 0000000000..d91285616f --- /dev/null +++ b/articles/building-apps/react/add-service.adoc @@ -0,0 +1,273 @@ +--- +title: Hilla +page-title: How to call application services from Hilla | Vaadin +meta-description: Learn how to make Java services browser-callable and call them from Hilla views. This guide covers service annotations, TypeScript client generation, type safety, and best practices. +order: 10 +--- + + += Hilla +:toclevels: 2 + +In this guide, you'll learn how to make a Java service browser-callable and how to call it from a Hilla view. This guide covers the basics to get you started. For more details, see the <<{articles}/hilla/guides/endpoints#,Hilla Endpoints Reference Guide>>. + + +== Making a Service Browser-Callable + +To call a Java service from Hilla, you must first make it _browser-callable_. When a service is browser-callable, Vaadin creates a server endpoint and generates a TypeScript client for it. *This allows you to call the Java service from the browser in a type-safe way, without having to create a REST controller for it.* Additionally, the generated client handles the endpoint URL automatically, eliminating the need for manual configuration. + +To make a Java service browser-callable, annotate it with [annotationname]`@BrowserCallable`: + +[source,java] +---- +// tag::snippet[] +@BrowserCallable +@AnonymousAllowed +// end::snippet[] +public class CounterService { + public int addOne(int number) { + return number + 1; + } +} +---- + +Vaadin protects browser-callable services by default to prevent unauthorized access. This means you must always use a security annotation like [annotationname]`@AnonymousAllowed` or [annotationname]`@RolesAllowed` to explicitly define access control. Refer to the <<../../security/protect-services/hilla#,Protect Browser-Callable Services>> guide for more details. + + +=== When is the TypeScript Client Generated? + +Vaadin generates TypeScript clients on the following occasions: + +- When the application starts up in development mode +- When a browser-callable service is hotswapped +- When Spring DevTools reloads the application +- During a production build + +If needed, you can manually trigger the TypeScript client generation using Maven: + +[.example] +-- +.Terminal +[source,terminal,subs="+attributes"] +---- + +./mvnw compile vaadin:generate +---- + +.PowerShell +[source,terminal,subs="+attributes"] +---- + +mvnw compile vaadin:generate +---- +-- + + +[NOTE] +The generator analyzes Java byte code, which is why you need to compile your Java classes before you run the generator. + + +== Calling a Service + +You can import TypeScript clients from the `src/main/frontend/generated/endpoints.ts` file. + +The following example calls the `CounterService` when the user clicks a button: + +[source,tsx] +---- +import { Button } from "@vaadin/react-components"; +import { useSignal } from "@vaadin/hilla-react-signals"; +// tag::snippet[] +import { CounterService } from "Frontend/generated/endpoints"; +// end::snippet[] + +export default function CounterView() { + const number = useSignal(0); // Initialize state with 0 + const addOne = async () => { +// tag::snippet[] + number.value = await CounterService.addOne(number.value); +// end::snippet[] + }; + return ( +
+
{number}
+ +
+ ); +} +---- + +TypeScript client functions are always `async`, so you must use the `await` keyword or handle the returned `Promise`. + + +== Input & Output + +A browser-callable service can return and receive as input both primitive types and non-primitive types. When a non-primitive type (such as an object) is used, Vaadin automatically generates a corresponding TypeScript interface. + +You can use both Java classes and records as input and output. They can be standalone classes, inner classes of the service itself, or come from a project dependency. + +.Prefer Records to Classes +[TIP] +If you're unsure whether to use classes or records for input and output objects, opt for records. Records are preferred over classes because they are more concise and immutable by default, reducing boilerplate code. + +Vaadin uses Jackson for object serialization between Java and JSON. You can use some Jackson annotations to customize the generated TypeScript interface as well. See the <<{articles}/hilla/reference/type-conversion#,Type Conversion Reference Guide>> for details. + +For example, consider the following Java code: + +.City.java +[source,java] +---- +public record City ( // <1> + String name, + String country +) {} +---- +<1> This is a standalone record (which could also be a class). + +.CityService.java +[source,java] +---- +@BrowserCallable +@AnonymousAllowed +public class CityService { + public List findCities(Query query) { + // ... + } + + public static class Query { // <1> + private final int numberOfCities; + + public Query(final int numberOfCities) { + this.numberOfCities = numberOfCities; + } + + public int getNumberOfCities() { + return numberOfCities; + } + } +} +---- +<1> This is an inner class. + +The generated TypeScript interfaces for [classname]`City` and [classname]`Query` would look like this: + +.city.ts +[source,typescript] +---- +interface City { + name?: string; + country?: string; +} +export default City; +---- + +.query.ts +[source,typescript] +---- +interface Query { + numberOfCities: number; +} +export default Query; +---- + + +== Nullable & Non-Nullable Types + +In TypeScript, attributes can be either nullable or non-nullable. Vaadin follows this rule when generating TypeScript interfaces: + +* Primitive types (e.g., `int`, `double`, `boolean`) are non-nullable by default. +* Reference types (e.g., `String`, `UUID`, `LocalDate`) are nullable by default. + +If you look at the earlier examples, you'll see that `numberOfCities` is non-nullable, whereas `name` and `country` are both nullable. This is because `numberOfCities` is a primitive type (`int`) and the others are reference types (`String`). + +You can force a reference type to be generated as non-nullable by using the *JSpecify* [annotationname]`@NonNull` annotation. You can control nullability in other ways as well; see the <<{articles}/hilla/reference/type-nullability#,Type Nullability Reference Guide>> for details. + +[NOTE] +Starting from version 24.7, Vaadin includes *JSpecify* as a transitive dependency. + +For example, to make `name` and `country` non-nullable, you'd do this: + +[source,java] +---- +import org.jspecify.annotations.NonNull; + +public record City( + @NonNull String name, + @NonNull String country +) {} +---- + +The generated TypeScript interface would then look like this: + +.city.ts +[source,typescript] +---- +interface City { + name: string; + country: string; +} +export default City; +---- + +The fields are no longer marked as nullable (i.e., the `?` is missing). + + +=== Service Methods + +The nullability rules apply to input parameters and return values of browser-callable service methods as well. For example, consider the following service: + +[source,java] +---- +@BrowserCallable +@AnonymousAllowed +public class CityService { + public List findCities(Query query) { + // ... + } +} +---- + +The generated TypeScript function would look like this: + +[source,typescript] +---- +async function findCities_1( + query: Query | undefined, + init?: EndpointRequestInit_1 +): Promise | undefined> { + // ... +} +---- + +By default, the query parameter, the returned array, and its elements are all nullable, which may not always be desirable. To make everything non-nullable, you'd have to annotate all three items, like this: + +[source,java] +---- +@BrowserCallable +@AnonymousAllowed +public class CityService { +// tag::snippet[] + public @NonNull List<@NonNull City> findCities(@NonNull Query query) { +// end::snippet[] + // ... + } +} +---- + +The generated TypeScript function would then look like this: + +[source,typescript] +---- +async function findCities_1( + query: Query, + init?: EndpointRequestInit_1 +): Promise> { + // ... +} +---- + +.Change the Default Nullability +[TIP] +If most types in your project should be non-nullable by default, apply Spring's [annotationname]`@NonNullApi` annotation at the package level in `package-info.java`. This makes all types in the package non-nullable unless explicitly marked as `@Nullable`. + +// TODO Write a mini-tutorial (left out for now due to a tight schedule) \ No newline at end of file diff --git a/articles/building-apps/react/add-view.adoc b/articles/building-apps/react/add-view.adoc new file mode 100644 index 0000000000..5f429f2c26 --- /dev/null +++ b/articles/building-apps/react/add-view.adoc @@ -0,0 +1,170 @@ +--- +title: Add a React View +page-title: How to add a React view to a Vaadin application +meta-description: Learn to create, name, and organize React views in Vaadin - routing conventions, directory structures, route aliases, and best practices for defining explicit routes. +order: 5 +--- + + += React Views +:toclevels: 2 + +In this guide, you'll learn how to create and name views in React, assign multiple routes to a single view, and organize views into view directories. + +== Creating Views + +Hilla views are React components that are returned by the default exported function defined in a TSX file: + +[source,tsx] +.hello.tsx +---- +export default function HelloView() { + return

Hello, World!

; +} +---- + +The default way for creating routes for views in Hilla is based on the file system structure. The route for a view is determined based on: + +1. where the respective file for a view is located, relative to the `src/main/frontend/views` directory, and + +2. how the respective file for a view is named. + +For example, if the [filename]`hello.tsx` file is located directly under `src/main/frontend/views`, then the route for `HelloView` component (default export from this file) is `/hello`, which means the file name [filename]`hello.tsx` excluding the `.tsx`. If the application is running from the root context, users would be able to access this view by navigating to `\https://example.com/hello`. Moreover, moving this file under `src/main/frontend/views/greeting` directory changes the route to `/greeting/hello`, so the users can access this view by navigating to `\https://example.com/greeting/hello`. + +Likewise, naming the view file as [filename]`hello-world.tsx` or [filename]`HelloWorld.tsx` results in the route `/hello-world` or `/HelloWorld`. + +To avoid repeating the `src/main/frontend/views` throughout this guide, `views` is used as an alias to refer to this directory. + +If a view is supposed to target the root route, the file should be named as [filename]`@index.tsx` and located directly under the `views` directory: + +[source,tsx] +.@index.tsx +---- +export default function HomeView() { + return

Home

; +} +---- + +Then, the route for the above component is `/`, and if the application is running from the root context, users can access this view by navigating to `\https://example.com/`. + + +== View Naming + +React only recognizes an exported function as a component if the function name starts with an uppercase letter. For example, the following component is recognized as a valid React component: + +[source,tsx] +.customers.tsx +---- +export default function CustomersView() { + return

Customers

; +} +---- + +Defining the function name as `customersView` or `customerList` does not result in a compile or runtime error, but is not recognized as a React component either. + +The routing uses the React component's name for creating the default automatic title for the view. For example, the title for the `CustomersView` component is `Customers`, and the title for the `HelloWorldView` component is `Hello World`. This automatically-determined title is used when creating the navigation menu based on utilities from the routing API. + +Another important convention to consider while naming the views and directories is to use the `_` (underscore) character at the beginning of the file or directory name to instruct the routing system to ignore it. For example, a file named as `_AddressFormComponent.tsx` is ignored when creating routes for views. This is useful for creating utility files and reusable components that are not intended to be available as navigation targets. + +The details about the automatic title and the navigation menu are covered in more detail in the <<../navigate#,Navigate to a View>> guide. + + +== Route Aliases + +In Hilla, there is no shortcut for creating multiple routes that target the same view. However, you can create a new view file that exports a component returning the target component. You can place this alias component in any directory to create desired route alias. For example, the following view targets the root route (`/`): + +[source,tsx] +.@index.tsx +---- +export default function RootView() { + return

Home

; +} +---- + +Now to have the same view accessible via `/home` and `/main`, you can create two additional view files: + +[source,tsx] +.home.tsx +---- +export default function HomeView() { + return RootView(); +} +---- + +and + +[source,tsx] +.main.tsx +---- +export default function MainView() { + return RootView(); +} +---- + + +== Directory Structure + +As mentioned earlier, the file system structure determines the route for a view. Therefore, organizing views into directories helps maintain a clear structure for views and routes. + +Simple views that consist of only one file can be placed directly under the `views` directory. For example, "About", "Contact Us", and the "Home" view do not need a complex composition of components, nor should have a long route, so it is a good practice to place them directly under the `views` directory: + +[source] +---- +src +└── main + └── frontend + └── views + ├── @index.tsx + ├── about.tsx + └── contact-us.tsx +---- + +This way, the routes for these views are `/`, `/about`, and `/contact-us`, respectively. + +However, for more complex views that consist of other components, or if they should accept route parameters, it is recommended to place all the files related to that view under a directory, named by the functionality they provide. For example, views related to customers can be grouped under a `customers` directory, and views related to products under a `products` directory. The following is an example of a directory structure for view files that handle the customer related functionalities: + +[source] +---- +src +└── main + └── frontend + └── views + └── customers + ├── {id} <1> + │ ├── edit.tsx <2> + │ └── index.tsx <3> + ├── @index.tsx <4> + └── new.tsx <5> +---- +1. The `{id}` directory is a placeholder for the route parameter. You will learn more about route parameters in the <<../navigate#,Navigate to a View>> guide. +2. The [filename]`edit.tsx` file is responsible for editing a specified customer details. The route for this view is `/customers/:id/edit`. +3. The [filename]`@index.tsx` file is responsible for displaying the details of a specified customer. The route for this view is `/customers/:id`. +4. The [filename]`index.tsx` file is responsible for displaying the list of customers. The route for this view is `/customers`. +5. The [filename]`new.tsx` file is responsible for adding a new customer. The route for this view is `/customers/new`. + +As this guide focuses on basics of creating views in Hilla, further details about routing conventions are covered in the <<{articles}/hilla/guides/routing#, Routing>> guide. + + +== Defining Explicit Routes + +So far, you have learned how to create views and how routes are automatically resolved based on the file system structure and file name. However, if you want to have a custom route for a view, you can export a `ViewConfig` object named `config` from the view file. The path specified for the `route` overrides the automatically-resolved path according to the routing conventions. For example, the following view has a custom route `/custom-route`: + +[source,tsx] +.hello.tsx +---- +import { ViewConfig } from "@vaadin/hilla-file-router/types.js"; + +export const config: ViewConfig = { + route: "/custom-route", +}; + +export default function HelloView() { + return

Hello, World!

; +} +---- + +Now, users can access this view by navigating to `\https://example.com/custom-route`. + +[NOTE] +Avoid using explicit routes unless absolutely necessary. The routing system is designed to automatically resolve the routes based on the file system structure and the file name, which helps to keep the routes consistent and predictable. + diff --git a/articles/building-apps/react/dialogs-and-drawers.adoc b/articles/building-apps/react/dialogs-and-drawers.adoc new file mode 100644 index 0000000000..99d94a9826 --- /dev/null +++ b/articles/building-apps/react/dialogs-and-drawers.adoc @@ -0,0 +1,254 @@ +--- +title: Hilla +page-title: How to show a form in dialogs and drawers in Hilla | Vaadin +meta-description: Learn how to reuse a form component for both creating and editing items in Vaadin Hilla. +order: 10 +--- + += Form Dialogs and Drawers in Hilla + +This guide explains how to reuse a form component for both creating and editing items in Hilla, manage form state using callbacks, and integrate with other UI elements like grids. + +.Part of a Series +[TIP] +This is part 4 of the *Add a Form* series. It builds on the concepts introduced in the <<../fields-and-binding/hilla#,Fields & Binding>>, <<../validation/hilla#,Form Validation>> and <<../loading-and-saving/hilla#,Loading & Saving>> guides. You should also read the <<.#,Overview>>. + + +== Displaying Forms in Dialogs + +The following example shows a dialog used to create new project proposals. The parent supplies an `onSave` event handler, which is called when the user clicks the `Create Proposal` button. The example uses the `useForm` hook, introduced in the <<../fields-and-binding/hilla#,Fields & Binding>> guide: + +.ProposalDialog.tsx +[source,tsx] +---- +import { Button, Dialog } from "@vaadin/react-components"; +import { useForm } from "@vaadin/hilla-react-form"; + +import ProposalForm from "Frontend/components/ProposalForm"; +import Proposal from "Frontend/generated/com/example/application/tutorial/domain/Proposal"; +import ProposalModel from "Frontend/generated/com/example/application/tutorial/domain/ProposalModel"; + +export type ProposalDialogProps = { + opened?: boolean, + onOpenedChanged?: (opened: boolean) => void, + onSave?: (proposal: Proposal) => Promise +} + +export function ProposalDialog( + {opened = false, onOpenedChanged, onSave}: ProposalDialogProps + ) { + + const form = useForm(ProposalModel, { + onSubmit: onSave // <1> + }); + + const handleClose = () => { + form.clear(); // <2> + onOpenedChanged?.(false); + }; + + const handleSubmit = async () => { + await form.submit(); // <3> + handleClose(); + }; + + return ( + onOpenedChanged?.(e.detail.value)} + footer={ + <> + + + + }> + + + ); +} +---- +<1> Uses a callback to let the caller decide how to save the FDO. +<2> Clears the form after the dialog is closed, so that it is fresh when re-opened. +<3> Throws an error if the form is invalid, keeping the dialog visible. + +.Tip for Flow Developers +[TIP] +In Hilla, you don't call `open()` to show a dialog. Instead, you control its visibility using the `opened` property. The dialog itself can't update this property directly. Instead, it emits an `onOpenedChanged` event to inform the parent that it was closed. The parent is then responsible for updating the opened property accordingly. + + +Here's how you might use the dialog in your application: + +[source,tsx] +---- +// (Imports omitted for clarity) + +export default function ProposalView() { + const createDialogOpened = useSignal(false); // <1> + const dataProvider = useDataProvider({ // <2> + list: (pageable) => ProposalService.list(pageable), + }); + const selection = useSignal([]); // <3> + + const insertProposal = async (proposal: Proposal) => { + try { + const result = await ProposalService.save(proposal); // <4> + dataProvider.refresh(); // <5> + selection.value = [result]; // <6> + } catch (error) { + handleError(error); + } + }; + + return ( +
+ + { + const item = event.detail.value; + selection.value = item ? [item] : []; + }}> + + + + + createDialogOpened.value = opened} + onSave={insertProposal} + /> +
+ ); +} +---- +<1> This signal controls whether the dialog is open or closed. +<2> This is a data provider for <<{articles}/components/grid#lazy-loading,lazy-loading a Grid>>. +<3> This signal contains the selected proposals. +<4> Inserts the proposal with an application service using *Single Save*. +<5> Refreshes the grid of proposals so that the new one shows up. +<6> Selects the newly added proposal, opening the edit drawer. + + +== Displaying Forms in Drawers + +The following example shows *a drawer that reuses the same form component from the dialog example to edit project proposals*: + +.ProposalDrawer.tsx +[source,tsx] +---- +import { Button } from "@vaadin/react-components"; +import { useForm } from "@vaadin/hilla-react-form"; +import { useEffect } from "react"; + +import ProposalForm from "Frontend/components/ProposalForm"; +import Proposal from "Frontend/generated/com/example/application/tutorial/domain/Proposal"; + +export type ProposalDrawerProps = { + opened?: boolean, + onOpenedChanged?: (opened: boolean) => void, + proposal?: Proposal, + onSave?: (proposal: Proposal) => Promise +} + +export function ProposalDrawer( + {opened = false, onOpenedChanged, proposal, onSave}: ProposalDrawerProps + ) { + + const form = useForm(ProposalModel, { + onSubmit: onSave // <1> + }); + + const handleClose = () => { + onOpenedChanged?.(false); + } + + const handleSubmit = async () => { + await form.submit(); + handleClose(); // <2> + } + + useEffect(() => { + form.read(proposal); // <3> + }, [proposal]); + + return ( + + ); +} +---- +<1> Uses a callback to let the caller decide how to save the FDO. +<2> Closes the drawer after submitting. Depending on the UX design, you may want to keep the drawer open. +<3> Populates the form whenever the `proposal` prop is changed. + +To show the drawer when a user selects an item from a grid, you can use the following pattern: + +[source,tsx] +---- +// (Imports omitted for clarity) + +export default function ProposalView() { + const dataProvider = useDataProvider({ + list: (pageable) => ProposalService.list(pageable), + }); + const selection = useSignal([]); // <1> + + const updateProposal = async (proposal: Proposal) => { + try { + await ProposalService.save(proposal); // <2> + dataProvider.refresh(); // <3> + } catch (error) { + handleError(error); + } + } + + return ( +
+ { + const item = event.detail.value; + selection.value = item ? [item] : []; + }}> + + + + + 0} + onOpenedChanged={opened => { + if (!opened) { + selection.value = []; + } + }} + proposal={selection.value[0]} + onSave={updateProposal}/> +
+ ); +} +---- +<1> The drawer is visible whenever the this signal contains a proposal. When the drawer is closed, the signal is cleared. The drawer uses *Load from Selection*. +<2> Saves the proposal with an application service using *Single Save*. +<3> Refreshes the grid of proposals so that the changes show up. + +.Tip for Flow Developers +[TIP] +In Hilla, you can't directly access the current selection of a Grid. Instead, you create a signal to hold the selected item and update it using the `onActiveItemChanged` event. In the example above, the grid's selection, the drawer's visibility, and the form's content are all driven by a single `selection` signal. + +If you need a refresher on form loading and saving strategies, see the <<../loading-and-saving#,Loading & Saving>> guide. \ No newline at end of file diff --git a/articles/building-apps/react/fields-and-binding.adoc b/articles/building-apps/react/fields-and-binding.adoc new file mode 100644 index 0000000000..c80af531dc --- /dev/null +++ b/articles/building-apps/react/fields-and-binding.adoc @@ -0,0 +1,279 @@ +--- +title: Hilla +page-title: How to use fields and data binding in Hilla | Vaadin +meta-description: Learn how to build forms in Vaadin Hilla using layout components, input fields, and the useForm hook to bind data with Java-based FDOs. +order: 10 +--- + + += Fields and Binding in Hilla +:toclevels: 2 + + +This guide introduces the fundamentals of building forms and binding fields in Hilla. It covers how to lay out form fields, choose the appropriate input components, and use the `useForm` hook to connect those components to your application's data model. For more in-depth information about forms in Hilla, see the <<{articles}/hilla/guides/forms#,Hilla Reference Guide>>. + +.Part of a Series +[TIP] +This is part 1 of the *Add a Form* series. You should also read the <<.#,Overview>>. + + +== Laying Out the Fields + +In Hilla, you build forms visually by *adding input components to a layout component*. The most common layout components for forms are <>, <>, and an ordinary `
` (with some CSS). The Form Layout component supports multiple columns, whereas Vertical Layout lays out the components in a single column. + +.Layout or
? +[TIP] +If you are used to working with Flow, you can continue to work with the familiar layout components in Hilla. If you have a background in React or some other client-side framework, you may feel more comfortable working with `
`. + +The following example shows how to build a two-column project proposal form with various input components: + +.ProposalForm.tsx +[source,tsx] +---- +import { ComboBox, DatePicker, FormLayout, TextArea, TextField } from "@vaadin/react-components"; +import ProposalType from "Frontend/generated/com/example/application/domain/ProposalType"; + +export default function ProposalForm() { + return ( + + + {/* <1> */} +