Routing and Composites
到目前为止,本教程仅仅是一个单页应用程序。这一步改变了这一点。 您将把处理数据中创建的UI移动到其自己的页面,并创建另一个页面用于添加新客户。 然后,您将连接这些页面,使您的应用能够通过应用这些概念在它们之间导航:
完成此步骤后,您将创建一个3-routing-and-composites的版本。
运行应用程序
在开发应用程序时,您可以使用3-routing-and-composites作为比较。要查看应用程序的运行情况:
-
导航到包含
pom.xml文件的顶级目录;如果您按照GitHub上的版本进行操作,那么就是3-routing-and-composites。 -
使用以下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文件应如下所示:
@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()方法
}
将@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路由,MainView和FormView,但它没有特定的URL来加载它们。使用@Route注解在视图类上,您可以告诉webforJ在给定的URL段上加载它。例如,使用@Route("about")在视图上将类本地映射到http://localhost:8080/about。
顾名思义,MainView是您希望在应用程序运行时最初加载的类。为此,添加一个@Route注解,将MainView映射到您应用程序的根URL:
@Route("/")
public class MainView {
public MainView() {
}
}
对于FormView,将视图映射,使其在用户访问http://localhost:8080/customer时加载:
@Route("customer")
public class FormView {
public FormView() {
}
}
如果您没有为@Route注解明确分配值,则URL段为类名转换为小写,去掉View后缀。
MainView将映射到/mainFormView将映射到/form
共享特性
除了都是视图路由外,MainView和FormView还具有额外的特性。一些共享特性,例如使用Composite组件,是使用webforJ应用程序的基础,而另一些只是使管理您的应用程序更容易。
使用Composite组件
当应用程序是单页时,您将组件存储在一个Frame中。向前看,随着应用程序显示多个视图,您需要将这些UI组件包装在Composite组件内部。
Composite组件是包装器,可以轻松创建可重用组件。
要创建Composite组件,扩展Composite类,并指定一个作为该类基础的绑定组件,例如Composite<FlexLayout>。
本教程使用Div元素作为绑定组件,但它们可以是任何组件,例如FlexLayout或AppLayout。使用getBoundComponent()方法,您可以引用绑定组件并访问其方法。这让您可以设置大小、添加CSS类名、添加要在Composite组件中显示的组件,并访问特定于组件的方法。
对于MainView和FormView,将Composite与Div扩展为绑定组件。然后,引用该绑定组件,以便您可以稍后添加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
- FormView
@Route("/")
@FrameTitle("客户表")
public class MainView extends Composite<Div> {
private Div self = getBoundComponent();
public MainView(CustomerService customerService) {
}
}
@Route("customer")
@FrameTitle("客户表单")
public class FormView extends Composite<Div> {
private Div self = getBoundComponent();
public FormView(CustomerService customerService) {
}
}
共享CSS
通过在MainView和FormView中可以引用的绑定组件,您可以使用CSS进行样式设置。
您可以使用第一步中的CSS,创建基本应用,为两个视图提供相同的UI容器样式。
在每个视图的绑定组件中添加CSS类名card:
- MainView
- FormView
@Route("/")
@FrameTitle("客户表")
public class MainView extends Composite<Div> {
private Div self = getBoundComponent();
public MainView() {
self.addClassName("card");
}
}
@Route("customer")
@FrameTitle("客户表单")
public class FormView extends Composite<Div> {
private Div self = getBoundComponent();
public FormView() {
self.addClassName("card");
}
}
使用CustomerService
视图的最后一个共享特性是使用CustomerService类。
MainView中的Table显示每个客户,而FormView添加新客户。由于两个视图都与客户数据进行交互,因此它们需要访问应用程序的业务逻辑。
视图通过在处理数据中创建的Spring服务CustomerService访问该服务。要在每个视图中使用Spring服务,请将CustomerService设为构造函数参数:
- MainView
- FormView
@Route("/")
@FrameTitle("客户表")
public class MainView extends Composite<Div> {
private Div self = getBoundComponent();
public MainView(CustomerService customerService) {
this.customerService = customerService;
self.addClassName("card");
}
}
@Route("customer")
@FrameTitle("客户表单")
public class FormView extends Composite<Div> {
private Div self = getBoundComponent();
public FormView(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());
}
导航到FormView
用户需要一种通过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应该看起来的样子:
创建FormView
FormView将显示一个表单以添加新客户。对于每个客户属性,FormView将有一个可编辑的组件供用户交互。此外,它还将有一个按钮让用户提交数据,还有一个取消按钮来放弃数据。
创建Customer实例
当用户正在编辑新客户的数据时,只有在他们准备提交表单时,才能将更改应用于存储库。使用Customer对象的实例是一种方便的方式,可以编辑和维护新数据,而无需直接编辑存储库。在FormView中创建一个新的Customer实例以供表单使用:
private Customer customer = new Customer();
为了使Customer实例可编辑,除了id之外的每个属性都应该与一个可编辑的组件相关联。用户在UI中所做的更改应反映在Customer实例中。
添加TextField组件
Customer中的前三个可编辑属性(firstName、lastName和company)都是String值,应使用单行文本编辑器表示。TextField组件非常适合表示这些属性。
使用TextField组件,您可以添加标签以及在字段值更改时触发的事件监听器。每个事件监听器应更新Customer实例的相应属性。
添加三个TextField组件以更新Customer实例:
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并不是理想选择,因为该属性只能是五个枚举值之一:UNKNOWN、GERMANY、ENGLAND、ITALY和USA。
选择预定义选项列表的更好组件是ChoiceBox。
ChoiceBox组件的每个选项都表示为一个ListItem。每个ListItem有两个值,一个是Object键,另一个是要在UI中显示的String文本。为每个选项提供两个值,可让您在内部处理该Object,同时为用户在UI中提供更易读的选项。
例如,Object键可以是国际标准书号(ISBN),而String文本是书名,更容易理解。
new ListItem(isbn, bookTitle);
但是,在此应用程序中处理的是国家名称列表,而不是书籍。对于每个ListItem,您希望Object为Customer.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应如下所示:
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
通过添加TextField、ChoiceBox和Button组件,您现在拥有了表单的所有交互部分。
此步骤中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应如下所示:
下一步
由于用户现在可以添加客户,因此您的应用程序也应该能够使用相同的表单编辑现有客户。在下一步中,观察者和路由参数,您将允许客户id作为FormView的初始参数,从而填充该客户的数据表单并允许用户更改属性。