Responsive Templating
This article uses concepts introduced in the Fluid design and Adaptive design documentation. Reading these first is recommended if you have not already done so.
In this example, we will look at a real-world responsive design case in ZK. We will consider the following article from a developer's point of view when receiving the task to implement a new responsive page.
Example code available on github
Business Requirements: Building a responsive Grid
We received the following business requirements for this task:
- Displaying details regarding a set of employees, including employee data from multiple fields.
- The control will use the full width of the view container.
- User will be able to trigger an action dependent on the selected 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 (i.e. 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.
Analyzing the Requirements
We are tasked to display what is essentially the result of a database query. The obvious solution is to simply 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 send a command on click which will perform an action on server side. 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 prevent 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
Now we need to select our ranges, build the main page and implement templates for each responsive state. Here we will use ZK MVVM and shadow elements. This combination is a great fit in this case because the model object will be reused between each view states.
View entities generated by Shadow elements will be created on demand, but the model object doesn't need to be recreated when transitioning from one of the responsive states to another. Furthermore, by reusing the same model we can keep any change performed on the model when swapping views.
Lastly, since responsive state transition doesn't require rebuilding the model, we can avoid delays which could be caused by accessing the database layer of our application.
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.
More information on Media Queries git source
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”.
<apply template='@load(vm.viewTemplate eq "under400"?"under400Template":"otherTemplate")'>
<template name="under400Template">
...
</template>
<template name="otherTemplate">
...
</template>
Inside each template, we simply 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, based on the targeted templateURI attribute.
<apply viewTemplate="@load(vm.viewTemplate)" dataModel="@load(vm.dataModel)" templateURI="templates/staticVerticalLayout.zul"/>
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 box title (i.e, caption). The rest of the employee data are added inside another <vlayout> located inside the groupbox.
<groupbox>
<caption label="${each.firstName}"/>
<vlayout>
<label value="User details" />
<hlayout>
<label value="User ID:" />
<label value="${each.userId}" />
</hlayout>
...
</vlayout>
</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.
<forEach items="@load(dataModel)" >
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.
<vlayout>
<forEach items="@load(dataModel)" >
<groupbox>
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.
<columns>
<choose>
<when test='@load(vm.viewTemplate eq "400to500")'>
<column label="Details" />
<column label="First Name" />
</when>
...
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.
<grid model="@load(dataModel) @template(viewTemplate)">