Responsive Design in ZK Part 3
Introduction
In continuation of Responsive Design in ZK Part1 and Part2, we will look at a real-world responsive design case in ZK. We will consider the following article from a developer point of view in a mid-size project receiving the task to implement a new page.
Feature request
- Business requirements
-
- Displaying details regarding a set of employees, including employee data from multiple fields.
- The control will use the full width of the page.
- User will be able to trigger [action per employee] from the control
- Data will be formatted as rows containing fields such as: [userId, firstName, age, position, department, deskNumber]
- UI design chart guidelines
- Table-like UI elements should not use horizontal scrolling
- Table-like UI elements should maintain a reasonable minimum column width to maintain readability.
- Any device with a screen width < 400px should use the document level scrollbar only. (no horizontal, no vertical scrollbar inside the page)
- Project constraints
- Project is developed using the MVVM design pattern.
- Data is provided by a different module as a List of beans.
Request analysis
We are tasked to display what is essentially the result of a database query. The obvious solution is to simply to pass the data into a ListModelList, use it as the model for a grid and use databinding and command binding to build the UI.
Each field in the data bean will be bound to a cell label. The cell holding the firstName value will also receive send a command on click to perform [action]. This label will also receive a special style to indicate that it can be clicked.
This will fulfil the Business request, but might conflict with our design chart. Depending on our configuration, the Grid component will display a horizontal scrollbar or resize the columns if the screen size is greatly reduced.
At the same time, on medium-sized screens, the columns will shrink and reduce readability.
To resolve these issues, we consider the following solution:
We decide that any screen larger than 800px will be able to display the full grid.
On screen sizes between 800px and 400px, we will gradually remove columns. To provide the requested information, we will move the deleted columns content to a “details” container which can be opened from the grid view.
On screen sizes smaller than 400px, the grid will be removed and replace by a static template displaying the data as panels. This will improve readability and avoid component-level scrollbars.
We thus define two macro states:
- with Grid (400px and higher)
- with panels (under 400px)
We also divide the “With Grid” state into smaller sub-states:
- “400to500” (1 column)
- “500to600” (2 columns)
- “600to700” (3 columns)
- ”700to800” (4 columns)
- “over800” (full grid)
Implementation
Defining responsive ranges
The first step in this implementation is to defined a ViewModel field which will act as our UI State. To this end, we will use a simple string and update its value to reflect the current state.
private String viewTemplate;
We will then use the @MatchMedia annotation to update the value of this property based on the result of media queries. For this example we will use simple media queries, checking from device width only, but any query following the media query syntax can be used. If the query result is true, the annotated method will be called.
@MatchMedia("all and (min-width: 400px) and (max-width: 499px)") @NotifyChange("viewTemplate") public void handle400to500(@ContextParam(ContextType.TRIGGER_EVENT) ClientInfoEvent event){ viewTemplate="400to500"; }
We avoid overlapping conditions between ranges by ending the range 1px below the start of the next range. In case of range conflict, both methods will be called and the last one called will simply overwrite any previous values.
Building the main page
To switch between two widely different UI structure when swapping between the Grid and the Panel states, we will use the <apply> shadow element. Shadow elements are useful for this purpose since they are dynamically instantiated and destroyed when the ViewModel state change. We create an <apply> element and supply two possible templates: “under400” and “other”. We then use EL Expressions to test the value of the viewTemplate property of the ViewModel. If the property value is “under400”, we set the <apply> element to use the “under400Template”. Inside each template, we simple make a reference to a different zul file for each structure. <apply> elements are used to fetch the content of these zul pages and insert it in the main page.
Building the panel view
The panel view is a simple vertical layout containing a panel for each data entry.
To build a single panel we need the following:
The panel itself is created with a <groupbox> component, and hold the employee firstName field as . The rest of the employee data are added inside another <vlayout> located inside the groupbox. This template will be repeated for each entry in our model. To this end, we use the <forEach> shadow element. This element will repeat a pattern for each entry in a collection. The last step is to organize all panels generated by the forEach loop into a lightweight vertical container. We use the <vlayout> component for this task.
Building the grid view
The grid view requires two specific templates. When the state change, we need to adjust the template used by the content. We also need to adjust the number of column headers and their labels. To adjust the column headers, we use a <choose> <when> structure. This structure allows us to choose between many templates based on the value of a property.
The main rows template can be switch directly using the @template switch in the grid component. When filling the value of the model attribute, we can pass the desired template name for the model based on our dynamic state property. Then, we only need to define templates for each of these states.