Responsive Design in ZK Part 3

From Documentation

Introduction

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! CHANGE LINK TO PART 2 BEFORE RELEASE ! DON'T REMOVE WARNING BEFORE LINK IS UPDATED

In continuation of Responsive Design in ZK Part 1 and Part 2, 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

Assuming 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 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 (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 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.

responsive design slides

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.

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

<apply viewTemplate="@load(vm.viewTemplate)"	dataModel="@load(vm.dataModel)" templateURI="staticVerticalLayout.zul"/>

git source

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.

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

git source

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)">

git source