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:

my onboarding

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 (suspend) in the main toolbar.

In the Jmix tool window, click New (add) → View:

create screen 1

In the Create Jmix View window, select the Blank view template:

create screen 2

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

create screen 3

Click Next.

On the next step of the wizard, change the view title to My onboarding:

create screen 4

Click Create.

Studio will create an empty view and open it in the designer:

create screen 5

The new view will also be added to the main menu. Double-click on the User InterfaceMain Menu item in the Jmix tool window and switch to the Structure tab. Drag and drop MyOnboardingView to the top:

create screen 6

Run the application by clicking the Debug button (start debugger) 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:

data container 1

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 hierarchy 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 handlersBeforeShowEvent:

data container 3

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:

data container 4

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 DataLoadCoordinator facet:

<facets>
    <dataLoadCoordinator auto="true"/>
</facets>

That’s why you didn’t invoke load() method of data loaders in the CRUD views created in previous chapters.

Setting up Data Grid

Right-click the layout element of the Jmix UI hierarchy 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:

table 1

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:

table 2

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:

table 3

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:

gen column 1

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 LayoutsVBox (vertical box) to the layout element of the Jmix UI hierarchy panel before userStepsDataGrid. Then add three HTMLSpan 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:

  1. Invoke updateLabels() from the existing BeforeShowEvent handler:

    @Subscribe
    public void onBeforeShow(final BeforeShowEvent event) {
        // ...
        updateLabels();
    }

    So the labels will be updated on screen opening.

  2. Click Generate Handler and select Data container handlersuserStepsDcItemPropertyChangeEvent:

    label 2
  3. 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 their completedDate 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:

label 3

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 LayoutsHBox (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:

buttons 3

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:

theme 1

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 InterfaceThemes 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:

theme 4

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 base View 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.