Felipe Cypriano

You're about to read

Handling Events on ZK Macro Components

One of the great features of ZK is the possibility to create a new component based only on other existing components using the declarative language ZUML - the same you’ve been using in you zul files. Those are called Macro Components. The beauty of macro components is that it’s very easy to make a new one and hence avoid code duplication on your pages.

Talk about how to implement a macro component isn’t the scope of this post, so I suggest you to read the documentation before continue reading. We’ll work on a very simple macro component. But, don’t hesitate when you need to make a very complex one. Our macro component is this:

WEB-INF/mymacro.zul
1
2
3
4
<hbox>
    <label id="myLabel" value="Click the button"/>
    <button id="myButton" label="The Button"/>
</hbox>

Now that you already knows what is and how to make a macro component, let’s start talking about what you may think it’ll work but actually it doesn’t.

Automatically Forward Event Doesn’t Work

The standard way to register events on ZK’s component, using MVC, is to use the “autowire” feature of GenericForwardComposer. Like this:

WEB-INF/index.zul
1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<zk>
    <window apply="com.felipecypriano.IndexComposer">
        <label id="lblOutside"/>
        <button id="btnOutside" label="Not In Macro"/>
    </window>
</zk>
src/com/felipecypriano/IndexComposer.java
1
2
3
4
5
6
7
8
9
10
11
12
package com.felipecypriano;

import org.zkoss.zk.ui.util.GenericForwardComposer;
import org.zkoss.zul.Label;

public class IndexComposer extends GenericForwardComposer {
    private Label lblOutside;

    public void onClick$btnOutside() {
        lblOutside.setValue("Button 'not in macro' clicked");
    }
}

The button ‘btnOutside’ belongs to the window component and therefore its composer can automatically access and configure btnOutside events. When the Composer is initializing it detects the method onClick$btnOutside() and sets this method as a ‘btnOutside’ onClick event listener, in other words when the component is clicked this method is executed.

Knowing this we can think that add an event to ‘myButton’, which belongs to ‘mymacro’, is pretty much the same. Don’t we? Let’s add the macro in the index.zul and create a new method to handle onClick event of ‘myButton’:

WEB-INF/index.zul
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<?component name="mymacro" macroURI="/mymacro.zul" ?>
<zk>
    <window apply="com.felipecypriano.IndexComposer">
        <label id="lblOutside"/>
        <button id="btnOutside" label="Not In Macro"/>
        <separator bar="true"/>
        <mymacro/>
    </window>
</zk>

Just before <zk> tag is the macro component definition (line 2), this directive tells to ZK where is the file that contains the component - macroURI=”/mymacro.zul” - and what’s the tag name we’ll use - name=”mymacro” - to reference our component (line 8).

Adding the onClick event on ‘myButton’ cannot be done by just adding this method to IndexComposer:

src/com/felipecypriano/IndexComposer.java
1
2
3
    public void onClick$myButton() {
        lblOutside.setValue("Button 'myButton' clicked");
    }

So, if this doesn’t work what should I do?

Using a Speciliazed Class

To take care of mymacro’s behavior we need to write some code. We need to create a class to specifically handle our component’s needs. The purpose of mymacro component is to update the label when the button is clicked, this will not affect any of the components that doesn’t belong to mymacro so it makes sense to put the event in a specialized class that can be used to all pages.

The two components inside the macro - label and button - will be accessed using getFellow(string)) method and the event will be dynamically added to the button using addEventListener) method.

src/com/felipecypriano/MyMacro.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.felipecypriano;

import org.zkoss.zk.ui.HtmlMacroComponent;
import org.zkoss.zk.ui.event.*;
import org.zkoss.zul.*;

public class MyMacro extends HtmlMacroComponent {
    private Label myLabel;

    @Override
    public void afterCompose() {
        super.afterCompose(); // DON'T forget this
        Button myButton = (Button) getFellow("myButton");
        myLabel = (Label) getFellow("myLabel");

        myButton.addEventListener(Events.ON_CLICK, new EventListener() {
            public void onEvent(Event event) throws Exception {
                myLabel.setValue("Hooray! 'myButton' was clicked.");
            }
        });
    }
}

Now change the component’s definition to point to MyMacro.java as the component’s class:

<?component name="mymacro" macroURI="/mymacro.zul" class="com.felipecypriano.MyMacro" ?>

Voilà.

Global Component Definition

There’s a DRY principle violation and we must solve it. The main reason to use a macro component is to share a set of components between multiple pages, to avoid code duplication. The macro itself does this, but if you pay attention you’ll see that the component’s definition must be at the very beginning of each page which wants to use the component and this, definitely, isn’t a good a thing.

For instance, supposing that you have 25 pages that are currently using the macro component, and in each one of them has the component’s definition. Some months later, for some reason, you must change yours component’s class. Instead of changing just one simple place you’ll have to update 24 more places. Keep your code simple and don’t repeat anything. In order to make the component’s definition global we need to create a new xml file called lang-addon.xml:

WEB-INF/lang-addon.xml
1
2
3
4
5
6
7
8
9
<language-addon>
    <addon-name>myaddon</addon-name>
    <language-name>xul/html</language-name>
    <component>
        <component-name>mymacro</component-name>
        <component-class>com.felipecypriano.MyMacro</component-class>
        <macro-uri>/mymacro.zul</macro-uri>
    </component>
</language-addon>
WEB-INF/zml.xml
1
2
3
4
5
<zk>
    <language-config>
        <addon-uri>/WEB-INF/lang-addon.xml</addon-uri>
    </language-config>
</zk>

And at last, remove <? component ?> directive from index.zul. From now on mymacro component is available to all zul pages in your application.

Making the macro component shout to the world

Just one more detail to learn, the component can handle its own components events, but how do the page that has the macro component knows what’s going on? We need to make the macro component send events to the page, or any listener. And this is the our final step.

To keep it simple the macro component will respond just to onClick event, that’s when the button is clicked ‘myLabel’ will be update as usual and the macro component will send the onClick event to all its listeners. Replace myButton EventListener with the following code:

src/com/felipecypriano/MyMacro.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
myButton.addEventListener(Events.ON_CLICK, new EventListener() {
    public void onEvent(Event event) throws Exception {
        myLabel.setValue("Hooray! 'myButton' was clicked.");

        if (isListenerAvailable("onClick", true)) {
            Event clicked = new Event(Events.ON_CLICK, getParent());

            Iterator listeners = getListenerIterator(Events.ON_CLICK);
            while (listeners.hasNext()) {
                EventListener listener = (EventListener) listeners.next();
                listener.onEvent(clicked);
            }
        }
    }
});

The code between lines 5 and 13 is responsible to send the event to the listeners. But we need a couple more changes, like make IndexComposer listen to mymacro. First put an id in mymacro:

WEB-INF/index.zul
1
<mymacro id="mymacro"/>

And register the event using the standard way in IndexComposer:

1
2
3
public void onClick$mymacro() {
    lblOutside.setValue("I can hear you 'mymacro'!");
}

With all this it’s possible to do complex and incredible powerful macro components. Go ahead and try.