7. Creating UI from Scratch
At this stage, the application has everything in place for Administrators and HR Managers: they can set up departments and onboarding steps, manage users, generate and track onboarding steps for each user.
Now you need to create UI for users to manage their own onboarding process. A user should be able to enter the system and open the My Onboarding
view which shows their onboarding steps. Each step can be marked as done by selecting a checkbox. Overdue steps should be highlighted.
Below is a mockup of the My Onboarding
view:
Previously, you created UI by generating and modifying CRUD views for entities. In this chapter, you will create the My Onboarding
view from scratch.
Creating Blank View
If your application is running, stop it using the Stop button () in the main toolbar.
In the Jmix tool window, click New () → View:
In the Create Jmix View window, select the Blank view
template:
Click Next.
On the next step of the wizard, enter:
-
Descriptor name:
my-onboarding-view
-
Controller name:
MyOnboardingView
-
Package name:
com.company.onboarding.view.myonboarding
Click Next.
On the next step of the wizard, change the view title to My onboarding
:
Click Create.
Studio will create an empty view and open it in the designer:
The new view will also be added to the main menu. Double-click on the User Interface → Main Menu item in the Jmix tool window and switch to the Structure tab. Drag and drop MyOnboardingView
to the top:
Run the application by clicking the Debug button () in the main toolbar. Open http://localhost:8080
in your web browser and log in to the application.
Click on the My onboarding
item in the Application
menu and make sure your empty view is opened.
Adding Data Grid
Let’s begin with adding to the view a data grid displaying onboarding steps of the current user.
Defining Data Container
First, add a data container which will provide a collection of UserStep
entities for the data grid. Click Add Component in the actions panel, select the Data components
section, and double-click the Collection
item. In the Data Container Properties Editor window, select UserStep
in the Entity field and click OK:
Studio will create the collection container:
<data>
<collection id="userStepsDc" class="com.company.onboarding.entity.UserStep">
<fetchPlan extends="_base"/>
<loader id="userStepsDl" readOnly="true">
<query>
<![CDATA[select e from UserStep e]]>
</query>
</loader>
</collection>
</data>
Loading Data
First of all, remove the readOnly="true"
attribute from the generated loader, because we are going to modify and save the entities shown in this view. You can do it in the component inspector or right in the XML:
<loader id="userStepsDl">
<query>
<![CDATA[select e from UserStep e]]>
</query>
</loader>
The default query will load all UserStep
instances, but you need to select only steps of the current user and in a particular order. Let’s modify the query using JPQL Query Designer. Select userStepsDc
container in the Jmix UI structure panel and click the value of the query
attribute. Then add a where
clause by user
attribute with the :user
parameter and an order by
clause by sortValue
attribute.
The resulting query should be as below:
<query>
<![CDATA[select e from UserStep e
where e.user = :user
order by e.sortValue asc]]>
</query>
The next task is to provide a value for the :user
parameter. You can do it in the BeforeShowEvent
handler. Switch to the MyOnboardingView
controller class, click Generate Handler button in the top actions panel and select Controller handlers
→ BeforeShowEvent
:
Click OK. Studio will generate a handler method stub:
@Route(value = "MyOnboardingView", layout = MainView.class)
@ViewController("MyOnboardingView")
@ViewDescriptor("my-onboarding-view.xml")
public class MyOnboardingView extends StandardView {
@Subscribe
public void onBeforeShow(final BeforeShowEvent event) {
}
}
Now you need to get the currently logged-in user and set it to the loader’s query parameter.
Click Code Snippets in the actions panel to generate code for obtaining the current user:
Then inject the userStepsDl
loader using the Inject button in the actions panel, set the :user
parameter to the current user and invoke its load()
method to execute the query and load data into the collection container.
The resulting code for loading data into the collection container:
@Autowired
private CurrentAuthentication currentAuthentication;
@ViewComponent
private CollectionLoader<UserStep> userStepsDl;
@Subscribe
public void onBeforeShow(final BeforeShowEvent event) {
final User user = (User) currentAuthentication.getUser();
userStepsDl.setParameter("user", user);
userStepsDl.load();
}
In entity list and detail views generated by Studio, data loading is by default triggered by the
That’s why you didn’t invoke |
Setting up Data Grid
Right-click the layout
element of the Jmix UI structure panel and select the Add Component item in the context menu. Find and double-click the DataGrid
component. Select userStepsDc
data container in the DataGrid Properties Editor dialog:
Click OK.
As you can see, the created data grid doesn’t have a column for displaying the Step name:
<dataGrid id="userStepsDataGrid" dataContainer="userStepsDc" width="100%">
<columns>
<column property="dueDate"/>
<column property="completedDate"/>
<column property="sortValue"/>
</columns>
</dataGrid>
Step
is a reference attribute and is not included in the fetch plan and data grid by default. You already saw this situation in the previous chapter when displayed a data grid of UserSteps in the User detail view.
Add the step
attribute to fetch plan, then add a column for it to the data grid and remove the unneeded sortValue
column:
At this stage, the view XML should be as below:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<view xmlns="http://jmix.io/schema/flowui/view"
title="msg://myOnboardingView.title">
<data>
<collection id="userStepsDc" class="com.company.onboarding.entity.UserStep">
<fetchPlan extends="_base">
<property name="step" fetchPlan="_base"/>
</fetchPlan>
<loader id="userStepsDl">
<query>
<![CDATA[select e from UserStep e
where e.user = :user
order by e.sortValue asc]]>
</query>
</loader>
</collection>
</data>
<layout>
<dataGrid id="userStepsDataGrid" dataContainer="userStepsDc" width="100%">
<columns>
<column property="step.name"/>
<column property="dueDate"/>
<column property="completedDate"/>
</columns>
</dataGrid>
</layout>
</view>
Press Ctrl/Cmd+S and switch to the running application. Make sure your current user (perhaps it’s admin
) has a few UserSteps generated in the User detail view. Refresh My onboarding
view and see your onboarding steps:
Adding Component Column
In this section, you will add a column with checkboxes to mark onboarding steps done. You already did this before for the UserSteps table in the User edit screen.
In the XML descriptor, add the completed
column:
<columns>
<column key="completed" sortable="false" width="4em" flexGrow="0"/>
In the controller, inject the UiComponents
factory. Generate renderer
handler for the completed
column and implement it as follows:
@Autowired
private UiComponents uiComponents;
@Supply(to = "userStepsDataGrid.completed", subject = "renderer")
private Renderer<UserStep> userStepsDataGridCompletedRenderer() {
return new ComponentRenderer<>(userStep -> {
Checkbox checkbox = uiComponents.create(Checkbox.class);
checkbox.setValue(userStep.getCompletedDate() != null);
checkbox.addValueChangeListener(e -> {
if (userStep.getCompletedDate() == null) {
userStep.setCompletedDate(LocalDate.now());
} else {
userStep.setCompletedDate(null);
}
});
return checkbox;
});
}
Press Ctrl/Cmd+S and switch to the running application. Refresh My onboarding
view and test your latest changes:
Adding Labels
The data grid is almost ready, now let’s add the labels displaying counters of total, completed and overdue steps.
Click Add Component in the actions panel and drag and drop Layouts
→ VBox
(vertical box) to the layout
element of the Jmix UI structure panel before userStepsDataGrid
. Then add three HTML
→ Span
components into vbox
.
Set the label identifiers as below:
<layout>
<vbox>
<span id="totalStepsLabel"/>
<span id="completedStepsLabel"/>
<span id="overdueStepsLabel"/>
</vbox>
Now you need to calculate and set their values programmatically in the controller. Switch to the MyOnboardingView
controller, inject the labels and userStepsDc
collection container:
@ViewComponent
private Span completedStepsLabel;
@ViewComponent
private Span overdueStepsLabel;
@ViewComponent
private Span totalStepsLabel;
@ViewComponent
private CollectionContainer<UserStep> userStepsDc;
Then add a couple of methods for calculating and assigning counters:
private void updateLabels() {
totalStepsLabel.setText("Total steps: " + userStepsDc.getItems().size());
long completedCount = userStepsDc.getItems().stream()
.filter(us -> us.getCompletedDate() != null)
.count();
completedStepsLabel.setText("Completed steps: " + completedCount);
long overdueCount = userStepsDc.getItems().stream()
.filter(us -> isOverdue(us))
.count();
overdueStepsLabel.setText("Overdue steps: " + overdueCount);
}
private boolean isOverdue(UserStep us) {
return us.getCompletedDate() == null
&& us.getDueDate() != null
&& us.getDueDate().isBefore(LocalDate.now());
}
Finally, invoke the updateLabels()
method from two event handlers:
-
Invoke
updateLabels()
from the existingBeforeShowEvent
handler:@Subscribe public void onBeforeShow(final BeforeShowEvent event) { // ... updateLabels(); }
So the labels will be updated on screen opening.
-
Click Generate Handler and select
Data container handlers
→userStepsDc
→ItemPropertyChangeEvent
: -
Invoke the
updateLabels()
method from the handler you just generated:@Subscribe(id = "userStepsDc", target = Target.DATA_CONTAINER) public void onUserStepsDcItemPropertyChange(final InstanceContainer.ItemPropertyChangeEvent<UserStep> event) { updateLabels(); }
With the help of the
ItemPropertyChangeEvent
handler, the labels will be updated when you change theircompletedDate
attribute using the checkboxes in the table.
Press Ctrl/Cmd+S and switch to the running application. Refresh My onboarding
view and test the label values:
Saving Changes and Closing View
Now you can change the state of the onboarding steps but the changes are lost if you reopen the view. Let’s add the Save
button to save and close the view and the Discard
button to close without saving.
First, click Add Component in the actions panel, drag Layouts
→ HBox
(horizontal box) and drop it below userStepsDataGrid
. Then add two buttons into it.
Set the button ids and titles as shown below. For the Save
button, set primary
in the themeNames
attribute:
<hbox>
<button id="saveButton" text="Save" themeNames="primary"/>
<button id="discardButton" text="Discard"/>
</hbox>
Generate button click handlers using the Jmix UI inspector panel → Handlers tab.
Inject DataContext
into controller class and implement click handlers:
@ViewComponent
private DataContext dataContext;
@Subscribe(id = "saveButton", subject = "clickListener")
public void onSaveButtonClick(final ClickEvent<JmixButton> event) {
dataContext.save(); (1)
close(StandardOutcome.SAVE); (2)
}
@Subscribe(id = "discardButton", subject = "clickListener")
public void onDiscardButtonClick(final ClickEvent<JmixButton> event) {
close(StandardOutcome.DISCARD); (2)
}
1 | DataContext tracks changes in entities loaded into data containers. When you invoke its save() method, all changed instances are saved to the database. |
2 | The close() method closes the view. It accepts an "outcome" object that can be analysed by the calling code. |
Press Ctrl/Cmd+S and switch to the running application. Refresh My onboarding
view and see the buttons in action:
Styling Data Grid
The last requirement for My onboarding
view is to highlight overdue steps by changing font color of the Due date
cells. You will do it by writing some CSS and using it in the data grid.
First, assign the onboarding-steps
class to the data grid by adding it to the classNames
property:
Then select dueDate
column, switch to Handlers tab of the component inspector and generate the partNameGenerator
handler for the column. Implement it as follows:
@Install(to = "userStepsDataGrid.dueDate", subject = "partNameGenerator")
private String userStepsDataGridDueDatePartNameGenerator(final UserStep userStep) {
return isOverdue(userStep) ? "overdue-step" : null;
}
The handler accepts UserStep instance of the rendered row and returns a name to use in a CSS selector for this column.
Finally, open onboarding.css
file from the User Interface → Themes section of the Jmix tool window and add the following CSS code:
vaadin-grid.onboarding-steps::part(overdue-step) {
color: red;
}
In the selector above, vaadin-grid.onboarding-steps
specifies the particular data grid instance, and ::part(overdue-step)
points to the cells that need to be highlighted.
Press Ctrl/Cmd+S and switch to the running application. Refresh My onboarding
view and test the style for overdue steps:
Summary
In this section, you have developed from scratch an entire view working with data.
You have learned that:
-
Query of a data loader can contain parameters. Parameter values can be set in a
BeforeShowEvent
handler or any other view or UI component event handler. -
To trigger data loading, you should either invoke the
load()
method of the data loader in an event handler, or add the dataLoadCoordinator facet to the screen. -
vbox and hbox layouts are used to place UI components vertically or horizontally. The root
layout
is itself a vertical box. -
The
save()
method of DataContext saves all changed entities to the database. -
A view can be closed programmatically by the
close()
method provided by the baseView
class. -
The CSS file located in the project theme can define styles to be used by UI components.
-
The
partNameGenerator
handler should be used to change the style of a table cell. -
The Code Snippets palette can be used to quickly find and generate code working with the framework API.