Composable Composers - Creating a Popup Reference"

From Documentation
Line 222: Line 222:
 
</source>
 
</source>
  
Also demonstrated in [https://github.com/zkoss-demo/zk-popup-reference/blob/master/src/main/webapp/basics.zul#L14-L27 basics.zul].
+
(see [https://github.com/zkoss-demo/zk-popup-reference/blob/master/src/main/webapp/basics.zul#L14-L27 basics.zul].)
  
 
= Popup Reference (A More useful example) =
 
= Popup Reference (A More useful example) =

Revision as of 04:28, 4 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>

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-event, which delivers the referencing component.

    <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 that's more like it: the referencing component can be stored in a component scoped attribute. And is then available as a variable in child components. What's left to do is to get rid of the inline script code. Of course we could implement this behavior in our composer.To avoid implementing it over and over again we can add a real java class implementation of the onOpen-event-listener explicitely or we can use the Composer interface mentioned above to apply the event listener automatically from a zul file - resulting in cleaner MVVM separation.

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");
            }
        });
    }
}
    <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>

The inline onClick-event-listener can be replaced in MVC:

    @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 ViewModel code.

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

Listbox with shared Listitem Popup

As an idea I added a more versatile version using a mapper to supply a mapping from the popup reference component into a suitable value in your application code. LINK CODE

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

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

    @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);
    }

For more usage examples please check out the source code. LINK ME

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 event is not sent to the server immediately. Instead they are deferred, waiting for the 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 loading animation of the popup and takes care of adding/removing the popupReference in the same request/response cycle.

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

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.

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.