Composable Composers - Creating a Popup Reference"

From Documentation
(No difference)

Revision as of 03:45, 9 July 2019

DocumentationSmall Talks2019JulyComposable Composers - Creating a Popup Reference
Composable Composers - Creating a Popup Reference

Author
Robert Wenzel, Engineer, Potix Corporation
Date
July, 2019
Version
ZK 8.6.2

Introduction

Sometimes you want to implement a common/recurring feature for components (e.g. a discardable initial help overlay or image preview overlay, a recurring visual effect, remember some kind of contextual data for a popup, fire an occasional custom event ... I could go on).

You can of course extend existing components, adding a new property (getter and setter) and implement the functionality on that component, extend/define the component-definition in a lang-addon.xml.

This may lead to multiple specialized extensions of various component classes just for a small recurring feature. Instead of extending each class separately - bloating up the component class hierarchy - you can implement and apply such a behavior (or multiple) on demand.

(Un)fortunately Java's uses a static type system, so you often read the unsatisfying recommendation ...

A: Favor Composition over Inheritance!

Leading to more questions ...

Q: How would I do this without changing/recompiling ZK's provided component hierarchy?

Q: Is this even possible with ZK components?

Q: Which feature could be use for behavior Composition?

A: ... a Composer ... maybe? Let's cover some basics first!

Composers as Re-Usable Behaviors

For these scenarios ZK provides Composers which are invoked after component creation. The more traditionally used composers types are GenericForwardComposer, SelectorComposer (since ZK 6) and BindComposer (for MVVM since ZK 6). Their purpose is to ease integration of application logic and convenient access to components. Technically they could be used to implement any attachable, custom component behavior, however they are quite "heavy" - running complex/expensive wiring, annotation scanning and binding behavior (which is the price for abstraction and convenience).

All these composers implement the Composer interface, providing the most light-weight extension point - a simple callback method.

public interface Composer<T extends Component> {
	public void doAfterCompose(T comp) throws Exception;
}

That's it - a single method. No fields, no side effects, (almost) no overhead. Just a single method invoked after the component (and children) have been created and attached => ready to apply our plug-in-behavior.

Closable for any Component

Panel, Tab and Window have a unique behavior and event triggered by the close button. When clicking the 'close' icon, they detach themselves after firing an onClose event. In the 'onClose' listener it is possible to prevent the detaching by calling event.stopPropagation() so that the default listener is never reached and the component remains on the page. This is often useful, in case of validation failures or the component should be reused - i.e. instead of detaching simply making it invisible.

A general-purpose-closable-behavior can be implemented without extending any existing component.

e.g. Closable.java

public class Closeable implements Composer {

    public final static Closeable instance = new Closeable();

    @Override
    public void doAfterCompose(Component comp) throws Exception {
        comp.addEventListener(Events.ON_CLICK,
                e -> Events.postEvent(Events.ON_CLOSE, comp, null));
        comp.addEventListener(-1000, Events.ON_CLOSE,
                e -> comp.detach());
    }
}
  • "onClick" listener (reacting to user clicks on that component (you might decide to add the listener to only a specific child component), which triggers an "onClose" event (instead of detaching immediately)
  • "onClose" event listener (with a lower priority, so that it can be intercepted by calling event.stopPropagation())

I'd like to mention the static field instance here. Since the Closable composer is stateless, a single instance can be reused as many times as needed, saving additional memory.

Basics.zul demonstrates the basic usage.

A theoretical scenario ...

<?import zk.example.Closeable?>
<zk>
    <div apply="your.package.ApplicationComposer">
        ...
        <div id="someInformation" apply="${Closeable.instance}"/>
            Please click this area away to accept our terms and conditions. 
            If you're lucky we won't bother you any longer.
            <grid>...</grid>
        </div>
    </div>
<zk/>

In order to customize auto-closing behavior, your ApplicationComposer can now add an event "onClose"-listener, e.g. prevent the closing in 50% of the cases, to test his patience.

E.g. In a SelectorComposer you can then prevent the auto-closing based on your application logic or give user feedback when the information div is closed.

public class ApplicationComposer extends SelectorComposer<Div> {
    ...
    @Listen("onClose=#someInformation")
    public void closeSomeInformation() {
        if (Math.random() < 0.5) {
            event.stopPropagation();
            Clients.showNotification("Bad luck, Please click again to remove!");
        } else {
            Clients.showNotification("Component removed.");
        }
    }
    ...

Tiny Q&A

In anticipation of your next questions - here a quick Q&A:

Q: I want to add this behavior dynamically. Where's the component.setApply() method?

A: There isn't one.

Q: How do I set the Closable composer from Java code?

A: You don't. Simply call Closable.doAfterCompose from your ApplicationComposer's doAfterCompose method.

public class ApplicationComposer extends SelectorComposer<Div> {
    @Wire
    private Div someInformation;

    @Override
    public void doAfterCompose(Div comp) {
        super.doAfterCompose(comp);
        Closable.instance.doAfterCompose(someInformation);
        //if you prefer implement your own static helper method
        //Closable.applyTo(someInformation);
    }
}

Q: What about MVVM?

A: Just add a command-binding -> onClose="@command('handleSomeinfoClosing', event=event)".

A Hover-to-Toggle-a-CSS-Class Behavior

A completely unrelated behavior is adding a css class when the user hovers a certain component. (I know this can be done purely using the pseudo class :hover). However for this example I chose to do this, obviously you can execute more complicated client side code (HoverToggleClass.java).

public class HoverToggleClass implements Composer<Component> {
    private String cssClass;

    static Map<String, HoverToggleClass> cache = new ConcurrentHashMap<>();

    public HoverToggleClass(String cssClass) {
        this.cssClass = cssClass;
    }

    public static HoverToggleClass instanceFor(String cssClass) {
        return cache.computeIfAbsent(cssClass, HoverToggleClass::new);
    }

    @Override
    public void doAfterCompose(Component comp) throws Exception {
        comp.setWidgetListener("onMouseOver", String.format("jq(this).toggleClass('%s', true);", cssClass));
        comp.setWidgetListener("onMouseOut", String.format("jq(this).toggleClass('%s', false);", cssClass));
    }
}
  • doAfterCompose adds two client side listeners: to toggle the desired css class only at client side (this time, sending no event to the server, just client side)

More interesting is the Constructor using an argument, which naturally prevents instantiation without a parameter.

    <!-- So the following won't work: -->
    <label apply="zk.example.HoverToggleClass" value="hover me"/>

    <!-- Neither will this -->
    <label apply="zk.example.HoverToggleClass('myHoverClass')" value="hover me"/>

One Possible way to instantiate this Composer is:

<?taglib uri="http://www.zkoss.org/dsp/web/core" prefix="c"?>
<zk>
    <label apply="${c:new1('zk.example.HoverToggleClass', 'myHoverClass')}" value="hover me"/>
</zk>

Still we can do better, by calling a static factory method, returning cached instances for the same css-class, instead of creating a new instance every time.

<?import zk.example.HoverToggleClass?>
<zk>
    <label apply="${HoverToggleClass.instanceFor('myHoverClass')}" value="hover me" />
</zk>

(also demonstrated in basics.zul)

HoverToggleClass caches previously created instances, in a Map to maximize reuse. This happens in the line ...

return cache.computeIfAbsent(cssClass, HoverToggleClass::new);

... which lazily creates the Map entry for that "cssClass" on demand. (Of course this is only beneficial as long as the UI uses finite number of css classes - entries are cached forever, but also reused forever in return).

Applying Multiple Composers

You might already be familiar with the following syntax, to apply 2 or more Composers at a time:

    <div apply="my.package.ComposerA, my.package.ComposerB" />

This works as long our Composers don't need constructor arguments, or even factory methods need to be called. Still our 2 independent behaviors from above can be used simultaneously on the same element. It just needs a slightly different syntax: The EL 3.0 syntax for collection literals - ${[a, b, c, ...]} - comes to the rescue:

    <div apply="${[Closeable.instance, HoverToggleClass.instanceFor('myHoverClass')]}"
         onClose='Clients.log("closed by user user click")'>
        multiple composers: hover to toggle css, click to detach
    </div>

(see basics.zul)

Popup Reference (A More useful example)

Let's start with this trivial case: (I know zscript is 'bad' - but ideal for demos and prototyping. I assume you already know how to put event listeners in java code, so please keep on doing that!)

<zk>
    <textbox id="tb1" context="menu1"/>
    <menupopup id="menu1">
        <menuitem label="clear" onClick='tb1.setValue("")'/>
        ... <!-- many options -->
    </menupopup>

    <textbox id="tb2" context="menu2"/>
    <menupopup id="menu2">
        <menuitem label="clear" onClick='tb2.setValue("")'/>
        ... <!-- the exact same options again -->
    </menupopup>

     ... <!-- repeat -->
</zk>

The same <menupopup> is repeated multiple times for each textbox. While the components are completely identical, the difference lies in the onClick-listener. It's the same xyz.setValue("") method call only on a different object each time.

Q: Who even does that?

A: Trust me I've seen this or something similar many times ... and that's why I decided to write this article in the first place.

Q: What about <templates>?

A: They might reduce the amount of code you have to write, still each template instance creates a new component.

Obviously we can't reduce the number of inputs required in our form, but what we can do is share the menu popup.

<zk>
    <textbox id="tb1" context="menu"/>
    <textbox id="tb2" context="menu"/>
    ... <!-- repeat -->

    <menupopup id="menu">
        <menuitem label="clear" onClick='???.setValue("")'/>
        ... <!-- many options, only once -->
    </menupopup>

</zk>

Most users get stuck at the three question marks. However the solution is quite simple, each time a menu popup opens/closes it triggers an OpenEvent, which delivers the referencing component. Adding more ZSCRIPT code.

    <menupopup id="menu" 
        onOpen='if(event.isOpen()) {
                    self.setAttribute("popupReference", event.getReference());
                } else {
                    self.removeAttribute("popupReference");
                }'>
        <menuitem label="clear" onClick='popupReference.setValue("")'/>
        ... <!-- many options, only once -->
    </menupopup>

Now the referencing component is stored in a component scoped attribute, making it available as an EL variable in child components. What's left to do is to get rid of the inline ZSCRIPT code. Of course we could implement this behavior in our application composer. To avoid mixing business logic and technical concerns repeatedly, we can implement a dedicated Composer registering the onOpen-event-listener automatically.

A simple initial implementation could be PopupReferenceSimple.java

public class PopupReferenceSimple<T extends Popup> implements Composer<T> {
    @Override
    public void doAfterCompose(T popup) {
        popup.addEventListener(Events.ON_OPEN, (OpenEvent event) -> {
            if (event.isOpen()) {
                popup.setAttribute("popupReference", event.getReference());
            } else {
                popup.removeAttribute("popupReference");
            }
        });
    }
}

And the equally simple usage in popupReferenceSimple.zul

    <div id="ctrl">
        <textbox id="tb1" context="menu"/>
        <textbox id="tb3" context="menu"/>
        <textbox id="tb4" context="menu"/>
        <textbox id="tb5" context="menu"/>

        <menupopup id="menu" apply="zk.example.PopupReferenceSimple">
            <menuitem id="menuitemClear" label="clear" onClick='popupReference.setValue("")'/>
        </menupopup>
    </div>

To clean up the inline onClick-event-listener can be replaced in Java (MVC) as follows.

    @Listen("onClick = #menuitemClear") 
    public void clearTextbox(Event event) {
        Textbox tb = (Textbox) event.getTarget().getAttribute("popupReference", true); //true - search in parent scopes
        tb.setValue("");
    }

In MVVM this still leaves us with the textbox component leaking into our View Model:

    <menupopup id="menu" apply="zk.example.PopupReferenceSimple">
        <menuitem label="clear" onClick="@command('clearText', textbox=popupReference)"/>
    </menupopup>

In this simple case passing the component Id or a custom component attribute might be enough to identify the property in the View Model code.

    <menuitem label="clear" 
             onClick="@command('clearText', textboxId=popupReference.id)"/>
   <!-- OR using another custom attribute -->
   <menuitem label="clear" 
             onClick="@command('clearText', textboxId=popupReference.getAttribute('vmProperty'))"/>

Specifying additional unique component IDs or adding more custom attributes doesn't really scale well. That's why the example doesn't end here and continues with a typical scenario where we have a shared context menu for listitems in a listbox - rendered using a ListModel.

Listbox with shared Listitem Popup

In this scenario we are only interested in the actual value of each listitem, ideally corresponding to the template variable each from the list model. Especially in MVVM we don't care about the component, just the raw data. As an solution I added a more versatile Composer - PopupReference.java - supporting an optional mapping to convert from the popup reference component into an arbitrary object suitable in the application code. (50+ lines of code make it clear, why it is desirable to implement such a behavior only once, and reuse it via composition).

This now allows seamless MVVM integration with a listbox (here displaying a list of Locales). It reuses the same context popup for all listitems. The view model will only ever receive the actual Locale object.

Let's look at popupReferenceListbox.zul:

<?import zk.example.PopupReference?>
<zk>
	<div viewModel="@id('vm') @init('zk.example.popupref.ListitemContextVM')">
		<listbox model="@init(vm.model)" checkmark="true" width="400px" onSelect="@command('handleSelection')">
			<custom-attributes org.zkoss.zul.listbox.rightSelect="false"/>
			<listhead>
				<listheader label="Locale"/>
				<listheader label="Language"/>
				<listheader label="Country"/>
			</listhead>
			<template name="model">
				<listitem context="menupopup1">
					<listcell label="${each}"/>
					<listcell label="${each.displayLanguage}"/>
					<listcell label="${each.displayCountry}"/>
				</listitem>
			</template>
		</listbox>

		<!-- PopupReference created in ZUL -->
		<menupopup id="menupopup1" apply="${PopupReference.forLambda(li -> li.value)}">
			<menuitem label="Menuitem 1" onClick="@command('contextClick1', contextLocale=popupReference)" />
			<menuitem label="Menuitem 2" onClick="@command('contextClick2')" />
		</menupopup>
               ...
	</div>
</zk>

The factory method PopupReference.forLambda leverages an EL 3 lambda expression to convert from a component (org.zkoss.zul.Listitem) to its actual model value (java.util.Locale).

Menuitem 1 and Menuitem 2 show alternative @Command bindings for the popupReference parameter as an explicit @BindingParam or an implicitly resolved @ScopeParam. Both ways avoid direct component access in the view model.

Extract from the view model class PopupReferenceListboxVM.java

    @Command
    public void contextClick1(@BindingParam("contextLocale") Locale contextLocale) {
        Clients.log("Menuitem 1: clicked for " + contextLocale);
    }

    @Command
    public void contextClick2(@ScopeParam(POPUP_REFERENCE_ATTR) Locale popupReference) {
        Clients.log("Menuitem 2: clicked for " + popupReference);
    }

The code also contains an example how to create the Composer from java code (e.g. for more complext dynamic/conditional mapping) and still apply it in the zul file.

Worth mentioning is the fact that PopupReference adds an onOpen-event-listener implementing the Deferrable-interface. While this is optional the benefits are that the onOpen-events are not sent to the server immediately. Instead they are deferred, waiting for the menuitem's onClick event followed by another onOpen-event (open=false) in the same AuRequest. While this naturally reduces the number network round trips it avoids the (in this case unnecessary) loading animation of the popup and takes care of adding/removing the popupReference-attribute in the same request/response cycle.

Here the effective sequence of commands sent in a single websocket message or ajax request:

cmd_0: "onOpen"
cmd_1: "onClick"
cmd_2: "onOpen"
data_0: "{"open":true,"reference":"cTwPr"}"
data_1: "{"pageX":283,"pageY":215,"which":1,"x":283,"y":215}"
data_2: "{"open":false}"
uuid_0: "cTwP6"
uuid_1: "cTwP8"
uuid_2: "cTwP6"

Summary

Composers provide a flexible way to implement reusable component features and apply them to multiple components (or multiple composers to one component) - avoiding excessive component class hierarchies - promoting composition.

Q: When can I use this new feature: Composers?

A: Actually the Composer interface was added in ZK 3.0.0 - so practically available everywhere :P - if not, it's time to upgrade.

Example Code

Available on github - checkout the README.md for running instructions.


Comments



Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.