Composable Composers - Creating a Popup Reference"

From Documentation
m
Line 265: Line 265:
 
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 <code>OpenEvent</code>-event, which delivers the referencing component.
 
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 <code>OpenEvent</code>-event, which delivers the referencing component.
  
<source  lang="xml">
+
<source  lang="xml" high="7">
 
     <menupopup id="menu"  
 
     <menupopup id="menu"  
 
         onOpen='if(event.isOpen()) {
 
         onOpen='if(event.isOpen()) {
Line 275: Line 275:
 
         ... <!-- many options, only once -->
 
         ... <!-- many options, only once -->
 
     </menupopup>
 
     </menupopup>
 +
</source>
 +
 +
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. But then we implement it over an over again. Luckily we can use the Composer interface mentioned above in the same way.
 +
 +
<source lang="java">
 +
public class PopupReference implements Composer<Popup>
 
</source>
 
</source>
  

Revision as of 10:39, 28 June 2019

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

Author
Robert Wenzel, Engineer, Potix Corporation
Date
June, 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, 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 often leads to multiple specialized implementations of

or <label> component in separate classes.

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?

Composers as Re-Usable Behaviors

For these scenarios ZK provides Composers which are called 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 wiring, annotation scanning and binding behavior (which is the price for abstraction and convenience).

Still all these composers implement the Composer [LINK] 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, 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 useful. In case prior validation failed or the component should be reused - so instead of detaching simply making it invisible.

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

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) which triggers an "onClose" event (don't detach immediately)
  • "onClose" event listener (with a lower priority, so that it can be intercepted by calling event.stopPropagation)

Mentionable is the static field instance. Since the Closable composer is stateless a single instance can be reused as many times as needed saving additional memory.

Inside a zul file the usage may look like this.

<?import zk.example.Closeable?>
<zk>
    <div apply="your.package.ApplicationComposer">
        <label value="click to detach" apply="zk.example.Closeable"/> <!-- creates a new instance -->
        <separator/>
        <label value="click to detach" apply="${Closeable.instance}"/> <!-- use the shared instance -->
        <separator/>
        <div id="someInformation" apply="${Closeable.instance}"/>
            Please click this area away to accept our terms and conditions.
            Listed below:
            <grid>...</grid>
        </div>
    </div>
<zk/>

Besides the auto-closing behavior, your ApplicationComposer can now add an event listener for the "onClose" event, for use case specific actions.

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.

    @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'm using Java. 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 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 might be to add 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 we chose to do this, obviously you can execute more complicated client side code.

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 to client side listeners: to toggle the desired css class only at client side (sending no event to the server)

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>

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 be already familiar with the following syntax to apply 2 or more Composers at a time:

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

Our 2 independent behaviors from above can also be used simultaneously on the same element. But need a slightly different syntax in order to provide the cached instances. 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>

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. But then we implement it over an over again. Luckily we can use the Composer interface mentioned above in the same way.

public class PopupReference implements Composer<Popup>

Summary

Comments



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