跳至主要内容

Routing and Composites

在 ChatGPT 中打开

到目前为止,本教程仅仅是一个单页应用程序。这一步改变了这一点。 您将把处理数据中创建的UI移动到其自己的页面,并创建另一个页面用于添加新客户。 然后,您将连接这些页面,使您的应用能够通过应用这些概念在它们之间导航:

完成此步骤后,您将创建一个3-routing-and-composites的版本。

运行应用程序

在开发应用程序时,您可以使用3-routing-and-composites作为比较。要查看应用程序的运行情况:

  1. 导航到包含pom.xml文件的顶级目录;如果您按照GitHub上的版本进行操作,那么就是3-routing-and-composites

  2. 使用以下Maven命令在本地运行Spring Boot应用程序:

    mvn

运行应用程序会自动在http://localhost:8080打开一个新浏览器。

可路由的应用程序

以前,您的应用程序只有一个功能:显示现有客户数据的表格。 在这一步中,您的应用程序还能够通过添加新客户来修改客户数据。 将显示和修改的UI分开有利于长期维护和测试,因此您将把此功能作为单独页面添加。 您将使您的应用程序可路由,以便webforJ可以单独访问和加载两个UI。

可路由的应用程序根据URL呈现UI。通过将扩展App类的类注解为@Routify,可以启用路由,而packages元素告诉webforJ哪些包包含UI组件。

当您将@Routify注解添加到Application时,请移除run()方法。您将把该方法中的组件移动到您将在com.webforj.tutorial.views包中创建的类中。您更新后的Application.java文件应如下所示:

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

//添加@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);
}

// 移除了重写的App.run()方法

}
全局CSS

@BundleEntry注解保留在Application中,将CSS文件添加到应用程序级前端包中,因此样式在路由视图间保持可用。

创建路由

添加@Routify注解使您的应用程序可路由。一旦它可路由,您的应用程序将在com.webforj.tutorial.views包中查找路由。 您需要为您的UI创建路由,并指定它们的路由类型。路由类型决定了如何将UI内容映射到URL。

第一个路由类型是View。这种类型的路由直接映射到您的应用程序中某个特定的URL段。用于表格和新客户表单的UI都会是View路由。

第二种路由类型是Layout,它包含在多个页面中出现的UI,例如页眉或侧边栏。布局路由还可以包装子视图,而不对URL产生影响。

要指定类的路由类型,可以在类名后面追加路由类型作为后缀。 例如,MainView就是一个View路由类型。

为了保持应用程序的两个功能分开,您的应用需要将UI映射到两个唯一的View路由:一个用于表格,一个用于客户表单。在/src/main/java/com/webforj/tutorial/views中,创建两个类,后缀为View

  • MainView:此视图将包含之前在Application类中的Table
  • FormView:此视图将包含一个用于添加新客户的表单。

将URL映射到组件

您的应用程序是可路由的,并且知道要寻找两个View路由,MainViewFormView,但它没有特定的URL来加载它们。使用@Route注解在视图类上,您可以告诉webforJ在给定的URL段上加载它。例如,使用@Route("about")在视图上将类本地映射到http://localhost:8080/about

顾名思义,MainView是您希望在应用程序运行时最初加载的类。为此,添加一个@Route注解,将MainView映射到您应用程序的根URL:

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

public MainView() {
}

}

对于FormView,将视图映射,使其在用户访问http://localhost:8080/customer时加载:

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

public FormView() {
}

}
默认行为

如果您没有为@Route注解明确分配值,则URL段为类名转换为小写,去掉View后缀。

  • MainView将映射到/main
  • FormView将映射到/form

共享特性

除了都是视图路由外,MainViewFormView还具有额外的特性。一些共享特性,例如使用Composite组件,是使用webforJ应用程序的基础,而另一些只是使管理您的应用程序更容易。

使用Composite组件

当应用程序是单页时,您将组件存储在一个Frame中。向前看,随着应用程序显示多个视图,您需要将这些UI组件包装在Composite组件内部。

Composite组件是包装器,可以轻松创建可重用组件。 要创建Composite组件,扩展Composite类,并指定一个作为该类基础的绑定组件,例如Composite<FlexLayout>

本教程使用Div元素作为绑定组件,但它们可以是任何组件,例如FlexLayoutAppLayout。使用getBoundComponent()方法,您可以引用绑定组件并访问其方法。这让您可以设置大小、添加CSS类名、添加要在Composite组件中显示的组件,并访问特定于组件的方法。

对于MainViewFormView,将CompositeDiv扩展为绑定组件。然后,引用该绑定组件,以便您可以稍后添加UI。这两个视图的结构应类似于以下内容:

// 扩展Composite与绑定组件
public class MainView extends Composite<Div> {

// 访问绑定组件
private Div self = getBoundComponent();

// 创建一个组件UI
private Button submit = new Button("Submit");

public MainView() {

// 将UI组件添加到绑定组件中
self.add(submit);
}
}

设置框架标题

当用户在浏览器中有多个标签时,一个唯一的框架标题帮助他们快速识别他们打开的应用程序部分。

@FrameTitle注解定义在浏览器的标题或页面标签中显示的内容。对于这两个视图,使用@FrameTitle注解添加框架标题:

MainView.java
@Route("/")
@FrameTitle("客户表")
public class MainView extends Composite<Div> {

private Div self = getBoundComponent();

public MainView(CustomerService customerService) {
}
}

共享CSS

通过在MainViewFormView中可以引用的绑定组件,您可以使用CSS进行样式设置。 您可以使用第一步中的CSS,创建基本应用,为两个视图提供相同的UI容器样式。 在每个视图的绑定组件中添加CSS类名card

MainView.java
@Route("/")
@FrameTitle("客户表")
public class MainView extends Composite<Div> {

private Div self = getBoundComponent();

public MainView() {

self.addClassName("card");
}
}

使用CustomerService

视图的最后一个共享特性是使用CustomerService类。 MainView中的Table显示每个客户,而FormView添加新客户。由于两个视图都与客户数据进行交互,因此它们需要访问应用程序的业务逻辑。

视图通过在处理数据中创建的Spring服务CustomerService访问该服务。要在每个视图中使用Spring服务,请将CustomerService设为构造函数参数:

MainView.java
@Route("/")
@FrameTitle("客户表")
public class MainView extends Composite<Div> {

private Div self = getBoundComponent();

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

创建MainView

在使您的应用可路由、为视图提供Composite组件包装器并包括CustomerService之后,您准备好构建每个视图独特的UI。如前所述,MainView包含在Application中的UI组件。这个类还需要一种方法来导航到FormView

组合Table方法

在将组件从Application移动到MainView的同时,开始对应用程序的部分进行分段,以便一个自定义方法可以一次性更改Table是个好主意。现在对代码进行分段可以在应用程序变得更复杂时使其更易于管理。

现在,您的MainView构造函数应仅调用一个buildTable()方法,该方法添加列、设置大小并引用存储库:

private void buildTable() {
table.setSize("1000px", "294px");
table.setMaxWidth("90vw");
table.addColumn("firstName", Customer::getFirstName).setLabel("名字");
table.addColumn("lastName", Customer::getLastName).setLabel("姓氏");
table.addColumn("company", Customer::getCompany).setLabel("公司");
table.addColumn("country", Customer::getCountry).setLabel("国家");
table.setColumnsToAutoFit();
table.getColumns().forEach(column -> column.setSortable(true));
table.setRepository(customerService.getRepositoryAdapter());
}

用户需要一种通过UI从MainView导航到FormView的方法。

在webforJ中,您可以通过使用视图的类直接导航到新视图。通过类而不是URL段进行路由可以确保webforJ能够找到加载视图的正确路径。

要导航到不同的视图,请使用Router类获取当前位置,使用getCurrent(),然后使用navigate()方法,参数为视图类:

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

这段代码将通过编程将用户发送到新的客户表单,但导航需要连接到用户操作。 要允许用户添加新客户,您可以修改或替换Application中的信息按钮。该按钮可以导航到FormView类,而不是打开消息对话框:

private Button addCustomer = new Button("添加客户", ButtonTheme.PRIMARY,
e -> Router.getCurrent().navigate(FormView.class));

完成的MainView

通过导航到FormView和组合表方法,以下是您在创建FormView之前MainView应该看起来的样子:

MainView.java
@Route("/")
@FrameTitle("客户表")
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("添加客户", 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")

创建FormView

FormView将显示一个表单以添加新客户。对于每个客户属性,FormView将有一个可编辑的组件供用户交互。此外,它还将有一个按钮让用户提交数据,还有一个取消按钮来放弃数据。

创建Customer实例

当用户正在编辑新客户的数据时,只有在他们准备提交表单时,才能将更改应用于存储库。使用Customer对象的实例是一种方便的方式,可以编辑和维护新数据,而无需直接编辑存储库。在FormView中创建一个新的Customer实例以供表单使用:

private Customer customer = new Customer();

为了使Customer实例可编辑,除了id之外的每个属性都应该与一个可编辑的组件相关联。用户在UI中所做的更改应反映在Customer实例中。

添加TextField组件

Customer中的前三个可编辑属性(firstNamelastNamecompany)都是String值,应使用单行文本编辑器表示。TextField组件非常适合表示这些属性。

使用TextField组件,您可以添加标签以及在字段值更改时触发的事件监听器。每个事件监听器应更新Customer实例的相应属性。

添加三个TextField组件以更新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("名字", e -> customer.setFirstName(e.getValue()));
private TextField lastName = new TextField("姓氏", e -> customer.setLastName(e.getValue()));
private TextField company = new TextField("公司", e -> customer.setCompany(e.getValue()));

public FormView(CustomerService customerService) {
this.customerService = customerService;
self.addClassName("card");
}
}
共享命名约定

将组件命名为它们所代表的Customer实体属性,使将来在验证和绑定数据步骤中绑定数据变得更容易。

添加ChoiceBox组件

country属性表示为TextField并不是理想选择,因为该属性只能是五个枚举值之一:UNKNOWNGERMANYENGLANDITALYUSA

选择预定义选项列表的更好组件是ChoiceBox

ChoiceBox组件的每个选项都表示为一个ListItem。每个ListItem有两个值,一个是Object键,另一个是要在UI中显示的String文本。为每个选项提供两个值,可让您在内部处理该Object,同时为用户在UI中提供更易读的选项。

例如,Object键可以是国际标准书号(ISBN),而String文本是书名,更容易理解。

new ListItem(isbn, bookTitle);

但是,在此应用程序中处理的是国家名称列表,而不是书籍。对于每个ListItem,您希望ObjectCustomer.Country枚举,而文本则为其String表示。

要将所有country选项添加到ChoiceBox中,您可以使用迭代器为每个Customer.Country枚举创建一个ListItem,并将它们放入ArrayList<ListItem>中。然后,您可以将该ArrayList<ListItem>插入ChoiceBox组件中:

// 创建ChoiceBox组件
private ChoiceBox country = new ChoiceBox("国家");

// 创建ListItem对象的ArrayList
ArrayList<ListItem> listCountries = new ArrayList<>();

// 添加一个迭代器,为每个Customer.Country选项创建ListItem
for (Country countryItem : Customer.Country.values()) {
listCountries.add(new ListItem(countryItem, countryItem.toString()));
}

// 将填充的ArrayList插入ChoiceBox
country.insert(listCountries);

// 使表单加载时第一个ListItem成为默认选项
country.selectIndex(0);

然后,当用户在ChoiceBox中选择一个选项时,Customer实例应使用所选项的键进行更新,这个键是一个Customer.Country值。

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

为了保持代码整洁,创建ArrayList<ListItem>并将其添加到ChoiceBox的迭代器应放在一个单独的方法中。 在添加允许用户选择country属性的ChoiceBox后,FormView应如下所示:

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("名字", e -> customer.setFirstName(e.getValue()));
private TextField lastName = new TextField("姓氏", e -> customer.setLastName(e.getValue()));
private TextField company = new TextField("公司", e -> customer.setCompany(e.getValue()));

private ChoiceBox country = new ChoiceBox("国家",
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);
}

}

添加Button组件

使用新客户表单时,用户应该能够保存或放弃他们的更改。 创建两个Button组件以实现此功能:

private Button submit = new Button("提交");
private Button cancel = new Button("取消");

提交和取消按钮都应使用户返回MainView。 这让用户可以立即查看他们的操作结果,无论是看到表格中的新客户还是它保持不变。 由于FormView中的多个输入将用户带到MainView,因此导航应放在一个可以调用的方法中:

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

取消按钮

放弃表单中的更改不需要额外的事件代码,只需返回MainView。但是,由于取消不是主要操作,将按钮的主题设置为轮廓样式可以使提交按钮更突出。 按钮组件页面中的主题部分列出了所有可用主题。

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

提交按钮

当用户按下提交按钮时,Customer实例中的值应被用来在存储库中创建新的条目。

使用CustomerService,您可以使用Customer实例来更新H2数据库。当执行此操作时,给该Customer分配一个新的唯一id。更新存储库后,您可以将用户重定向到MainView,他们可以在表格中看到新的客户。

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

//...

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

使用ColumnsLayout

通过添加TextFieldChoiceBoxButton组件,您现在拥有了表单的所有交互部分。 此步骤中FormView的最后一个改进是将这六个组件进行视觉上的组织。

该表单可以使用ColumnsLayout将组件分成两列,而无需设置任何交互组件的宽度。 要创建ColumnsLayout,请指定应在布局内部的每个组件:

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

要设置ColumnsLayout的列数,请使用Breakpoint对象的List。每个Breakpoint告诉ColumnsLayout根据最小宽度应用指定的列数。通过使用ColumnsLayout,您可以创建一个表单,只有在屏幕足够宽以显示两列时才会显示两列。在较小的屏幕上,组件将显示在单一列中。

ColumnsLayout文章中的断点部分更详细地解释了断点。

为了保持代码可维护性,将断点设置在单独的方法中。在该方法中,您还可以使用setSpacing()方法控制组件之间的水平和垂直间距。

private void setColumnsLayout() {

// 在宽度大于600px时有两列
List<Breakpoint> breakpoints = List.of(
new Breakpoint(600, 2));

// 添加断点列表
layout.setBreakpoints(breakpoints);

// 设置组件之间的间距,使用DWC CSS变量
layout.setSpacing("var(--dwc-space-l)")
}

最后,您可以将新创建的ColumnsLayout添加到FormView的绑定组件中,同时设置最大宽度,并添加之前的类名:

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

完成的FormView

在添加了Customer实例、交互组件和ColumnsLayout之后,您的FormView应如下所示:

FormView.java
@Route("customer")
@FrameTitle("客户表单")
public class FormView extends Composite<Div> {
private final CustomerService customerService;
private Customer customer = new Customer();
private Div self = getBoundComponent();
private TextField firstName = new TextField("名字", e -> customer.setFirstName(e.getValue()));
private TextField lastName = new TextField("姓氏", e -> customer.setLastName(e.getValue()));
private TextField company = new TextField("公司", e -> customer.setCompany(e.getValue()));
private ChoiceBox country = new ChoiceBox("国家",
e -> customer.setCountry((Customer.Country) e.getSelectedItem().getKey()));
private Button submit = new Button("提交", ButtonTheme.PRIMARY, e -> submitCustomer());
private Button cancel = new Button("取消", ButtonTheme.OUTLINED_PRIMARY, e -> navigateToMain());
private ColumnsLayout layout = new ColumnsLayout(
firstName, lastName,

下一步

由于用户现在可以添加客户,因此您的应用程序也应该能够使用相同的表单编辑现有客户。在下一步中,观察者和路由参数,您将允许客户id作为FormView的初始参数,从而填充该客户的数据表单并允许用户更改属性。