ZK8 Wizard Example - Part 4 final
Robert Wenzel, Engineer, Potix Corporation
March 2016
ZK 8.0
Introduction
- Part 1 - Defining the Wizard
- Part 2 - Order Wizard (a more complex example)
- Part 3 - Form Handling and Input Validation
- Part 4 - Styling the wizard (with Bootstrap) (You are here)
To wrap up the Wizard Example Series here the final Part showing how things finally fit together when applying a 3rd party CSS framework (Bootstrap) to the wizard.
The implementation decisions/techniques used in the previous chapters now become handy. The strict separation between view and viewModel code enables the style overhaul without touching any Java code. Restyling an application still requires a significant amount of work where the major task is to adjust the various zul pages and templates to render the markup required by Bootstrap. But compared to changing both zul and java code the possibility to introduce errors is much smaller (and even if you cause probems, you know it's somewhere in the view).
Here a recording of the results (descriptions of the most significant changes will follow below):
Adding the Bootstrap resources
It's straight forward: just add the required css/js resources. ZK offers several ways to do so:
- script / link directives
- script / style components
- javascript / stylesheet elements in a lang-addon.xml
- embed element in zk.xml
For simplicity of the example I just added them to my root page.
- /wizardexample/src/main/webapp/order.zul [1]
<!-- Latest compiled and minified CSS -->
<?link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" ?>
<!-- Latest compiled and minified JavaScript -->
<?script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js" ?>
Of course in a real application you'll likely chose to include the css/js files in your web application (not the topic here).
Render the UI in Bootstrap style
Once the layout is decided (in the ideal case a web designer decided for you, and provided static html mockups), you can change you zul files accordingly.
Here I'll not try to make all ZK components look "like" Bootstrap, instead I'll use XHTML components and native elements to produce the HTML necessary for Bootstrap. This allows to use and update the 3rd-party css styles directly whenever needed (or even apply different bootstrap themes later).
ZK8's reusable templates and shadow elements help again to avoid repeating the more verbose HTML markup. If necessary the markup structure of an existing ZK component can be adjusted using a custom "mold" LINK ME!!!! to fit into the desired layout.
Page/Wizard layout
- /wizardexample/src/main/webapp/order.zul [2]
Here the bootstrap css class (container) - was added to provide a responsive fixed width layout - nothing else worth mentioning here.
<x:div class="container"
viewModel="@id('vm') @init('zk.example.order.OrderViewModel')"
validationMessages="@id('vmsgs')"
onBookmarkChange="@command('gotoStep', stepId=event.bookmark)">
...
More interesting are the changes in the wizard template defining the overall look and feel:
- /wizardexample/src/main/webapp/WEB-INF/zul/template/wizard/wizard.zul [3]
One of the straight forward changes is applying the bootstrap button styles:
<button zclass="btn btn-default" label="@load(wizardVM.backLabel)" onClick="@command(wizardVM.backCommand)" />
...
<button zclass="btn btn-success pull-right" label="@load(wizardVM.nextLabel)" onClick="@command(wizardVM.nextCommand)" />
NOTE: I use zclass instead of sclass to replace ZK's own button class (z-button) using bootstrap's css-classes (btn btn-default ...).
For the progress bar it was also simple to adapt the bootstrap progressbar example markup. The outer <n:div> is rendered using the native namespace, since it does not require dynamic updates, while the inner <x:div> uses an xhtml component to enable data binding on the dynamic style and textContent properties. A notifyChange on the progress-property in our wizardVM object will automatically adjust the label and progressbar width (triggering the css animation provided by bootstrap).
<template name="wizardProgress">
<n:div class="progress">
<x:div class="progress-bar progress-bar-success progress-bar-striped"
style="@load(('width: ' += wizardVM.progress += '%; min-width: 2em;'))"
textContent="@load((wizardVM.progress += '%'))"/>
</n:div>
</template>
- Line 5: the textContent-attribute on XHTML components is a new ZK8 feature to allow dynamic text content directly inside any html tag
In a similar fashion a bootstrap panel can be composed (I assume you notice the pattern):
<div zclass="panel panel-primary"
viewModel="@id('wizardVM') @init(wizardModel)"
validationMessages="@id('vmsgs')"
onOK="@command(wizardVM.nextCommand)">
<n:div class="panel-heading">
<x:h3 class="panel-title" textContent="@load(wizardVM.currentStep.title)"/>
</n:div>
<n:div class="panel-body">
<sh:apply template="@init(empty wrapperTemplate ? 'defaultWizardContentWrapper' : wrapperTemplate)"/>
</n:div>
</div>
Basket Layout
On basket page the <grid> was replaced by a striped bootstrap table. The shadow element <sh:forEach> allows to reuse the same ListModel of BasketItems from the BasketViewModel only changing the layout and bind expressions. As described above xhtml and native elements are used to render the markup required by bootstrap.
The Tooltips are enabled using the Bootstrap JS API more on that below.
In order to make the page a little more responsive to browser width changes the Basket recommendations at the bottom use the Bootstrap grid system to vary the number of recommendations per row at different display sizes:
- /wizardexample/src/main/webapp/WEB-INF/zul/order/steps/basket.zul [4]
<x:div class="row">
<sh:forEach items="@init(basketVM.recommendedItemsModel)">
<x:div class="col-lg-4 col-sm-6">
<x:div class="well well-sm">
<sh:apply template="basketItemLabel" item="@init(each)"/>
<button zclass="btn btn-info btn-xs pull-right" iconSclass="z-icon-shopping-cart"
onClick="@command('addRecommendedItem', item=each)"
ca:data-toggle="tooltip" ca:data-placement="right"
ca:data-title="${i18n:nls('order.basket.add')}"/>
</x:div>
</x:div>
</sh:forEach>
</x:div>
Form layout
The remaining pages previously rendered using a <grid> layout are now implemented using Bootstrap's horizontal form layout. This achieves the popular look and responsive sizing/positioning of input elements and their labels. The parameters to the <formRow> template remain identical to the previous version, since the model data hasn't changed.
<x:div class="form-horizontal">
<formRow type="static" label="@init(i18n:nls('order.basket'))" value="@init(savedOrder.basket)"/>
<x:div class="alert alert-info" role="alert" textContent="${i18n:nls('order.shippingAddress.hint')}"/>
<formRow type="textbox" label="@init(i18n:nls('order.shippingAddress.street'))"
value="@ref(order.shippingAddress.street)"
error="@ref(vmsgs['p_shippingAddress.street'])"/>
<formRow type="textbox" label="@init(i18n:nls('order.shippingAddress.city'))"
value="@ref(order.shippingAddress.city)"
error="@ref(vmsgs['p_shippingAddress.city'])"/>
<formRow type="textbox" label="@init(i18n:nls('order.shippingAddress.zipCode'))"
value="@ref(order.shippingAddress.zipCode)"
error="@ref(vmsgs['p_shippingAddress.zipCode'])"/>
</x:div>
- Lines 1,3: using bootstrap specifc css classes (form-horizontal, alert, alert-info) on xhtml components
Inside the formRow template the restyling continues. I also chose to display validation errors in using a bootstrap css classes.
<x:div sclass="@load(empty error ? 'form-group' : 'form-group has-error')">
<sh:choose>
<sh:when test="@load(type eq 'checkbox')">
<n:div class="col-sm-offset-3 col-sm-9">
<sh:apply template="checkbox"/>
</n:div>
</sh:when>
<sh:otherwise>
<x:label class="col-sm-3 control-label" textContent="@init(label)"/>
<n:div class="col-sm-9">
<sh:apply template="@load(type)"/>
</n:div>
</sh:otherwise>
</sh:choose>
</x:div>
<sh:if test="@load(!empty error)">
<x:div sclass="alert alert-danger well-sm" textContent="@load(error)"/>
</sh:if>
<template name="checkbox">
<n:div class="checkbox">
<checkbox zclass=" " checked="@bind(value)" onCheck="@command(updateCommand)" label="@load(label)"
mold="formgroup"/>
</n:div>
</template>
<template name="textbox">
<textbox zclass="form-control" value="@bind(value)" onChange="@command(changeCommand)"/>
</template>
...
- Line 1: <row> is replaced by a <x:div> with dynamic styling based depending on the presence of an error message
- Line 17: conditionally output an error using a Bootstrap alert style
- Line 23: apply a custom mold to render the checkbox and it's label in the markup required by bootstrap
Since bootstrap requires different html markup to render a checkbox inside a horizontal form I decided to define a custom mold for this scenario.
The markup requried by Bootstrap.
<label>
<input type="checkbox"> Remember me
</label>
The corresponding custom mold implementation:
function (out) {
var uuid = this.uuid, content = this.domContent_();
out.push('<label', this.domAttrs_(), ' for="', uuid,'-real">',
'<input type="checkbox" id="', uuid, '-real"', this.contentAttrs_(), '/>',
content,'</label>');
}
As custom mold LINK ME!!!! implementation requires some additional knowledge about the internal structure of a ZK widget I try to use this approach only if necessary. This is an (often forgotten) available option for any ZK widget in case the rendered markup needs to be customized.
When upgrading to a different ZK version later this may need to be adjusted, on the other hand it provides the most efficient way to render custom markup, since it only happens at the client side.
Leverage the Bootstrap JS API
Bootstrap Tooltips
Bootstrap also offers a Javascript-api to enable dynamic effects. On the basket page I use Bootstrap-tooltips configured with ZK8's data-handler, which can be used throughout the application by simply adding client-side data-attributes.
<zk xmlns:sh="shadow" xmlns:x="xhtml" xmlns:n="native" xmlns:ca="client/attribute" xmlns:z="zk">
...
<button zclass="close" iconSclass="z-icon-times" onClick="@command('removeItem', basketItem=item)"
ca:data-toggle="tooltip" ca:data-placement="right" ca:data-title="${i18n:nls('order.basket.remove')}"/>
...
<button zclass="btn btn-info btn-xs pull-right" iconSclass="z-icon-shopping-cart"
onClick="@command('addRecommendedItem', item=each)"
ca:data-toggle="tooltip" ca:data-placement="right" ca:data-title="${i18n:nls('order.basket.add')}"/>
An alternative to using "ca:data-title" is to use ZK's tooltiptext property which renders into the title attribute of the resulting DOM element. Since tooltip text is a server side attribute it also allows the data-binding annotations @init/@load.
<span zclass=" " status="@ref(item.status)" sclass="@load(i18n:nlsSub(status, 'style'))"
ca:data-toggle="tooltip" ca:data-placement="top" tooltiptext="@load(i18n:nls(status))">
Implementing the data-handler
Following the Bootstrap JS documentation the following JS call is necessary to initialize a tooltip for one or more specified DOM elements.
$('#example').tooltip(options)
Since ZK pages are dynamic the selector needs to be dynamic too because element Ids are not static and on top of that ZK components may be added at any time into the DOM via AJAX. In such cases the client side widget lifecycle event w:onBind is a good hook to initialize JS behavior. Prior to ZK8 this was possible doing using a client side event Listener.
<zk xmlns:w="client">
<button w:onBind="jq(this).tooltip({placement: 'top'})" tooltiptext="my tooltip"/>
</zk>
A simple data-handler configuration could look like this in zk.xml.
<data-handler>
<name>tooltip</name>
<script>
function (wgt, placement) {
jq(wgt.$n()).tooltip({placement: placement});
}
</script>
</data-handler>
<zk xmlns:w="client/attribute">
<button ca:data-tooltip="top" tooltiptext="my tooltip"/>
</zk>
Modal
Summary
That's it for now, any ideas what to show next? Waiting for your comments.
Download
- The source code for this article can be found in github (branch: part-4).
Running the Example
Checkout part-4
git checkout part-4
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. |