ZK8 Wizard Example - Part 2"

From Documentation
m (correct highlight (via JWB))
 
(70 intermediate revisions by one other user not shown)
Line 1: Line 1:
 
{{Template:Smalltalk_Author|
 
{{Template:Smalltalk_Author|
 
|author=Robert Wenzel, Engineer, Potix Corporation
 
|author=Robert Wenzel, Engineer, Potix Corporation
|date=July/August 2015
+
|date=September 2015
 
|version=ZK 8.0
 
|version=ZK 8.0
 
}}
 
}}
Line 7: Line 7:
 
== Introduction ==
 
== Introduction ==
  
 +
*[[Small_Talks/2015/September/ZK8_Wizard_Example_-_Part_1|Part 1 - Defining the Wizard]]
 +
*[[Small_Talks/2015/September/ZK8_Wizard_Example_-_Part_2|Part 2 - Order Wizard (a more complex example)]] (You are here)
 +
*[[Small_Talks/2016/February/ZK8_Wizard_Example_-_Part_3|Part 3 - Form Handling and Input Validation]]
 +
*[[Small_Talks/2016/April/ZK8_Wizard_Example_-_Part_4_final|Part 4 - Styling the wizard (with Bootstrap)]]
  
== More complex Usage (Order Wizard) ==
+
In the previous Part 1 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 typical shopping basket and checkout process with the following steps:
  
 
#Basket
 
#Basket
#: adjust basket (add/ remove/ change items)
+
#: review/adjust basket (add/remove/change items)
 +
#: display recommendations based on the basket content
 
#Shipping Address  
 
#Shipping Address  
 
#:enter shipping address
 
#:enter shipping address
Line 21: Line 32:
 
#: user feedback when order was successful
 
#: user feedback when order was successful
  
=== Data Model ===
+
Here a preview of the order wizard:
 +
<gflash width="500" height="450">Order-wizard-1.swf</gflash>
 +
 
 +
=== 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'''.)
 +
 
 +
[[File:order_class_dia.png]]
 +
 
 +
=== Wizard/Step View Models ===
 +
 
 +
There are 3 view model classes representing our Ordering process. The <code>OrderViewModel</code> controls the overall wizard, initializes the steps and eventually submits the final order. Two of the wizard steps require additional logic which is implemented in <code>BasketViewModel</code>(adding/removing basket items and display recommendations) and <code>PaymentViewModel</code> (handle payment method changes).
 +
 
 +
[[File:viewmodel_services_class_dia.png|800px]]
 +
 
 +
=== Creating the UI ===
 +
 
 +
The order.zul [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/webapp/order.zul] doesn't contain anything new.
 +
 
 +
<source lang="xml">
 +
<?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>
 +
</source>
 +
 
 +
More interesting are the individual step pages.
 +
 
 +
==== steps 2-4 shipping/payment/confirmation [https://github.com/cor3000/zk-wizard-example/tree/part-2/src/main/webapp/WEB-INF/zul/order/steps] ====
 +
I'll start with the simpler steps for inputting the order information, as they share a common row based layout: one row for each input field.
 +
 
 +
;/src/main/webapp/WEB-INF/zul/order/steps/shippingAddress.zul [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/webapp/WEB-INF/zul/order/steps/shippingAddress.zul]
 +
:plain input form no addtional viewmodel required
 +
<source lang="xml" highlight="6,13,15,17">
 +
<?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>
 +
</source>
 +
* '''Line 6:''' Note: the static value is passed using '''@init'''
 +
* '''Lines 13, 15, 17:''' Note: the editable values use reference binding - '''@ref(...)''' [http://books.zkoss.org/zk-mvvm-book/8.0/syntax/ref.html] - not a new but often overlooked feature
 +
 
 +
 
 +
;/src/main/webapp/WEB-INF/zul/order/steps/payment.zul [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/webapp/WEB-INF/zul/order/steps/payment.zul]
 +
:the payment page uses the '''PaymentViewModel''' [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/java/zk/example/order/PaymentViewModel.java] to control the visibility of conditional input fields for credit card and direct debit
 +
<source lang="xml" highlight="15,16,18,22,28,37">
 +
<?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>
 +
</source>
 +
 
 +
* '''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 optional parameters).
 +
Internally 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 parameters such as '''model''' (used for selectbox) and '''updateCommand''' for @command binding.
 +
 
 +
;/src/main/webapp/WEB-INF/zul/order/steps/payment.zul [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/webapp/WEB-INF/zul/template/wizard/formRow.zul]
 +
 
 +
<source lang="xml" highlight="15, 19, 23">
 +
<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>
 +
</source>
 +
* '''Lines 15, 19, 23:''' binds the value passed in via '''@ref''' from the outside
 +
 
 +
==== step 1 basket.zul ====
 +
 
 +
;/src/main/webapp/WEB-INF/zul/order/steps/basket.zul [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/webapp/WEB-INF/zul/order/steps/basket.zul]
 +
:The first wizard step renders the basket items in a grid - nothing special about it, hence the abbreviated source.
 +
 
 +
<source lang="xml" highlight="9,18,20,27">
 +
<?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>
 +
</source>
 +
* '''Lines 9, 20, 27:''' a simple way to use a basketItemLabel-template in 2 places
 +
* '''Line 18:''' using <code><sh:forEach></code> bound to a ListModelList
 +
 
 +
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. Especially powerful is the combination of forEach and ListModelList. It will automatically expand/shrink/rearrange the items whenever the ListModelList (recommendedItemsModel) is changed.
 +
 
 +
Here a sample of what is required for this in the '''BasketViewModel''' [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/java/zk/example/order/BasketViewModel.java]:
 +
 
 +
<source lang="java" highlight="8,16,23">
 +
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;
 +
 
 +
...
  
zk.example.order.api.Order
+
@Command("addRecommendedItem")
 +
public void addRecommendedItem(@BindingParam("item") BasketItem item) {
 +
basketItemsModel.add(item);
 +
BindUtils.postNotifyChange(null, null, basket, "totalPrice");
 +
loadRecommendations();
 +
}
 +
 +
...
  
*Order
+
private void loadRecommendations() {
** Basket
+
recommendedItemsModel.clear();
*** BasketItem (list of)
+
recommendedItemsModel.addAll(recommendationService.chooseRecommendations(this.basket));
** Payment (payment method)
+
BindUtils.postNotifyChange(null, null, this, "hasRecommendations");
*** CreditCard (based on payment method)
+
}
*** or BackAccount  (based on payment method)
+
...
** ShippingAddress (city, street, zip code)
+
</source>
 +
* '''Line 8:''' the view model holds a <javadoc>org.zkoss.zul.ListModelList</javadoc> to contain the basket recommendations
 +
* '''Line 16:''' whenever the basket is updated (e.g. an item is added) reload the recommendations
 +
* '''Line 23:''' by changing the contents of the ListModelList the bound <forEach> element does it's job automatically
  
=== Form Row template ===
+
It is not necessary to clear and replace all items - forEach can also handle single item additions/removals/movements, and will only reflect those changes in the component tree to reduce the overhead. This example simply illustrates, that it happens automatically, no additional '''notifyChange''' on the '''recommendedItemsModel''' is required to reflect the updated recommendations in the page.
  
 
=== Additional features ===
 
=== Additional features ===
 +
 +
As in a real application there are always requirements that somehow "distract" from the core solution. I just picked a few to show they don't necessarily break the overall design.
  
 
==== Input Mask ====
 
==== Input Mask ====
==== Bookmarks Handling ====
+
 
 +
The payment.zul [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/webapp/WEB-INF/zul/order/steps/payment.zul] page uses an input mask to format the credit card number during user input. These kind of client side 'effects' can now be applied easily with [[ZUML_Reference/ZUML/Namespaces/Client_Attribute#Data-Attribute_Handler|custom data-handlers (ZK 8)]] ([https://github.com/zkoss/zk8-datahandler more data handler examples] can be found in our growing example repository).
 +
 
 +
<source lang="xml">
 +
<template name="creditcard">
 +
    <textbox value="@bind(value)" ca:data-mask="${i18n:nls('order.creditCard.number.format')}"/>
 +
</template>
 +
</source>
 +
 
 +
In the zk.xml [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/webapp/WEB-INF/zk.xml] the handler is configured as follows. It is loading an external jquery plugin (which also be a JS resource inside the webapp), and decorates the widget with the JS code during initialization on the client side. The server side component behaves as usual receiving the unmasked value while the view model isn't even aware of that.
 +
 
 +
<source lang="xml">
 +
<data-handler>
 +
<name>mask</name><!-- the attribute name, i.e. data-mask -->
 +
<script src="http://igorescobar.github.io/jQuery-Mask-Plugin/js/jquery.mask.min.js"/>
 +
<script>
 +
function (wgt, dataValue) {
 +
jq(wgt.$n()).mask(dataValue);
 +
wgt.listen({
 +
onChange: function (event) {
 +
event.data.value = jq(this.$n()).cleanVal();
 +
}
 +
});
 +
}
 +
</script>
 +
</data-handler>
 +
</source>
 +
 
 +
==== Bookmark Handling ====
 +
 
 +
A simple [[ZK_Developer's_Reference/UI_Patterns/Browser_History_Management|bookmarking mechanism]] is added to enable browser back navigation without leaving the wizard, nothing new but often forgotten while very simple to implement.
 +
 
 +
;/wizardexample/src/main/webapp/order.zul [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/webapp/order.zul]
 +
:when a bookmark change is detected send the 'gotoPage' command
 +
 
 +
<source lang="java" highlight="6">
 +
<?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>
 +
</source>
 +
 
 +
;zk.example.order.OrderViewModel [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/java/zk/example/order/OrderViewModel.java]
 +
: whenever the wizard step changes set a bookmark, and vice versa, goto a step when the bookmark changes
 +
<source lang="java" highlight="4,10">
 +
wizardModel = new WizardViewModel<WizardStep>(availableSteps) {
 +
@Override
 +
protected void onStepChanged(WizardStep currentStep) {
 +
bookmark.set(currentStep.getId(), false);
 +
}
 +
};
 +
...
 +
@Command("gotoStep")
 +
public void gotoStep(@BindingParam("stepId") String stepId) {
 +
if(!getWizardModel().gotoStep(stepId)) {
 +
//if step change unsuccessful override the bookmark
 +
bookmark.set(wizardModel.getCurrentStep().getId(), true);
 +
};
 +
}
 +
</source>
 +
 
 +
;zk.example.wizard.Bookmark [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/java/zk/example/wizard/Bookmark.java]
 +
:for completeness the trivial Bookmark class
 +
 
 +
<source lang="java">
 +
public class Bookmark {
 +
public void set(String currentStepId, boolean replaceBookmark) {
 +
Executions.getCurrent().getDesktop().setBookmark(currentStepId, replaceBookmark);
 +
}
 +
}
 +
</source>
 +
 
 
==== Custom I18N ====
 
==== Custom I18N ====
using the same convenience functions in the zul and java code
 
  
= Download =
+
To enable I18N the labels have been defined in the [[ZK_Developer's_Reference/Internationalization/Labels#Internationalization_Labels|zk-label.properties]] and a custom mechanism is put place to conveniently convert enumerations to labels, icons or styles, in the same way as resolving plain simple labels.
* The source code for this article can be found in [https://github.com/cor3000/zk-wizardexample github].
+
 
 +
;/wizardexample/src/main/webapp/WEB-INF/zk-label.properties [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/webapp/WEB-INF/zk-label.properties]
 +
:defines the labels, enables adding localized versions e.g. zk-label_de.properties (see: [[ZK_Developer's_Reference/Internationalization/Labels | documentation]])
 +
 
 +
;zk.example.i18n.NlsFunctions [https://github.com/cor3000/zk-wizard-example/blob/part-2/src/main/java/zk/example/i18n/NlsFunctions.java]
 +
:a set of I18n functions used throughout the application, both in zul and java code
 +
 
 +
Used via [[ZUML_Reference/ZUML/Processing_Instructions/taglib/Custom_Taglib|custom taglib]] functions in a zul file:
 +
<source lang="xml">
 +
<?taglib uri="/WEB-INF/tld/i18n.tld" prefix="i18n"?>
 +
...
 +
${i18n:nls('order.shippingAddress.hint')}
 +
...
 +
<formRow type="static" label="@init(i18n:nls('order.basket'))" value="@init(order.basket)"/>
 +
...
 +
</source>
 +
 
 +
Called directly in java code:
 +
<source lang="java">
 +
DecimalFormat decimalFormat = new DecimalFormat(NlsFunctions.nls("order.price.format"));
 +
return NlsFunctions.nlsArgs("order.basket.format", getTotalItems(), decimalFormat.format(getTotalPrice()));
 +
</source>
 +
 
 +
=Summary =
 +
 
 +
As shown above I almost didn't talk about the wizard template at all, since we just reuse the same frame and fill in a different content and the order wizard is almost working ... but (as we all know) users usually have their own mind when filling out forms.
 +
So we also need to make sure consistent and complete data ends up in our Order object before submitting it. If my assumption is correct, '''input validation''' is every developer's "most favorite" topic. That's why I dedicated the next [[Small_Talks/2015/September/ZK8_Wizard_Example_-_Part_3|Part 3 (Preview)]] to this important topic.
 +
 
 +
== Download ==
 +
 
 +
* The source code for this article can be found in [https://github.com/cor3000/zk-wizard-example/tree/part-2 github (branch: part-2)].
  
 
== Running the Example ==
 
== Running the Example ==
The example consists of a maven web application project. It can be launched with the following command:
+
Checkout '''part-2'''
 +
 
 +
    git checkout part-2
 +
 
 +
The example war file can be built with maven:
 +
 
 +
    mvn clean package
 +
 
 +
Execute using jetty:
  
 
     mvn jetty:run
 
     mvn jetty:run

Latest revision as of 04:21, 20 January 2022

DocumentationSmall Talks2015SeptemberZK8 Wizard Example - Part 2
ZK8 Wizard Example - Part 2

Author
Robert Wenzel, Engineer, Potix Corporation
Date
September 2015
Version
ZK 8.0

Introduction

In the previous Part 1 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 typical shopping basket and checkout process with the following steps:

  1. Basket
    review/adjust basket (add/remove/change items)
    display recommendations based on the basket content
  2. Shipping Address
    enter shipping address
  3. Payment
    choose payment method + enter conditional details
  4. Confirmation
    review data, accept GTC submit order (handle exceptions)
  5. Feedback
    user feedback when order was successful

Here a preview of the order wizard:

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

Order class dia.png

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

Viewmodel services class dia.png

Creating the UI

The order.zul [1] 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.

steps 2-4 shipping/payment/confirmation [2]

I'll start with the simpler steps for inputting the order information, as they share a common row based layout: one row for each input field.

/src/main/webapp/WEB-INF/zul/order/steps/shippingAddress.zul [3]
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 static value is passed using @init
  • Lines 13, 15, 17: Note: the editable values use reference binding - @ref(...) [4] - not a new but often overlooked feature


/src/main/webapp/WEB-INF/zul/order/steps/payment.zul [5]
the payment page uses the PaymentViewModel [6] to control the visibility of conditional input fields for credit card and direct debit
<?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 optional parameters). Internally 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 parameters such as model (used for selectbox) and updateCommand for @command binding.

/src/main/webapp/WEB-INF/zul/order/steps/payment.zul [7]
<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

/src/main/webapp/WEB-INF/zul/order/steps/basket.zul [8]
The first wizard 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>
  • Lines 9, 20, 27: a simple way to use a basketItemLabel-template in 2 places
  • Line 18: using <sh:forEach> bound to a ListModelList

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. Especially powerful is the combination of forEach and ListModelList. It will automatically expand/shrink/rearrange the items whenever the ListModelList (recommendedItemsModel) is changed.

Here a sample of what is required for this in the BasketViewModel [9]:

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");
	}
	...
  • Line 8: the view model holds a ListModelList to contain the basket recommendations
  • Line 16: whenever the basket is updated (e.g. an item is added) reload the recommendations
  • Line 23: by changing the contents of the ListModelList the bound <forEach> element does it's job automatically

It is not necessary to clear and replace all items - forEach can also handle single item additions/removals/movements, and will only reflect those changes in the component tree to reduce the overhead. This example simply illustrates, that it happens automatically, no additional notifyChange on the recommendedItemsModel is required to reflect the updated recommendations in the page.

Additional features

As in a real application there are always requirements that somehow "distract" from the core solution. I just picked a few to show they don't necessarily break the overall design.

Input Mask

The payment.zul [10] page uses an input mask to format the credit card number during user input. These kind of client side 'effects' can now be applied easily with custom data-handlers (ZK 8) (more data handler examples can be found in our growing example repository).

<template name="creditcard">
    <textbox value="@bind(value)" ca:data-mask="${i18n:nls('order.creditCard.number.format')}"/>
</template>

In the zk.xml [11] the handler is configured as follows. It is loading an external jquery plugin (which also be a JS resource inside the webapp), and decorates the widget with the JS code during initialization on the client side. The server side component behaves as usual receiving the unmasked value while the view model isn't even aware of that.

		<data-handler>
			<name>mask</name><!-- the attribute name, i.e. data-mask -->
			<script src="http://igorescobar.github.io/jQuery-Mask-Plugin/js/jquery.mask.min.js"/>
			<script>
				function (wgt, dataValue) {
					jq(wgt.$n()).mask(dataValue);
					wgt.listen({
						onChange: function (event) {
							event.data.value = jq(this.$n()).cleanVal();
						}
					});
				}
			</script>
		</data-handler>

Bookmark Handling

A simple bookmarking mechanism is added to enable browser back navigation without leaving the wizard, nothing new but often forgotten while very simple to implement.

/wizardexample/src/main/webapp/order.zul [12]
when a bookmark change is detected send the 'gotoPage' command
<?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>
zk.example.order.OrderViewModel [13]
whenever the wizard step changes set a bookmark, and vice versa, goto a step when the bookmark changes
		wizardModel = new WizardViewModel<WizardStep>(availableSteps) {
			@Override
			protected void onStepChanged(WizardStep currentStep) {
				bookmark.set(currentStep.getId(), false);
			}
		};
	...
	@Command("gotoStep")
	public void gotoStep(@BindingParam("stepId") String stepId) {
		if(!getWizardModel().gotoStep(stepId)) {
			//if step change unsuccessful override the bookmark
			bookmark.set(wizardModel.getCurrentStep().getId(), true);
		};
	}
zk.example.wizard.Bookmark [14]
for completeness the trivial Bookmark class
public class Bookmark {
	public void set(String currentStepId, boolean replaceBookmark) {
		Executions.getCurrent().getDesktop().setBookmark(currentStepId, replaceBookmark);
	}
}

Custom I18N

To enable I18N the labels have been defined in the zk-label.properties and a custom mechanism is put place to conveniently convert enumerations to labels, icons or styles, in the same way as resolving plain simple labels.

/wizardexample/src/main/webapp/WEB-INF/zk-label.properties [15]
defines the labels, enables adding localized versions e.g. zk-label_de.properties (see: documentation)
zk.example.i18n.NlsFunctions [16]
a set of I18n functions used throughout the application, both in zul and java code

Used via custom taglib functions in a zul file:

<?taglib uri="/WEB-INF/tld/i18n.tld" prefix="i18n"?>
	...
	${i18n:nls('order.shippingAddress.hint')}
	...
	<formRow type="static" label="@init(i18n:nls('order.basket'))" value="@init(order.basket)"/> 
	...

Called directly in java code:

		DecimalFormat decimalFormat = new DecimalFormat(NlsFunctions.nls("order.price.format"));
		return NlsFunctions.nlsArgs("order.basket.format", getTotalItems(), decimalFormat.format(getTotalPrice()));

Summary

As shown above I almost didn't talk about the wizard template at all, since we just reuse the same frame and fill in a different content and the order wizard is almost working ... but (as we all know) users usually have their own mind when filling out forms. So we also need to make sure consistent and complete data ends up in our Order object before submitting it. If my assumption is correct, input validation is every developer's "most favorite" topic. That's why I dedicated the next Part 3 (Preview) to this important topic.

Download

Running the Example

Checkout part-2

   git checkout part-2

The example war file can be built with maven:

   mvn clean package

Execute using jetty:

   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.