ZK8 Wizard Example - Part 2
Robert Wenzel, Engineer, Potix Corporation
July/August 2015
ZK 8.0
Introduction
In the previous Part 1 LINK ME I created a wizard template together with a model. I showed its usage in a trivial case. This Part will focus on reusing the same wizard template in a different scenario with more complex steps. I'll go deeper into templating and reuse various parts of the UI.
I'll also highlight some optional features to give the example more of a real life feeling.
Order Wizard (a more complex example)
As an example I chose a classical Shopping basket and Checkout process with the following steps:
- Basket
- adjust basket (add/remove/change items)
- Shipping Address
- enter shipping address
- Payment
- choose payment method + enter conditional details
- Confirmation
- review data, accept GTC submit order (handle exceptions)
- Feedback
- user feedback when order was successful
Order Model
The order model consists of straight forward java bean classes, to hold the data input during the order process. These classes are unaware of being used in inside a Wizard they simply provide getters and setters to hold/represent their state. (When looking into the code don't be confused by the validation annotations I'll talk about this topic in Part 3 LINK ME.)
Wizard/Step View Models
There are 3 view model classes representing our Ordering process. The OrderViewModel
controls the overall wizard, initializes the steps and eventually the submits the final order. Two of the wizard steps require additional logic which is implemented in BasketViewModel
(adding/removing basket items and display recommendations) and PaymentViewModel
(handle payment method changes).
Creating the UI
The order.zul doesn't contain anything new.
<?component name="wizard" templateURI="/WEB-INF/zul/template/wizard/wizard.zul" ?>
<zk>
<div width="500px"
viewModel="@id('vm') @init('zk.example.order.OrderViewModel')"
validationMessages="@id('vmsgs')"
onBookmarkChange="@command('gotoStep', stepId=event.bookmark)">
<wizard wizardModel="@init(vm.wizardModel)" order="@init(vm.order)"/>
</div>
</zk>
More interesting are the individual step pages.
step 2-4 basket.zul
I'll start with the simpler steps for inputting the order information, as they share some common layout. They all consist of a simple row based layout: one row for each input field.
- /src/main/webapp/WEB-INF/zul/order/steps/shippingAddress.zul
- plain input form no addtional viewmodel required
<?taglib uri="/WEB-INF/tld/i18n.tld" prefix="i18n"?>
<?component name="formRow" templateURI="/WEB-INF/zul/template/wizard/formRow.zul" ?>
<zk xmlns:sh="shadow">
<grid>
<rows>
<formRow type="static" label="@init(i18n:nls('order.basket'))" value="@init(order.basket)"/>
</rows>
</grid>
${i18n:nls('order.shippingAddress.hint')}
<grid>
<rows>
<formRow type="textbox" label="@init(i18n:nls('order.shippingAddress.street'))"
value="@ref(order.shippingAddress.street)"/>
<formRow type="textbox" label="@init(i18n:nls('order.shippingAddress.city'))"
value="@ref(order.shippingAddress.city)"/>
<formRow type="textbox" label="@init(i18n:nls('order.shippingAddress.zipCode'))"
value="@ref(order.shippingAddress.zipCode)"/>
</rows>
</grid>
</zk>
- Line 6: Note: the value passed in statically using @init
- Lines 13, 15, 17: Note: the value is bound by reference using @ref
- /src/main/webapp/WEB-INF/zul/order/steps/shippingAddress.zul
- plain input form no addtional viewmodel required
<?taglib uri="/WEB-INF/tld/i18n.tld" prefix="i18n"?>
<?component name="formRow" templateURI="/WEB-INF/zul/template/wizard/formRow.zul" ?>
<zk xmlns:sh="shadow" xmlns:ca="client/attribute">
<grid>
<rows>
<formRow type="static" label="@init(i18n:nls('order.basket'))" value="@init(order.basket)"/>
<formRow type="static" label="@init(i18n:nls('order.shippingAddress'))" value="@init(order.shippingAddress)"/>
</rows>
</grid>
${i18n:nls('order.payment.hint')}
<grid viewModel="@id('paymentVM') @init('zk.example.order.PaymentViewModel', payment=order.payment)"
payment="@ref(order.payment)">
<rows>
<formRow type="selectbox" label="@init(i18n:nls('order.payment.method'))" value="@ref(payment.method)"
model="@init(paymentVM.availablePaymentMethods)"
updateCommand="@init(paymentVM.paymentMethodUpdateCommand)"/>
<sh:if test="@load(paymentVM.hasCreditCard)">
<formRow type="selectbox" label="@init(i18n:nls('order.payment.creditCard.type'))"
value="@ref(payment.creditCard.type)"
model="@init(paymentVM.availableCreditCards)" />
<formRow type="creditcard" label="@init(i18n:nls('order.payment.creditCard.number'))"
value="@ref(payment.creditCard.number)" />
<formRow type="textbox" label="@init(i18n:nls('order.payment.creditCard.owner'))"
value="@ref(payment.creditCard.owner)" />
</sh:if>
<sh:if test="@load(paymentVM.hasBankAccount)">
<formRow type="textbox" label="@init(i18n:nls('order.payment.bankAccount.iban'))"
value="@ref(payment.bankAccount.iban)" />
<formRow type="textbox" label="@init(i18n:nls('order.payment.bankAccount.bic'))"
value="@ref(payment.bankAccount.bic)" />
</sh:if>
</rows>
</grid>
<template name="creditcard">
<textbox value="@bind(value)" ca:data-mask="${i18n:nls('order.creditCard.number.format')}"/>
</template>
</zk>
- Lines 18, 28: conditional inputs
- Line 15: using model to render the selectbox
- Line 16: passing in a viewmodel command via updateCommand to be notified of value changes
- Lines 22, 37: injecting a custom template/type, to render a special input using an input mask
Every row is rendered using the same formRow template.
Form Row template
The core of this example is the formRow template which renders a form field based on the parameters type, label, value (and some conditional parameters). It uses several shadow components to achieve dynamic row rendering. It tries to reuse as much layout per row as possible (we'll enhance this template with validation messages in PART 3 LINK ME). The templates for input elements require additional template parameters such as model (used for selectbox) and updateCommand.
<zk xmlns:sh="shadow">
<row>
<sh:choose>
<sh:when test="@init(type eq 'checkbox')">
<cell/>
</sh:when>
<sh:otherwise>
<label value="@init(label)"/>
</sh:otherwise>
</sh:choose>
<sh:apply template="@init(type)"/>
</row>
<template name="checkbox">
<checkbox checked="@bind(value)" onCheck="@command(updateCommand)" label="@load(label)"/>
</template>
<template name="textbox">
<textbox value="@bind(value)" onChange="@command(changeCommand)"/>
</template>
<template name="selectbox">
<selectbox selectedItem="@bind(value)" model="@load(model)" onSelect="@command(updateCommand)">
<template name="model">
<label value="@init(i18n:nls(each))"/>
</template>
</selectbox>
</template>
<template name="static">
<label value="@load(value)" />
</template>
<template name="static-bookmark-link">
<a label="@load(value)" href="@init(('#' += bookmark))"/>
</template>
</zk>
- Lines 15, 19, 23: binds the value passed in via @ref from the outside
step 1 basket.zul
WEB-INF/zul/order/steps/basket.zul
The basket-step renders the basket items in a grid - nothing special about it, hence the abbreviated source.
<?taglib uri="/WEB-INF/tld/i18n.tld" prefix="i18n"?>
<zk xmlns:sh="shadow" xmlns:x="xhtml" >
<div viewModel="@id('basketVM') @init('zk.example.order.BasketViewModel', basket=order.basket)">
...
${i18n:nls('order.basket.hint')}
<grid model="@init(basketVM.itemsModel)">
...
<div>
<sh:apply template="basketItemLabel" item="@init(item)"/>
<a iconSclass="z-icon-times" sclass="red" onClick="@command('removeItem', basketItem=item)" tooltiptext="remove"/>
</div>
...
</grid>
<sh:if test="@load(basketVM.hasRecommendations)">
<vlayout>
${i18n:nls('order.basket.recommendation')}
<sh:forEach items="@init(basketVM.recommendedItemsModel)">
<div sclass="recommendation" onClick="@command('addRecommendedItem', item=each)" tooltiptext="add to basket">
<sh:apply template="basketItemLabel" item="@init(each)"/>
<a iconSclass="z-icon-plus green" href="#" />
</div>
</sh:forEach>
</vlayout>
</sh:if>
<template name="basketItemLabel">
<label value="@load(item.label)"/>
<label value="@load(item.unitPrice) @converter(basketVM.priceFormatterParentheses)"/>
</template>
</div>
</zk>
At the bottom some of the new shadow elements (if, foreach, apply) are used to dynamically display the recommendations based on the current basket contents. Here a simple example what happens in the view model.
public class BasketViewModel {
//in the real life application injected using @WireVariable
private RecommendationService recommendationService = new RecommendationService();
private Basket basket;
private ListModelList<BasketItem> basketItemsModel;
private ListModelList<BasketItem> recommendedItemsModel;
...
@Command("addRecommendedItem")
public void addRecommendedItem(@BindingParam("item") BasketItem item) {
basketItemsModel.add(item);
BindUtils.postNotifyChange(null, null, basket, "totalPrice");
loadRecommendations();
}
...
private void loadRecommendations() {
recommendedItemsModel.clear();
recommendedItemsModel.addAll(recommendationService.chooseRecommendations(this.basket));
BindUtils.postNotifyChange(null, null, this, "hasRecommendations");
}
...
Additional features
Input Mask
Bookmarks Handling
Custom I18N
using the same convenience functions in the zul and java code
Download
- The source code for this article can be found in github.
Running the Example
The example consists of a maven web application project. It can be launched with the following command:
mvn jetty:run
Then access the overview page http://localhost:8080/wizardexample/order.zul
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |