Saltar al contenido principal

Routing and Composites

Abrir en ChatGPT

Hasta ahora, este tutorial ha sido solo una aplicación de una sola página. Este paso cambia eso. Moverás la UI que creaste en Trabajando con Datos a su propia página y crearás otra página para agregar nuevos clientes. Luego, conectarás estas páginas para que tu aplicación pueda navegar entre ellas aplicando estos conceptos:

Completar este paso crea una versión de 3-routing-and-composites.

Ejecutando la aplicación

A medida que desarrollas tu aplicación, puedes usar 3-routing-and-composites como comparación. Para ver la aplicación en acción:

  1. Navega al directorio de nivel superior que contiene el archivo pom.xml; este es 3-routing-and-composites si estás siguiendo la versión en GitHub.

  2. Utiliza el siguiente comando de Maven para ejecutar la aplicación de Spring Boot localmente:

    mvn

Ejecutar la aplicación abre automáticamente un nuevo navegador en http://localhost:8080.

Aplicaciones enrutables

Anteriormente, tu aplicación tenía una única función: mostrar una tabla de datos de clientes existentes. En este paso, tu aplicación también podrá modificar los datos de los clientes al agregar nuevos clientes. Separar las UIs para visualización y modificación es beneficioso para el mantenimiento y pruebas a largo plazo, por lo que agregarás esta característica como una página separada. Harás que tu aplicación sea enrutable para que webforJ pueda acceder y cargar las dos UIs individualmente.

Una aplicación enrutable renderiza la UI en función de la URL. Anotar la clase que extiende la clase App con @Routify habilita el enrutamiento, y el elemento packages le dice a webforJ qué paquetes contienen componentes de UI.

Cuando agregues la anotación @Routify a Application, elimina el método run(). Moverás los componentes de ese método a una clase que crearás en el paquete com.webforj.tutorial.views. Tu archivo Application.java actualizado debería verse así:

Application.java
@SpringBootApplication
@BundleEntry("css/card.css")
@AppTheme("system")

// Añadida anotación @Routify
@Routify(packages = "com.webforj.tutorial.views")

@AppProfile(name = "CustomerApplication", shortName = "CustomerApplication")
public class Application extends App {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

// Eliminado método App.run() sobrescrito

}
CSS global

Mantener la anotación @BundleEntry en Application agrega el archivo CSS al paquete frontend a nivel de aplicación, por lo que los estilos siguen disponibles en todas las vistas enrutadas.

Creando rutas

Agregar la anotación @Routify hace que tu aplicación sea enrutable. Una vez que sea enrutable, tu aplicación buscará en el paquete com.webforj.tutorial.views para encontrar rutas. Necesitarás crear las rutas para tus UIs y también especificar sus Tipos de Ruta. El tipo de ruta determina cómo mapear el contenido de la UI a la URL.

El primer tipo de ruta es View. Este tipo de rutas se mapean directamente a un segmento de URL específico en tu aplicación. Las UIs para la tabla y el formulario del nuevo cliente serán ambas rutas de tipo View.

El segundo tipo de ruta es Layout, que contiene UI que aparece en múltiples páginas, como un encabezado o una barra lateral. Las rutas de diseño también envuelven vistas secundarias sin contribuir a la URL.

Para especificar el tipo de ruta de una clase, agrega el tipo de ruta al final del nombre de la clase como sufijo. Por ejemplo, MainView es de tipo ruta View.

Para mantener las dos funciones de la aplicación separadas, tu aplicación necesita mapear las UIs a dos rutas únicas de tipo View: una para la tabla y otra para el formulario de cliente. En /src/main/java/com/webforj/tutorial/views, crea dos clases con un sufijo View:

  • MainView: Esta vista tendrá la Tabla que estaba previamente en la clase Application.
  • FormView: Esta vista tendrá un formulario para agregar nuevos clientes.

Mapeando URLs a componentes

Tu aplicación es enrutable y sabe buscar dos rutas de tipo View, MainView y FormView, pero no tiene una URL específica para cargarlas. Usando la anotación @Route en una clase de vista, puedes decirle a webforJ dónde cargarla en función de un segmento de URL dado. Por ejemplo, usando @Route("about") en una vista mapea localmente la clase a http://localhost:8080/about.

Como su nombre indica, MainView es la clase que quieres cargar inicialmente cuando se ejecuta la aplicación. Para lograr esto, agrega una anotación @Route que mapee MainView a la URL raíz de tu aplicación:

MainView.java
@Route("/")
public class MainView {

public MainView() {
}

}

Para el FormView, mapea la vista para que se cargue cuando un usuario vaya a http://localhost:8080/customer:

FormView.java
@Route("customer")
public class FormView {

public FormView() {
}

}
Comportamiento predeterminado

Si no asignas explícitamente un valor para la anotación @Route, el segmento de URL es el nombre de la clase convertido a minúsculas, con el sufijo View eliminado.

  • MainView se mapearía a /main
  • FormView se mapearía a /form

Características compartidas

Además de ser ambas rutas de vista, MainView y FormView comparten características adicionales. Algunas de estas características compartidas, como el uso de componentes Composite, son fundamentales para usar aplicaciones webforJ, mientras que otras simplemente facilitan la gestión de tu aplicación.

Usando componentes Composite

Cuando la aplicación era de una sola página, almacenabas los componentes dentro de un Frame. En adelante, con una aplicación con múltiples vistas, necesitarás envolver esos componentes de UI dentro de componentes Composite.

Los componentes Composite son envoltorios que facilitan la creación de componentes reutilizables. Para crear un componente Composite, extiende la clase Composite con un componente base especificado que sirva como base de la clase, por ejemplo, Composite<FlexLayout>.

Este tutorial utiliza elementos Div como los componentes base, pero pueden ser cualquier componente, como FlexLayout o AppLayout. Usando el método getBoundComponent(), puedes hacer referencia al componente base y tener acceso a sus métodos. Esto te permite establecer el tamaño, agregar un nombre de clase CSS, agregar componentes que deseas mostrar en el componente Composite, y acceder a métodos específicos del componente.

Para MainView y FormView, extiende Composite con Div como componente base. Luego, haz referencias a ese componente base para que puedas agregar las UIs más adelante. Ambas vistas deberían tener una estructura similar a la siguiente:

// Extiende Composite con un componente base
public class MainView extends Composite<Div> {

// Accede al componente base
private Div self = getBoundComponent();

// Crea un componente UI
private Button submit = new Button("Submit");

public MainView() {

// Agrega el componente UI al componente base
self.add(submit);
}
}

Estableciendo el título del marco

Cuando un usuario tiene múltiples pestañas en su navegador, un título de marco único lo ayuda a identificar rápidamente qué parte de la aplicación tiene abierta.

La anotación @FrameTitle define lo que aparece en el título del navegador o en la pestaña de la página. Para ambas vistas, agrega un título de marco usando la anotación @FrameTitle:

MainView.java
@Route("/")
@FrameTitle("Tabla de Clientes")
public class MainView extends Composite<Div> {

private Div self = getBoundComponent();

public MainView(CustomerService customerService) {
}
}

CSS compartido

Con un componente base al que puedes hacer referencia en MainView y FormView, puedes estilizarlo con CSS. Puedes usar el CSS del primer paso, Creando una Aplicación Básica, para darle a ambas vistas estilos de contenedor de UI idénticos. Agrega el nombre de clase CSS card al componente base en cada vista:

MainView.java
@Route("/")
@FrameTitle("Tabla de Clientes")
public class MainView extends Composite<Div> {

private Div self = getBoundComponent();

public MainView() {

self.addClassName("card");
}
}

Usando CustomerService

La última característica compartida para las vistas es el uso de la clase CustomerService. La Tabla en MainView muestra cada cliente, mientras que FormView agrega nuevos clientes. Dado que ambas vistas interactúan con los datos de los clientes, necesitan acceso a la lógica de negocio de la aplicación.

Las vistas obtienen acceso a través del servicio de Spring creado en Trabajando con Datos, CustomerService. Para usar el servicio de Spring en cada vista, haz de CustomerService un parámetro del constructor:

MainView.java
@Route("/")
@FrameTitle("Tabla de Clientes")
public class MainView extends Composite<Div> {

private Div self = getBoundComponent();

public MainView(CustomerService customerService) {
this.customerService = customerService;
self.addClassName("card");
}
}

Creando MainView

Después de hacer que tu aplicación sea enrutable, darle a las vistas envolturas de componentes Composite, y incluir el CustomerService, estás listo para construir las UIs únicas para cada vista. Como se mencionó anteriormente, MainView contiene los componentes de UI que inicialmente estaban en Application. Esta clase también necesita una manera de navegar a FormView.

Agrupando los métodos de la Tabla

A medida que mueves los componentes de Application a MainView, es una buena idea comenzar a seccionar partes de tu aplicación, de modo que un método personalizado pueda realizar cambios en la Tabla de una vez. Seccionar tu código ahora lo hace más manejable a medida que la aplicación crece en complejidad.

Ahora, tu constructor de MainView debería llamar solo a un método buildTable() que agrega las columnas, establece el tamaño y hace referencia al repositorio:

private void buildTable() {
table.setSize("1000px", "294px");
table.setMaxWidth("90vw");
table.addColumn("firstName", Customer::getFirstName).setLabel("Nombre");
table.addColumn("lastName", Customer::getLastName).setLabel("Apellido");
table.addColumn("company", Customer::getCompany).setLabel("Empresa");
table.addColumn("country", Customer::getCountry).setLabel("País");
table.setColumnsToAutoFit();
table.getColumns().forEach(column -> column.setSortable(true));
table.setRepository(customerService.getRepositoryAdapter());
}

Los usuarios necesitan una forma de navegar de MainView a FormView usando la UI.

En webforJ, puedes navegar directamente a una nueva vista usando la clase de vista. Enrutar a través de una clase en lugar de un segmento de URL garantiza que webforJ tomará la ruta correcta para cargar la vista.

Para navegar a una vista diferente, usa la clase Router para obtener la ubicación actual con getCurrent(), luego usa el método navigate() con la clase de vista como parámetro:

Router.getCurrent().navigate(FormView.class);

Este código enviará programáticamente a los usuarios al formulario de nuevo cliente, pero la navegación debe estar conectada a una acción del usuario. Para permitir que los usuarios agreguen un nuevo cliente, puedes modificar o reemplazar el botón de información de Application. En lugar de abrir un cuadro de diálogo de mensaje, el botón puede navegar a la clase FormView:

private Button addCustomer = new Button("Agregar Cliente", ButtonTheme.PRIMARY,
e -> Router.getCurrent().navigate(FormView.class));

MainView completado

Con la navegación a FormView y los métodos de tabla agrupados, aquí está cómo debería verse MainView antes de pasar a crear FormView:

MainView.java
@Route("/")
@FrameTitle("Tabla de Clientes")
public class MainView extends Composite<Div> {
private final CustomerService customerService;
private Div self = getBoundComponent();
private Table<Customer> table = new Table<>();
private Button addCustomer = new Button("Agregar Cliente", ButtonTheme.PRIMARY,
e -> Router.getCurrent().navigate(FormView.class));

public MainView(CustomerService customerService) {
this.customerService = customerService;
addCustomer.setWidth(200);
buildTable();
self.setWidth("fit-content")
.addClassName("card")

Creando FormView

FormView mostrará un formulario para agregar nuevos clientes. Para cada propiedad del cliente, FormView tendrá un componente editable con el que los usuarios interactuarán. Además, tendrá un botón para que los usuarios envíen los datos y un botón de cancelar para descartarlos.

Creando una instancia de Customer

Cuando un usuario está editando datos de un nuevo cliente, los cambios solo deben aplicarse al repositorio cuando estén listos para enviar el formulario. Usar una instancia del objeto Customer es una forma conveniente de editar y mantener los nuevos datos sin editar el repositorio directamente. Crea un nuevo Customer dentro de FormView para usarlo para el formulario:

private Customer customer = new Customer();

Para hacer que la instancia de Customer sea editable, cada propiedad, excepto por el id, debería estar asociada con un componente editable. Los cambios que un usuario realice en la UI deben reflejarse en la instancia de Customer.

Agregando componentes TextField

Las primeras tres propiedades editables en Customer (firstName, lastName, y company) son todos valores de tipo String, y deberían representarse con un editor de texto de una sola línea. Los componentes TextField son una excelente opción para representar estas propiedades.

Con el componente TextField, puedes agregar una etiqueta y un listener de eventos que se activa cada vez que el valor del campo cambia. Cada listener de eventos debe actualizar la instancia de Customer para la propiedad correspondiente.

Agrega tres componentes TextField que actualicen la instancia de Customer:

FormView.java
public class FormView extends Composite<Div> {
private final CustomerService customerService;
private Customer customer = new Customer();
private Div self = getBoundComponent();

private TextField firstName = new TextField("Nombre", e -> customer.setFirstName(e.getValue()));
private TextField lastName = new TextField("Apellido", e -> customer.setLastName(e.getValue()));
private TextField company = new TextField("Empresa", e -> customer.setCompany(e.getValue()));

public FormView(CustomerService customerService) {
this.customerService = customerService;
self.addClassName("card");
}
}
Convención de nombres compartidos

Nombrar los componentes igual que las propiedades que representan para la entidad Customer facilita la vinculación de datos en un paso futuro, Validando y Vinculando Datos.

Agregando un componente ChoiceBox

Usar un TextField para la propiedad country no sería ideal, porque la propiedad solo puede ser uno de cinco valores de enum: UNKNOWN, GERMANY, ENGLAND, ITALY, y USA.

Un mejor componente para seleccionar de una lista de opciones predefinidas es el ChoiceBox.

Cada opción para un componente ChoiceBox está representada como un ListItem. Cada ListItem tiene dos valores, una clave Object y un texto String para mostrar en la UI. Tener dos valores para cada opción permite manejar el Object internamente mientras se presenta una opción más legible para los usuarios en la UI.

Por ejemplo, la clave Object podría ser un Número Internacional Normalizado de Libros (ISBN), mientras que el texto String es el título del libro, que es más legible para los humanos.

new ListItem(isbn, bookTitle);

Sin embargo, esta aplicación maneja una lista de nombres de países, no libros. Para cada ListItem, querrás que el Object sea el enum Customer.Country, mientras que el texto puede ser su representación String.

Para agregar todas las opciones de country en un ChoiceBox, puedes usar un iterador para crear un ListItem para cada enum de Customer.Country, y ponerlos en un ArrayList<ListItem>. Luego, puedes insertar ese ArrayList<ListItem> en un componente ChoiceBox:

// Crear el componente ChoiceBox
private ChoiceBox country = new ChoiceBox("País");

// Crear un ArrayList de objetos ListItem
ArrayList<ListItem> listCountries = new ArrayList<>();

// Agregar un iterador que crea un ListItem para cada opción de Customer.Country
for (Country countryItem : Customer.Country.values()) {
listCountries.add(new ListItem(countryItem, countryItem.toString()));
}

// Insertar el ArrayList lleno en el ChoiceBox
country.insert(listCountries);

// Hacer que el primer `ListItem` sea el predeterminado cuando se carga el formulario
country.selectIndex(0);

Luego, cuando el usuario selecciona una opción en el ChoiceBox, la instancia de Customer debe actualizarse con la clave del elemento seleccionado, que es un valor de Customer.Country.

private ChoiceBox country = new ChoiceBox("País",
e -> customer.setCountry((Customer.Country) e.getSelectedItem().getKey()));

Para mantener el código limpio, el iterador que crea el ArrayList<ListItem> y lo agrega al ChoiceBox debería estar en un método separado. Después de que agregues un ChoiceBox que permite al usuario elegir la propiedad country, FormView debería verse así:

FormView.java
public class FormView extends Composite<Div> {
private final CustomerService customerService;
private Customer customer = new Customer();
private Div self = getBoundComponent();
private TextField firstName = new TextField("Nombre", e -> customer.setFirstName(e.getValue()));
private TextField lastName = new TextField("Apellido", e -> customer.setLastName(e.getValue()));
private TextField company = new TextField("Empresa", e -> customer.setCompany(e.getValue()));

private ChoiceBox country = new ChoiceBox("País",
e -> customer.setCountry((Customer.Country) e.getSelectedItem().getKey()));

public FormView(CustomerService customerService) {
this.customerService = customerService;
self.addClassName("card");
fillCountries();
}

private void fillCountries() {
ArrayList<ListItem> listCountries = new ArrayList<>();
for (Country countryItem : Customer.Country.values()) {
listCountries.add(new ListItem(countryItem, countryItem.toString()));
}
country.insert(listCountries);
country.selectIndex(0);
}

}

Agregando componentes Button

Al usar el nuevo formulario de cliente, los usuarios deberían poder guardar o descartar sus cambios. Crea dos componentes Button para implementar esta característica:

private Button submit = new Button("Enviar");
private Button cancel = new Button("Cancelar");

Tanto el botón de enviar como el de cancelar deben llevar al usuario de regreso a MainView. Esto permite al usuario ver inmediatamente los resultados de su acción, ya sea que vea un nuevo cliente en la tabla o permanezca sin cambios. Dado que múltiples entradas en FormView llevan a los usuarios a MainView, la navegación debe colocarse en un método recuperable:

private void navigateToMain(){
Router.getCurrent().navigate(MainView.class);
}

Botón de cancelar

Descartar los cambios en el formulario no requiere ningún código adicional para el evento más allá de retornar a MainView. Sin embargo, dado que cancelar no es una acción principal, establecer el tema del botón como contorneado le da más prominencia al botón de enviar. La sección Temas de la página del componente Button enumera todos los temas disponibles.

private Button cancel = new Button("Cancelar", ButtonTheme.OUTLINED_PRIMARY,
e -> navigateToMain());

Botón de enviar

Cuando un usuario presiona el botón de enviar, los valores en la instancia de Customer deben utilizarse para crear una nueva entrada en el repositorio.

Usando el CustomerService, puedes tomar la instancia de Customer para actualizar la base de datos H2. Cuando esto sucede, se asigna un nuevo y único id a ese Customer. Después de actualizar el repositorio, puedes redirigir a los usuarios a MainView, donde pueden ver al nuevo cliente en la tabla.

private Button submit = new Button("Enviar", ButtonTheme.PRIMARY,
e -> submitCustomer());

//...

private void submitCustomer() {
customerService.createCustomer(customer);
navigateToMain();
}

Usando un ColumnsLayout

Al agregar los componentes TextField, ChoiceBox y Button, ahora tienes todas las partes interactivas del formulario. La última mejora en FormView en este paso es organizar visualmente los seis componentes.

Este formulario puede usar un ColumnsLayout para separar los componentes en dos columnas sin tener que establecer el ancho de ningún componente interactivo. Para crear un ColumnsLayout, especifica cada componente que debe estar dentro del diseño:

private ColumnsLayout layout = new ColumnsLayout(
firstName, lastName,
company, country,
submit, cancel);

Para establecer el número de columnas para un ColumnsLayout, usa una List de objetos Breakpoint. Cada Breakpoint le dice al ColumnsLayout el ancho mínimo que debe tener para aplicar un número especificado de columnas. Al usar el ColumnsLayout, puedes hacer un formulario con dos columnas, pero solo si la pantalla es lo suficientemente ancha para mostrar dos columnas. En pantallas más pequeñas, los componentes se muestran en una sola columna.

La sección Breakpoints en el artículo de ColumnsLayout explica los breakpoints con más detalle.

Para mantener el código mantenible, establece los breakpoints en un método separado. En ese método, también puedes controlar el espaciado horizontal y vertical entre los componentes dentro del ColumnsLayout con el método setSpacing().

private void setColumnsLayout() {

// Tener dos columnas en el ColumnsLayout si es más ancho que 600px
List<Breakpoint> breakpoints = List.of(
new Breakpoint(600, 2));

// Agregar la lista de breakpoints
layout.setBreakpoints(breakpoints);

// Establecer el espaciado entre componentes usando una variable CSS de DWC
layout.setSpacing("var(--dwc-space-l)")
}

Finalmente, puedes agregar el ColumnsLayout recién creado al componente base de FormView, mientras también estableces el ancho máximo y agregas el nombre de clase de antes:

self.setMaxWidth(600)
.addClassName("card")
.add(layout);

FormView completado

Después de agregar una instancia de Customer, los componentes interactivos y el ColumnsLayout, tu FormView debería verse como sigue:

FormView.java
@Route("customer")
@FrameTitle("Formulario de Cliente")
public class FormView extends Composite<Div> {
private final CustomerService customerService;
private Customer customer = new Customer();
private Div self = getBoundComponent();
private TextField firstName = new TextField("Nombre", e -> customer.setFirstName(e.getValue()));
private TextField lastName = new TextField("Apellido", e -> customer.setLastName(e.getValue()));
private TextField company = new TextField("Empresa", e -> customer.setCompany(e.getValue()));
private ChoiceBox country = new ChoiceBox("País",
e -> customer.setCountry((Customer.Country) e.getSelectedItem().getKey()));
private Button submit = new Button("Enviar", ButtonTheme.PRIMARY, e -> submitCustomer());
private Button cancel = new Button("Cancelar", ButtonTheme.OUTLINED_PRIMARY, e -> navigateToMain());
private ColumnsLayout layout = new ColumnsLayout(
firstName, lastName,

Siguiente paso

Dado que los usuarios ahora pueden agregar clientes, tu aplicación debería poder editar clientes existentes utilizando el mismo formulario. En el siguiente paso, Observadores y Parámetros de Ruta, permitirás que el id del cliente sea un parámetro inicial para FormView, para que pueda llenar el formulario con los datos de ese cliente y permitir que los usuarios cambien las propiedades.