跳至主要内容

Routing and Composites

在 ChatGPT 中打开

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

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

运行应用

在开发您的应用时,您可以使用3-routing-and-composites作为参考。要查看应用的实际运行:

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

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

    mvn

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

可路由的应用

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

可路由的应用是基于URL渲染用户界面的。通过在扩展App类的类上注释@Routify来启用路由,packages元素告诉webforJ哪些包包含用户界面组件。

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

Application.java
@SpringBootApplication
@StyleSheet("ws://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

Application中保留@StyleSheet注释会使该CSS全局应用。

创建路由

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

第一个路由类型是View。这种类型的路由直接映射到应用中的特定URL段。表格和新客户表单的用户界面都将是View路由。

第二个路由类型是Layout,它包含在多个页面上显示的用户界面,例如标题或侧边栏。布局路由也会包装子视图,而不会对URL产生影响。

要指定类的路由类型,请将路由类型作为后缀附加到类名的末尾。 例如,MainViewView路由类型。

为了将应用的两个功能分开,您的应用需要将用户界面映射到两个唯一的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内部。向前发展,随着多个视图的应用,您需要将这些用户界面组件包装在Composite组件内。

Composite组件是包装器,使创建可重用组件变得容易。 要创建一个Composite组件,扩展Composite类并指定一个作为类基础的绑定组件,例如Composite<FlexLayout>

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

对于MainViewFormView,用Div扩展Composite作为绑定组件。然后,引用该绑定组件,以便稍后添加用户界面。这两个视图的结构应与以下类似:

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

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

// 创建组件用户界面
private Button submit = new Button("Submit");

public MainView() {

// 将用户界面组件添加到绑定组件中
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为两个视图提供相同的用户界面容器样式。 在每个视图中的绑定组件上添加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之后,您准备好构建每个视图特有的用户界面。如上所述,MainView包含最初在Application中的用户界面组件。此类还需要能够导航到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());
}

用户需要一种从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的导航和分组表格方法,以下是MainView在继续创建FormView之前的样子:

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。不过,由于取消不是主要操作,将按钮的主题设置为轮廓状可以让提交按钮更加突出。 Button组件页面的主题部分列出了所有可用主题。

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()方法控制ColumnsLayout内部组件之间的水平和垂直间距。

private void setColumnsLayout() {

// 如果宽度超过600px,则在ColumnsLayout中有两列
List<Breakpoint> breakpoints = List.of(
new Breakpoint(600, 2));

// 添加断点的List
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的初始参数,以便能够使用该客户的数据填充表单并允许用户更改属性。