Custom Components in ZK Framework
Want to reduce the size of your code? Click here to learn more about small custom components in the ZK framework that simplify your code.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
We all know that developers are lazy — we copy/paste and refactor code.
I can’t disagree on this; I don’t want to write a lot of code. I want to reuse code.
Tools like sonar can help us with detecting duplicated code, but sonar will look over code with minimal changes. With some common sense, we can detect this and make improvements so that we have less code. This makes it easier to maintain and speed up coding in the future. In this article, we will see how easy it is to make small custom components in the ZK Framework that actually reduces your code.
What Can We Expect in This Article?
In this article, I want to show you how easy it is to make custom components in ZK.
Custom components can be created in very complicated code, but I would like to show you how you can create it in a simpler way and reduce the amount of code. The code snippets I am going to show are actually used in real projects. Some extra features are left out to simplify the examples.
Each project also contains multiple custom components to simplify coding. The first component I want to show you is how to code is an EnumComboBox. Just as the class implies, we have a Combobox specifically made for the use of Enums.
The second component is an ActiveLabel. When working with catalogs, you can almost never delete entries. Instead, you need to deactivate them. The best way to show that it’s deactivated is to strikethrough our label text.
EnumCombobox
When creating a Combobox with Enum value’s in ZK, you need to provide a setter for the ListModelList filled with the Enum values. In most of the cases, we just want all the values in the Combobox. We actually write the same getter over and over again for other Enums. Certainly, if it screens with a lot of Combobox filled with Enum values, your ViewModel is getting filled with default code that you need to set it all up.
But, what if we can write that into a component? We don’t need to provide less code in the ViewModel. This means a cleaner ViewModel. The advantage is that Enums don’t come from any database, and with reflection, we can easily get all the values. The ComboBox needs to be aware of what Enum we want to address. A ComboBox should look like this:
<combobox selectedItem="@bind(vm.selected)" enumClass="MyEnum"/>
When we think a little further, it’s hard to give only the enum class to be correct. The FQN should be better and always return to the correct Enum class. A nice feature would be the option to have a null value. This is so that if you needed it in the future, it’s already there.
If you look at the Zul snippet I showed, you may have noticed that I didn’t specify a renderer. We can implement a default renderer specified to an interface that the Enums need, or just use the Enums toString
method. For this example, we just take the Enum toString
method.
The code should look like this:
public class EnumCombobox extends Combobox implements AfterCompose {
private Class<Enum> enumClass;
private boolean emptyValue = false;
@Override
public void afterCompose() {
ListModelList model = new ListModelList(enumClass.getEnumConstants());
if (emptyValue) {
model.add(0, null);
}
setModel(model);
setItemRenderer(new ComboitemRenderer<Enum>() {
@Override
public void render(Comboitem item, Enum data, int index) throws Exception {
if (data == null) {
item.setLabel("Empty selection");
item.setStyle("color:grey");
} else {
item.setLabel(data.toString());
}
item.setValue(data);
}
});
}
public void setEnumClass(String enumClass) throws ClassNotFoundException {
this.enumClass = (Class<Enum>) Class.forName(enumClass);
}
public void setEmptyValue(boolean emptyValue) {
this.emptyValue = emptyValue;
}
}
We don’t need to rewrite the whole ComboBox, so we just extend that class. But, I want to kick in after the ComboBox is created. Because of that, we need to implement the aftercompose
interface. The first thing to consider is what properties our component will have. For this example, we need a Class<Enum> and a boolean to know if we need to add an empty value.
When that is done, we need to implement the aftercompose
method. We need a ListModel
to set our model, check if we need to add a null to the ListModel, and, at last, set our itemrenderer
. Of course, we need to take care of the null value in the itemrenderer.
I don’t like empty space, so we add some specific text and style.
Now, we are done. Indeed, it’s not more than this to make it already running. We save every time we use this at least three lines in our ViewModel. Of course, the next step is to know how we can use this. For testing, we can use the following snippet:
<combobox use="be.chillworld.EnumCombobox" enumClass="be.chillworld.MyEnum"/>
This is easy to use. The use attribute will tell you which class to use for the ComboBox tag that we just used. But, of course, it can be much nicer when you find that your component is ready for the next step. Then, we need to create/alter the lang-addon.xml.
<language-addon>
<addon-name>test-addon</addon-name>
<version>1.0</version>
<language-name>xul/html</language-name>
<depends>zul, zkbind</depends>
<component>
<component-name>enumcombobox</component-name>
<extends>combobox</extends>
<component-class>be.chillworld.components.EnumCombobox</component-class>
</component>
<component>
<component-name>inuselabel</component-name>
<extends>label</extends>
<component-class>be.chillworld.components.InUseLabel</component-class>
</component>
</language-addon>
With this added to our project, we are able to write our component in the Zul like this:
<enumcombobox enumClass="be.chillworld.MyEnum" />
And, this is the result we will want to use.
InUseLabel
The next component is the InUseLabel
. This component is great when you have a lot of classes that have boolean for inUse or active. If there is something that’s not active anymore, we want to show it to the user. Of course, we need to streamline this behavior to the whole application so the user never gets confused. With a normal label, we will use the style or class attribute to set some extra styling when the object isn’t active anymore. Again, because it’s always the same thing, we need to write. But, why don’t we put it in our component class? This is because you must think about the scope of your component.
Do you want to use it in other projects, or do you want your project to be a little easier to use for this project? You can copy the code to another project, but refactoring would normally be a must. To make it easy, I give the two options here. The first option is to make the project independent. We call it InUseLabel2
:
<inuselabel2 value="@load(vm.user)" inUse="@load(vm.user.active)"/>
The isInUse
method is to know if we need to apply the extra style or not.
So, like this, we now know that our InUseLabel
needs a setter for a boolean. A getter isn’t needed, because it’s a label and the value can never change on the client side. However, you are always open to making one. The code of this component looks like this:
public class InUseLabel2 extends Label {
private static final String INACTIVE_STYLE = "color:red;text-decoration:line-through;";
private boolean inUse;
private String style;
public void setInUse(boolean inUse) {
this.inUse = inUse;
setStyle(style);
}
@Override
public String getStyle() {
return style;
}
@Override
public void setStyle(String style) {
this.style = style;
super.setStyle((style==null?"":style) + (inUse ? "" : INACTIVE_STYLE)) ;
}
}
Our second case is an example of an existing project. In that project, we had a lot of catalogs. They all shared the abstract class AbstractCatalog
.
public abstract class AbstractCatalog {
private boolean inUse;
private String code, description;
public AbstractCatalog(boolean inUse, String code, String description) {
this.inUse = inUse;
this.code = code;
this.description = description;
}
public boolean isInUse() {
return inUse;
}
public void setInUse(boolean inUse) {
this.inUse = inUse;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
The inUse
was a property that told you if the catalog was still being used. Otherwise, they can see it in older data, but they can never choose for new data again. So, in this case, we want our inUseLabel
to be like this :
<inuselabel catalog="@load(vm.catalog)"/>
We call this class the InUseLabel
. Our code does change a little because the inUse
property is gone, but we need a catalog property.
We don’t want to use or even expose the setValue
method, in order to make our component a little more robust. For the setStyle
, we can do the same, but like this, we don’t have any control over extra style or we need to use some class.
In the following example, I’ll show you how we set our layout by default, but we can still open it to change when our inactive style isn’t active. The code looks like:
public class InUseLabel extends Label {
private static final String INACTIVE_STYLE = "color:red;text-decoration:line-through;";
private AbstractCatalog catalog;
private String style;
public AbstractCatalog getCatalog() {
return catalog;
}
public void setCatalog(AbstractCatalog catalog) {
this.catalog = catalog;
if (catalog != null) {
super.setValue(catalog.getCode() + " - " + catalog.getDescription());
} else {
super.setValue(null);
}
setStyle(style);
}
@Override
public String getStyle() {
return style;
}
@Override
public void setStyle(String style) {
this.style = style;
boolean extraStyle = catalog == null || !catalog.isInUse();
super.setStyle((style==null?"":style) +(extraStyle ? INACTIVE_STYLE : "")) ;
}
@Override
public void setValue(String value) {
throw new UnsupportedOperationException("inuselabel can't set value, please set label through the catalog attribute.");
}
}
A Brief Explanation About the Style
We keep the style and the developer set with Zul in our class. And, we will set the super.setStyle
in our setStyle method. If you look closely, the INACTIVE_STYLE
is appended to the end. This is important, because, if the user set color:green, the textcolor will change to green, except when our object is inActive = true. Then, the color will be red.
This is because CSS takes the last attribute to set. In this case, it’s the color:red from the INACTIVE_STYLE
. If we set the INACTIVE_STYLE
first, the text in the label will always be green. Of course, we will add this in the lang-addon.xml
just like our Enum Combobox.
Now, we need some implementations of the AbstractCatalog
and the fun can begin.
public class Continent extends AbstractCatalog {
public Continent(boolean inUse, String code, String description) {
super(inUse, code, description);
}
}
public class Country extends AbstractCatalog {
private Continent continent;
public Country(boolean inUse, String code, String description) {
super(inUse, code, description);
}
public Continent getContinent() {
return continent;
}
public void setContinent(Continent continent) {
this.continent = continent;
}
}
The Actual Usage
For the Zul page, I make the same components: one time with usage of the name we gave in the lang-addon
and one time with the use attribute. However, the InUseLabel2
does not allow us to set it as a component to avoid silly repetition and to give you a chance to check it out yourself.
<?xml version="1.0" encoding="UTF-8"?>
<?page title="spring boot" contentType="text/html;charset=UTF-8" docType="html" ?>
<zk xmlns="http://www.zkoss.org/2005/zul" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.zkoss.org/2005/zul http://www.zkoss.org/2005/zul/zul.xsd">
<window border="normal" height="100%" viewModel="@id('vm') @init('be.chillworld.vm.IndexVM')">
<grid>
<columns>
<column/>
<column/>
</columns>
<rows>
<row spans="2">
<label value="EnumCombobox" style="font-weight: bold;"/>
</row>
<row>
<label value="With the use attribute"/>
<combobox selectedItem="@bind(vm.selectedColor)" enumClass="be.chillworld.model.enums.Color" use="be.chillworld.components.EnumCombobox"/>
</row>
<row>
<label value="defined as component"/>
<enumcombobox selectedItem="@bind(vm.selectedColor)" enumClass="be.chillworld.model.enums.Color" emptyValue="true"/>
</row>
<row spans="2">
<label value="InUseLabel" style="font-weight: bold;"/>
</row>
<row>
<label value="With the use attribute"/>
<label catalog="@load(vm.country)" use="be.chillworld.components.InUseLabel"/>
</row>
<row>
<label value="defined as component"/>
<inuselabel catalog="@load(vm.continent)" style="color:green;"/>
</row>
<row spans="2">
<label value="InUseLabel2" style="font-weight: bold;"/>
</row>
<row>
<label value="With the use attribute"/>
<label value="@load(vm.user)" inUse="@load(vm.user.active)" use="be.chillworld.components.InUseLabel2"/>
</row>
<row spans="2">
<button label="Switch active" onClick="@command('switchActive')"/>
</row>
</rows>
</grid>
</window>
</zk>
So, the only thing we need is a ViewModel to let this Zul work. You know a ViewModel is just like a POJO. In this case, it’s a very simple example, with one button to change the active state for the InUseLabel
change to see.
public class IndexVM {
private Color selectedColor;
private Country country = new Country(true, "BE","Belgie");
private Continent continent = new Continent(true, "EU","Europe");
private User user = new User("John","Doe");
@Init
public void init() {
country.setContinent(continent);
}
public Color getSelectedColor() {
return selectedColor;
}
public void setSelectedColor(Color selectedColor) {
this.selectedColor = selectedColor;
}
public Country getCountry() {
return country;
}
public void setCountry(Country country) {
this.country = country;
}
public Continent getContinent() {
return continent;
}
public void setContinent(Continent continent) {
this.continent = continent;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@Command
@NotifyChange({"continent","country","user"})
public void switchActive() {
country.setInUse(!country.isInUse());
continent.setInUse(!continent.isInUse());
user.setActive(!user.isActive());
}
}
Playtime
Now, it’s up to you! You can download or clone the GitHub repository:
https://github.com/chillworld/zk-components
Then, run the Spring Boot application (goal: spring-boot:run) or with NetBeans. There is a nbactions.xml provided. The page is http://localhost:8080/index.zul.
Opinions expressed by DZone contributors are their own.
Comments