Thursday, June 27, 2013

ZK Decimalbox Support Scientific Notation


Introduction

Currently ZK Decimalbox does not support scientific notation, this article describe how to customize it to support scientific notation.

Result

View demo online
http://screencast.com/t/ZtbTj0H2M

Pre-request

Extending and Customizing ZK Component as Separate Component
http://ben-bai.blogspot.tw/2013/01/extending-and-customizing-zk-component.html

Program

support_scientific_notation.zul

<zk>
    decimalbox:
    <decimalbox constraint="no empty"
        textAlign="right">
        <attribute name="onChange"><![CDATA[
            lb.setValue(self.getValue() + "");
            lbTwo.setValue(self.getValue().intValue() + "");
        ]]></attribute>
        <attribute name="onScientificNotation"><![CDATA[
            lb.setValue(self.getValue() + "");
            lbTwo.setValue(self.getValue().intValue() + "");
        ]]></attribute>
    </decimalbox>
    <div />
    BigDecimal value: <label id="lb" />
    <div />
    int value: <label id="lbTwo" />
    <div style="margin-top: 30px;" />
    rounded decimalbox
    <decimalbox mold="rounded" constraint="no empty"
        textAlign="right">
        <attribute name="onChange"><![CDATA[
            lbThree.setValue(self.getValue() + "");
            lbFour.setValue(self.getValue().intValue() + "");
        ]]></attribute>
        <attribute name="onScientificNotation"><![CDATA[
            lbThree.setValue(self.getValue() + "");
            lbFour.setValue(self.getValue().intValue() + "");
        ]]></attribute>
    </decimalbox>
    <div />
    BigDecimal value: <label id="lbThree" />
    <div />
    int value: <label id="lbFour" />
</zk>


Decimalbox.java

Custom java class for decimalbox, support scientific notation, also support text align attribute.

package test.components;

import java.math.BigDecimal;
import java.util.Map;

import org.zkoss.zk.au.out.AuInvoke;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.InputEvent;
import org.zkoss.zk.ui.util.Clients;


/** Tested with ZK 6.0.2
 * 
 * @author benbai123
 *
 */
public class Decimalbox extends org.zkoss.zul.Decimalbox {

    private String _textAlign = "left";
    private static final long serialVersionUID = -4797186118579295417L;
    static {
        // listen custom event
        addClientEvent(Decimalbox.class, "onCheckErrorValue", CE_IMPORTANT | CE_DUPLICATE_IGNORE | CE_NON_DEFERRABLE);
    }
    public void setTextAlign (String textAlign) {
        // update if there is a different value
        if (textAlign != null // no null
            && !textAlign.isEmpty() // no empty
            && !textAlign.equals(_textAlign)) { // value is changed
            _textAlign = textAlign;
            smartUpdate("textAlign", _textAlign);
        }
    }
    public String getTextAlign () {
        return _textAlign;
    }

    // override
    // process client event
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        // process custom event
        if (cmd.equals("onCheckErrorValue")) {
            // get data map
            Map<String, Object> data = request.getData();
            // get value
            String value = (String)data.get("value");
            boolean valid = true;
            BigDecimal val = null;
            // try create a BigDecimal with the wrong value
            try {
                val = new BigDecimal(value);
            } catch (Exception e) {
                // failed to create BigDecimal, not valid
                valid = false;
            }
            if (valid) {
                // it is valid for BigDecimal
                setValueDirectly(val);
                // post onScientificNotation event
                Events.postEvent("onScientificNotation", this, null);
                // clear the error status if any
                clearErrorMessage();
                Clients.clearWrongValue(this);
            } else {
                // it is invalid for BigDecimal
                // really do _markError at client side
                // the code below will call function _realMarkError
                // of the widget class at client side 
                response("DecimalboxSupportScientificNotation", new AuInvoke(this, "_realMarkError", (String[])null));
            }
        } else 
            super.service(request, everError);
    }
    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer)
        throws java.io.IOException {
        super.renderProperties(renderer);
        if (!"left".equals(_textAlign)) {
            render(renderer, "textAlign", _textAlign);
        }
    }
}


Decimalbox.js

Custom widget class for decimalbox, will ask server at first while do _maskError.

test.components.Decimalbox = zk.$extends(zul.inp.Decimalbox, {
    _textAlign: 'left',
    setTextAlign: function (v) {
        if (v != this._textAlign) {
            this._textAlign = v;
            var inp = this.getInputNode();
            if (inp)
                jq(inp).css('text-align', this._textAlign);
        }
    },
    bind_: function () {
        this.$supers('bind_', arguments);
        jq(this.getInputNode()).css('text-align', this._textAlign);
    },
    getAllowedKeys_: function () {
        // add 'E' into allowed keys
        return this.$supers('getAllowedKeys_', arguments) + 'E';
    },
    _markError: function (msg, val, noOnError, doDirectly) {
        if (doDirectly) {
            // do it without server confirmation
            this.$supers('_markError', arguments);
        } else {
            // store info only, waiting for server confirmation
            this._markErrorInfo = {msg: msg, val: val, noOnError: noOnError};
            // ask server side custom validation
            var wgt = this,
                timer = this._onCheckErrorValueTimer;
            // prevent duplicated event
            if (timer)
                clearTimeout(timer);
            this._onCheckErrorValueTimer = setTimeout (function () {
                wgt.fire('onCheckErrorValue', {value: jq(wgt.getInputNode()).val()});
            }, 300);
        }
    },
    // called by server
    // do _markError directly with the stored information
    _realMarkError: function () {
        var info = this._markErrorInfo;
        if (info) {
            this._markError(info.msg, info.val, info.noOnError, true);
            this._markErrorInfo = null;
        }
    }
});


zk.xml

Specify lang-addon.xml

<zk>
    <language-config>
        <addon-uri>/WEB-INF/lang-addon.xml</addon-uri>
    </language-config>
</zk>


lang-addon.xml

Define the customized decimalbox

<language-addon>
    <addon-name>decimalboxcustomization</addon-name>
    <language-name>xul/html</language-name>

    <!-- extends ZK Decimalbox and use custom classes -->
    <component>
        <component-name>decimalbox</component-name>
        <component-class>test.components.Decimalbox</component-class>
        <widget-class>test.components.Decimalbox</widget-class>
        <mold>
            <mold-name>default</mold-name>
        </mold>        
        <mold>
            <mold-name>rounded</mold-name>
        </mold>
    </component>
</language-addon>


zk.wpd

Used to load widget class.

<package name="test.components" language="xul/html" depends="zul.inp">
    <widget name="Decimalbox" />
</package>


References

InputWidget.js
https://github.com/zkoss/zk/blob/master/zul/src/archive/web/js/zul/inp/InputWidget.js

InputElement.java
https://github.com/zkoss/zk/blob/master/zul/src/org/zkoss/zul/impl/InputElement.java

Download

Sunday, June 23, 2013

ZK Custom Taglib


Simple Note

Introduction

This article describe how to define custom taglib in ZK.

Description

You can define taglib in two ways:

1. Put tld file under WebContent, and specify it in zul file with "/PATH/TO/YOUR/TLDFile" directly.

Note the path here starts from WebContent so you can specify it like "/WEB-INF/..."

2. Put tld file under classpath, and specify it in classpath/metainfo/tld/config.xml with a specific URI, and specify the specific URI in zul file.

Note the path specified in config.xml starts from classpath, the tld file should under the classpath of your web project or in a jar file.


Reference

ZK Custom Taglib
http://books.zkoss.org/wiki/ZUML_Reference/ZUML/Processing_Instructions/taglib/Custom_Taglib

Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/General/TaglibTest

Saturday, June 22, 2013

ZK CDT: Add EventListener into Model to Create UpdatableTextNote


Introduction

This is the 7th article of ZK CDT (ZK Component Development Tutorial) walkthrough, this article describe how to add event listener into model to update components while the data in model is changed.

Add Listener into Model

The goal of this walkthrough is to create a quicknote component that help you do some quick note on web page (similar to you crop some screenshot, highlight something and add some text in photo editor), and describe each part of a component through each article in this walkthrough.

This is the 7th part: Add listener into model.

Result

View demo online:
http://screencast.com/t/48GXGDlDIPaO

What you changed at client side will be updated to server side, and update all other components that use the same model at client side.

Any change via API of model within event thread (e.g., with a button click) at server side will also update all components that use the model at client side.

All of the works are handled by model with the event listener pattern automatically, do not need to do anything in  VM or Composer.

Pre-request

ZK CDT: Create Custom Event for RecordableTextNote
http://ben-bai.blogspot.tw/2013/06/zk-cdt-create-custom-event-for.html


Program

updatabletextnote.zul

Contains two updatabletextnote, they use the same model so their status will be sync by model automatically.

<zk>
    <!-- 
        completely new sample for updatabletextnote,
        two updatabletextnote use the same model,
        will sync automatically
     -->
    <div apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('custom.zk.samples.quicknote.UpdatableTextNoteVM')">
        <button label="remove selected" onClick="@command('removeSelected')" />
        <button label="clear" onClick="@command('clear')" />
        <hlayout>
            <!-- The updatabletextnote component that will cover
                its children by a mask
                you can click on the mask to add a textarea
                and type text in it
            -->
            <updatabletextnote width="150px" height="55px"
                model="@load(vm.model)"
                selectedTextNoteIndex="@save(vm.selectedIndex)"
                onTextNoteBlockChanged="@command('checkResult')">
                <hlayout style="margin-top: 15px;">
                    <div width="35px" />
                    <label value=" x " />
                    <div width="35px" />
                    <label value=" = 6" />
                </hlayout>
            </updatabletextnote>
            <updatabletextnote width="150px" height="55px"
                model="@load(vm.model)"
                selectedTextNoteIndex="@save(vm.selectedIndex)"
                onTextNoteBlockChanged="@command('checkResult')">
                <hlayout style="margin-top: 15px;">
                    <div width="35px" />
                    <label value=" + " />
                    <div width="35px" />
                    <label value=" = 5" />
                </hlayout>
            </updatabletextnote>
            Result: <label value="@load(vm.result)"
                        style="@load(vm.result eq 'Wrong'? 'color: red;' : '')" />
        </hlayout>
    </div>
</zk>


UpdatableTextNoteVM.java

VM used in updatabletextnote.zul, provide data, do command.

package custom.zk.samples.quicknote;

import java.util.ArrayList;
import java.util.List;

import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
import custom.zk.components.quicknote.Data.TextNoteData;
import custom.zk.components.quicknote.model.UpdatableTextNoteModel;
/**
 * VM used by updatabletextnote.zul<br>
 * <br>
 * As you can see, simply provide data and handle command,<br>
 * will not refresh model by VM<br>
 * 
 * @author benbai123
 *
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
public class UpdatableTextNoteVM {
    private UpdatableTextNoteModel _model;
    private int _selectedIndex = -1;
    private String _result = "Wrong";

    // getters, setters
    public UpdatableTextNoteModel getModel () {
        if (_model == null) {
            List l = new ArrayList();
            l.add(new TextNoteData(5, 10, 25, 25, ""));
            l.add(new TextNoteData(50, 10, 25, 25, ""));
            _model = new UpdatableTextNoteModel(l);
        }
        return _model;
    }
    public void setSelectedIndex (int selectedIndex) {
        _selectedIndex = selectedIndex;
    }
    public String getResult () {
        return _result;
    }
    @Command
    @NotifyChange("result")
    public void checkResult () {
        List<TextNoteData> datas = getModel().getTextNoteData();
        try {
            int valOne = Integer.parseInt(datas.get(0).getText());
            int valTwo = Integer.parseInt(datas.get(1).getText());
    
            if (valOne*valTwo == 6
                && valOne+valTwo == 5) {
                _result = "Correct";
            } else {
                _result = "Wrong";
            }
        } catch (Exception e) {
            _result = "Wrong";
        }
    }
    @Command
    public void removeSelected () {
        List<TextNoteData> datas = getModel().getTextNoteData();
        if (_selectedIndex >= 0
            &&datas.size() > _selectedIndex) {
            getModel().remove(datas.get(_selectedIndex));
        }
    }
    @Command
    public void clear () {
        getModel().clear();
    }
}


UpdatableTextNote.java

Java class of UpdatableTextNote component, extends RecordableTextNote and add TextNoteDataListener into model to handle TextNoteDataChangeEvent.

package custom.zk.components.quicknote;

import org.zkoss.json.JSONObject;

import custom.zk.components.quicknote.Data.TextNoteData;
import custom.zk.components.quicknote.event.TextNoteDataChangeEvent;
import custom.zk.components.quicknote.event.TextNoteDataListener;
import custom.zk.components.quicknote.model.UpdatableTextNoteModel;
import custom.zk.components.quicknote.model.TextNoteModel;

/** UpdatableTextNote, extends {@link custom.zk.components.quicknote.RecordableTextNote},<br>
 * hold UpdatableTextNoteModel ({@link custom.zk.components.quicknote.model.UpdatableTextNoteModel})<br>
 * and add event listener ({@link custom.zk.components.quicknote.event.TextNoteDataListener})<br>
 * into model<br>
 * <br>
 * One new thing:<br>
 *         Add event listener into model, the onChange function of event listener<br>
 *         will be called when data of model is changed<br>
 * <br>
 * Two effects:<br>
 *         will update change from server side to client side if<br>
 *         the change is made within UI thread<br>
 * <br>
 *         If two or more components use the same UpdatableTextNoteModel,<br>
 *         and one of them is changed at client side, all other components<br>
 *         will be updated when the change event is sent to server<br>
 * <br>
 *         the effects is similar to multiple components use<br>
 *         the same model and refresh whole model when there are<br>
 *         any changes<br>
 * <br>
 *         but there are two advantages with the listener pattern<br>
 * <br>
 * Advantages:<br>
 *         Clean code: the update is handled by model and listener in component,<br>
 *             you do not need to add a lot of @Command, @NotifyChange<br>
 *             to update the model<br>
 * <br>
 *         Save bandwidth and improve client side performance:<br>
 *             it can update only the specific text note block<br>
 *             instead of refresh whole model<br>
 * <br>
 * @author benbai123
 *
 */
public class UpdatableTextNote extends RecordableTextNote {

    private static final long serialVersionUID = -5105526564611614334L;

    // prevent unnecessary update
    private boolean _ignoreModelChangedEvent = false;
    // model, support text note data event listener
    private UpdatableTextNoteModel _model;
    // event listener, listen the change of model
    private TextNoteDataListener _textNoteDataListener;

    // setter
    public void setModel (TextNoteModel model) {
        // model is changed
        if (model != _model) {
            if (_model != null) {
                // remove listener from old model
                _model.removeListDataListener(_textNoteDataListener);
            }
            if (model != null) {
                if (model instanceof UpdatableTextNoteModel) {
                    _model = (UpdatableTextNoteModel)model;
                } else {
                    // create new model if the instance of model
                    // is not UpdatableTextNoteModel
                    // in this case, each component will use different instance of
                    // UpdatableTextNoteModel even they use the same model
                    _model = new UpdatableTextNoteModel(model.getTextNoteData());
                }
                // add listener into model
                initTextNoteDataListener();
            } else {
                _model = null;
            }
        }
        // call super
        super.setModel(_model);
    }
    private void initTextNoteDataListener () {
        if (_textNoteDataListener == null) {
            // create listener as needed
            _textNoteDataListener = new TextNoteDataListener () {
                public void onChange (TextNoteDataChangeEvent event) {
                    onTextNoteDataChange(event);
                }
            };
        }
        // add listener into model
        _model.addTextNoteDataListener(_textNoteDataListener);
    }
    // called by listener
    private void onTextNoteDataChange (TextNoteDataChangeEvent event) {
        // change type
        int type = event.getType();
        if (!_ignoreModelChangedEvent) {
            if (type == TextNoteDataChangeEvent.UPDATE_NOTE_BLOCK) {
                // update specific text note block
                smartUpdate("textNoteBlockToUpdate", getBlockInfo(event));
            } else if (type == TextNoteDataChangeEvent.ADD_NOTE_BLOCK) {
                // add a new text note block
                smartUpdate("textNoteBlockToAdd", getBlockInfo(event));
            } else if (type == TextNoteDataChangeEvent.REMOVE_NOTE_BLOCK) {
                // remove a specific text note block
                smartUpdate("textNoteBlockToRemove", getBlockInfo(event));
            } else if (type == TextNoteDataChangeEvent.REFRESH_MODEL) {
                // re-render all text note blocks
                updateNoteBlocks();
            }
        }
    }
    // get the JSON String that represents a text note block
    private String getBlockInfo (TextNoteDataChangeEvent event) {
        // index of changed note block
        int index = event.getIndex();
        // data of changed note block
        TextNoteData data = event.getData();
        JSONObject obj = new JSONObject();
        obj.put("index", index);
        obj.put("left", data.getPosX());
        obj.put("top", data.getPosY());
        obj.put("width", data.getWidth());
        obj.put("height", data.getHeight());
        obj.put("text", data.getText());
        return obj.toJSONString();
    }
    // override
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        _ignoreModelChangedEvent = true;
        super.service(request, everError);
        _ignoreModelChangedEvent = false;
    }
}


UpdatableTextNote.js

Widget class of UpdatableTextNote component, extends RecordableTextNote and add functions to update, add or remove a specific text note block.

/** UpdatableTextNote
 * 
 * Extends RecordableTextNote and add functions to update, add or remove
 * a specific text note block
 * 
 * No new thing
 * 
 */
custom.zk.components.quicknote.UpdatableTextNote = zk.$extends(custom.zk.components.quicknote.RecordableTextNote, {
    // update a specific text note block
    setTextNoteBlockToUpdate: function (blockInfo) {
        this._updateNoteBlock(jq.evalJSON(blockInfo));
    },
    // add a specific text note block
    setTextNoteBlockToAdd: function (blockInfo) {
        var data = jq.evalJSON(blockInfo),
            index = data.index,
            blocks = jq(this.$n()).find('.' + this.getZclass() + '-noteblock'),
            len = blocks.length; // keep length before the new one is added
        // add to the tail at first
        this._renderNoteBlock(data.left, data.top, data.width, data.height, data.text);

        // insert case,
        // the specified index is smaller than the length
        // of text note blocks
        if (index < len) { // insert
            // newDom: last block
            // ref: the block at the specified index
            var newDom = this._getTextNoteBlockByIndex(len),
                ref = this._getTextNoteBlockByIndex(index);
            // insert newDom before ref
            ref.parentNode.insertBefore(newDom, ref);
        }
    },
    // remove a specific text note block
    setTextNoteBlockToRemove: function (blockInfo) {
        // find block by specified index
        var block = this._getTextNoteBlockByIndex(jq.evalJSON(blockInfo).index);
        if (block) {
            // remove it
            block.parentNode.removeChild(block);
        }
    },
    // update a specific note block
    _updateNoteBlock: function (data) {
        // get attributes from data
        // find the block by specified index
        var index = data.index,
            block = this._getTextNoteBlockByIndex(index),
            textarea = block.firstChild,
            bstyle = block.style,
            tstyle = textarea.style,
            zattr = textarea.zkAttributes;

        // update block,
        // also update the stored attributes of block
        zattr.left = bstyle.left = data.left + 'px';
        zattr.top = bstyle.top = data.top + 'px';
        zattr.width = tstyle.width = data.width + 'px';
        zattr.height = tstyle.height = data.height + 'px';
        zattr.text = data.text;
        jq(textarea).val(data.text);
    },
    // find text note block by the given index
    _getTextNoteBlockByIndex: function (index) {
        var current = this.$n('mask').nextSibling,
            idx = 0;
    
        // for each text note block
        while (current) {
            if (idx == index) {
                return current;
            }
            current = current.nextSibling;
            idx++;
        }
        return null;
    }
});


UpdatableTextNoteModel.java

Model used in UpdatableTextNote component, extends TextNoteModel and override functions to fire event when the content of model is changed.

package custom.zk.components.quicknote.model;

import java.util.ArrayList;
import java.util.List;
import custom.zk.components.quicknote.Data.TextNoteData;
import custom.zk.components.quicknote.event.TextNoteDataChangeEvent;
import custom.zk.components.quicknote.event.TextNoteDataListener;
/** UpdatableTextNoteModel<br>
 * <br>
 * <p>Extends TextNoteModel and override functions to handle data change event,
 * will fire proper event to each event listener within this model to
 * update corresponding component</p>
 * 
 * <p>Used by {@link custom.zk.components.quicknote.UpdatableTextNote}</p>
 * 
 * @author benbai123
 *
 */
public class UpdatableTextNoteModel extends TextNoteModel {

    private List<TextNoteDataListener> _listeners = new ArrayList<TextNoteDataListener>();

    // Constructor
    @SuppressWarnings("rawtypes")
    public UpdatableTextNoteModel (List textNodeDatas) {
        super(textNodeDatas);
    }
    // fire data change event to each event listener
    protected void fireEvent(TextNoteData data, int index, int type) {
        final TextNoteDataChangeEvent evt = new TextNoteDataChangeEvent(this, data, index, type);
        for (TextNoteDataListener listener : _listeners) {
            listener.onChange(evt);
        }
    }
    // add an event listener
    public void addTextNoteDataListener (TextNoteDataListener listener) {
        if (listener == null) {
            throw new NullPointerException("Listener cannot be null");
        }
        _listeners.add(listener);
    }
    // remove an event listener
    public void removeListDataListener(TextNoteDataListener listener) {
        _listeners.remove(listener);
    }
    // get index of a text note data
    public int indexOf (TextNoteData data) {
        return super.getTextNoteData().indexOf(data);
    }
    // override
    // add
    public void add (TextNoteData data) {
        super.add(data);
        int index = getTextNoteData().indexOf(data);
        // fire event to add text note block at client side
        fireEvent(data, index, TextNoteDataChangeEvent.ADD_NOTE_BLOCK);
    }
    // add at specific position
    public void add (int index, TextNoteData data) {
        super.add(index, data);
        // fire event to add text note block at client side
        fireEvent(data, index, TextNoteDataChangeEvent.ADD_NOTE_BLOCK);
    }
    // remove
    public void remove (TextNoteData data) {
        int index = getTextNoteData().indexOf(data);
        super.remove(data);
        // fire event to remove text note block at client side
        fireEvent(data, index, TextNoteDataChangeEvent.REMOVE_NOTE_BLOCK);
    }
    public void update (int index, TextNoteData data) {
        super.getTextNoteData().remove(index);
        super.add(index, data);
        // fire event to update text note block at client side
        fireEvent(data, index, TextNoteDataChangeEvent.UPDATE_NOTE_BLOCK);
    }
    // clear all data
    public void clear () {
        super.clear();
        // refresh whole model
        fireEvent(null, -1, TextNoteDataChangeEvent.REFRESH_MODEL);
    }
}


TextNoteDataListener.java

Define the method used to listen when the content of UpdatableTextNoteModel is changed.

package custom.zk.components.quicknote.event;

/** Define the method used to listen when the content of
 * UpdatableTextNoteModel ({@link custom.zk.components.quicknote.model.UpdatableTextNoteModel}) is changed
 * 
 * @author benbai123
 * @see custom.zk.components.quicknote.model.UpdatableTextNoteModel
 * @see custom.zk.components.quicknote.event.TextNoteDataChangeEvent
 */
public interface TextNoteDataListener {
    /** Sent when the contents of text note blocks have been changed.
     */
    public void onChange(TextNoteDataChangeEvent event);
}


TextNoteDataChangeEvent.java

Defines an event that encapsulates changes to text note blocks.

package custom.zk.components.quicknote.event;

import custom.zk.components.quicknote.Data.TextNoteData;
import custom.zk.components.quicknote.model.TextNoteModel;

/**
 * Defines an event that encapsulates changes to text note blocks. 
 *
 * @author benbai123
 */
public class TextNoteDataChangeEvent {
    /** Identifies whole model should be refreshed */
    public static final int REFRESH_MODEL = 0;
    /** Identifies a text note block should be updated */
    public static final int UPDATE_NOTE_BLOCK = 1;
    /** Identifies a new text note block should be addad */
    public static final int ADD_NOTE_BLOCK = 2;
    /** Identifies a text note block should be removed */
    public static final int REMOVE_NOTE_BLOCK = 3;
    /* the model that trigger this event */
    private TextNoteModel _model;
    /* the affected data (which represents a text note block) */
    private TextNoteData _data;
    /* index of affected text note block */
    private int _index;
    /* action type of this event */
    private int _type;

    /** Constructor
     * 
     * @param data the changed text note data for a text note block if any
     * @param index the index of the changed text note block if any
     * @param type one of {@link #REFRESH_MODEL}, {@link #UPDATE_NOTE_BLOCK},
     * {@link #ADD_NOTE_BLOCK}, {@link #REMOVE_NOTE_BLOCK}
     */
    public TextNoteDataChangeEvent (TextNoteModel model, TextNoteData data,
            int index, int type) {
        _model = model;
        _data = data;
        _index = index;
        _type = type;
    }
    // getters
    public TextNoteModel getModel () {
        return _model;
    }
    public TextNoteData getData () {
        return _data;
    }
    public int getIndex () {
        return _index;
    }
    public int getType () {
        return _type;
    }
}


zk.wpd

Define components under "custom.zk.components.quicknote"

* only the added part, not the full code

NOTE: more components will be added with other articles later

    ...
    <widget name="UpdatableTextNote" />
    ...


lang-addon.xml

Define all components in the project

* only the added part, not the full code

NOTE: more components will be added with other articles later

    ...
    <!-- 7th, updatabletextnote component
        extends recordabletextnote and handle data change event
        within model to update component at client side as needed

     -->
    <component>
        <component-name>updatabletextnote</component-name>
        <extends>recordabletextnote</extends>
        <component-class>custom.zk.components.quicknote.UpdatableTextNote</component-class>
        <widget-class>custom.zk.components.quicknote.UpdatableTextNote</widget-class>
    </component>
    ...


References

Listbox.java
https://github.com/zkoss/zk/blob/master/zul/src/org/zkoss/zul/Listbox.java

AbstractListModel.java
https://github.com/zkoss/zk/blob/master/zul/src/org/zkoss/zul/AbstractListModel.java

ListDataListener.java
https://github.com/zkoss/zk/blob/master/zul/src/org/zkoss/zul/event/ListDataListener.java

ListDataEvent.java
https://github.com/zkoss/zk/blob/master/zul/src/org/zkoss/zul/event/ListDataEvent.java

Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Components_Development__Series/001_walkthrough/ZKQuickNote

updatabletextnote_component.swf
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/component_development_series/001_walkthrough/updatabletextnote_component.swf

Saturday, June 8, 2013

ZK CDT: Create Custom Event for RecordableTextNote


Introduction

This is the 6th article of ZK CDT (ZK Component Development Tutorial) walkthrough, this article describe why and how to wrap the request data by a custom event.

Create Custom Event

The goal of this walkthrough is to create a quicknote component that help you do some quick note on web page (similar to you crop some screenshot, highlight something and add some text in photo editor), and describe each part of a component through each article in this walkthrough.

This is the 6th part: Create custom event.

Result

View demo online:
http://screencast.com/t/w974QUMlI

What you changed at client side will be updated to server side directly now.

Pre-request

ZK CDT: Fire Event to Server to Create a SelectableTextNote
http://ben-bai.blogspot.tw/2013/06/zk-cdt-fire-event-to-server-to-create.html


Program

recordabletextnote.zul

Contains a recordabletextnote component, a control block that used to add/update/clear note blocks, a slider/colorbox that control the opacity/background-color of the recordabletextnote and an information block that display the changed note block.

<zk>
    <!-- several new things
    
        selectedTextNoteData="@save(vm.selectedTextNoteData, before='updateAttrs,addNoteBlock')"
                denotes save attribute selectedTextNoteData to VM before the
                command 'updateAttrs' in VM is triggered

                we need to update the data of selectedTextNoteData when
                the selected text note block is changed or when
                the attributes of selected text note block is changed
                
                but ZKBIND currently does not support specify multiple save event
                so we use @save annotation in MVVM to specify extra event

                however we still need to specify selectedTextNoteData in lang-addon.xml to
                change its direction to save at first

                without this line the data of selected text note block will be changed with
                onSelectedTextNoteBlockChanged event only

                also save it before add a new block with add button
                
                ref: http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVVM/Syntax/Data_Binding/@save

        onTextNoteBlockSelect="@command('updateAttrs')"
                trigger updateAttrs while onTextNoteBlockSelect event so it will
                trigger save action of selectedTextNoteData

        bind value of selected note block
                value="@bind(vm.selectedTextNoteData.xxx)"
                this will load/save attributes from/to selected text note data

                onChange="@command('updateModel')"
                this will trigger 'updateModel' command in VM,
                the command will refresh model with NotifyChange annotation
                update model manually since we didn't implement update mechanism
                of model

        added 'changed data' block to show which block is changed
     -->
    <div apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('custom.zk.samples.quicknote.RecordableTextNoteVM')">
        <hlayout>
            <!-- The recordabletextnote component that will cover
                its children by a mask
                you can click on the mask to add a textarea
                and type text in it
            -->
            <recordabletextnote width="700px" id="stn"
                opacity="@load(vm.opacity)" maskColor="@load(vm.maskColor)"
                model="@load(vm.model)"
                selectedTextNoteData="@save(vm.selectedTextNoteData, before={'updateAttrs','addNoteBlock'})"
                selectedTextNoteIndex="@save(vm.indexToUpdate)"
                onTextNoteBlockSelect="@command('updateAttrs')"
                onSelectedTextNoteBlockChanged="@command('updateAttrs')"
                onTextNoteBlockChanged="@command('updateChangedTextNoteData')">
                <button label="ZK Website" />
                <iframe width="100%"
                    height="1000px"
                    src="http://www.zkoss.org/"></iframe>
            </recordabletextnote>
            <vlayout>
                <!-- controll block for add/update/clear note blocks -->
                <vlayout>
                    <label value="controll block for add/update/clear note blocks" />
                    <hlayout>
                        index: <intbox value="@load(vm.indexToUpdate)" readonly="true" />
                    </hlayout>
                    <hlayout>
                        x: <intbox value="@bind(vm.textNoteData.posX)"
                                onChange="@command('updateModel')" />
                    </hlayout>
                    <hlayout>
                        y: <intbox value="@bind(vm.textNoteData.posY)"
                                onChange="@command('updateModel')" />
                    </hlayout>
                    <hlayout>
                        width: <intbox value="@bind(vm.textNoteData.width)"
                                    onChange="@command('updateModel')" />
                    </hlayout>
                    <hlayout>
                        height: <intbox value="@bind(vm.textNoteData.height)"
                                    onChange="@command('updateModel')" />
                    </hlayout>
                    <hlayout>
                        text: <textbox value="@bind(vm.textNoteData.text)"
                                    onChange="@command('updateModel')" />
                    </hlayout> 
                    <hlayout>
                        <button label="add" onClick="@command('addNoteBlock')" />
                    </hlayout>
                    <hlayout>
                        <button label="clear" onClick="@command('clearAllBlocks')" />
                    </hlayout>
                </vlayout>
                <hlayout style="margin-top: 10px;">
                    <!-- slider used to control opacity of recordabletextnote -->
                    <slider curpos="@bind(vm.opacity)" maxpos="100"
                        onScroll="@command('updateOpacity')" />
                    <!-- colorbox used to control mask color of recordabletextnote -->
                    <colorbox color="@bind(vm.maskColor)"
                        onChange="@command('updateMaskColor')" />
                </hlayout>
                <!-- information of changed note block data -->
                <vlayout style="margin-top: 10px;">
                    <label value="information of changed note block data" />
                    <hlayout>
                        changed idnex: <intbox readonly="true" value="@load(vm.changedIndex)" />
                    </hlayout>
                    <hlayout>
                        x: <intbox readonly="true" value="@load(vm.changedTextNoteData.posX)" />
                    </hlayout>
                    <hlayout>
                        y: <intbox readonly="true" value="@load(vm.changedTextNoteData.posY)" />
                    </hlayout>
                    <hlayout>
                        width: <intbox readonly="true" value="@load(vm.changedTextNoteData.width)" />
                    </hlayout>
                    <hlayout>
                        height: <intbox readonly="true" value="@load(vm.changedTextNoteData.height)" />
                    </hlayout>
                    <hlayout>
                        text: <textbox readonly="true" value="@load(vm.changedTextNoteData.text)" />
                    </hlayout> 
                </vlayout>
            </vlayout>
        </hlayout>
    </div>
</zk>


RecordableTextNoteVM.java

VM used in recordabletextnote.zul, provide data, do command and update data.

package custom.zk.samples.quicknote;

import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.ContextParam;
import org.zkoss.bind.annotation.ContextType;
import org.zkoss.bind.annotation.NotifyChange;

import custom.zk.components.quicknote.Data.TextNoteData;
import custom.zk.components.quicknote.event.TextNoteBlockUpdateEvent;

public class RecordableTextNoteVM extends SelectableTextNoteVM {

    // data from recordabletextnote component
    // there is a getter 'getSelectedTextNoteData' in
    // recordabletextnote so we can get data from it
    // via MVVM @save annotation
    TextNoteData _selectedTextNoteData;

    // data grabbed from TextNoteBlockUpdateEvent
    // there is no getter of changed text note data
    // so we need to grab it from event while
    // onTextNoteBlockChanged
    TextNoteData _changedTextNoteData;

    // index of changed text note data
    int _changedIndex;

    public void setSelectedTextNoteData (TextNoteData data) {
        if (data != null) {
            _selectedTextNoteData = data;
            // copy data for add function in super class (RenderableTextNoteVM.java)
            TextNoteData dataToAdd = super.getTextNoteData();
            dataToAdd.setPosX(data.getPosX());
            dataToAdd.setPosY(data.getPosY());
            dataToAdd.setWidth(data.getWidth());
            dataToAdd.setHeight(data.getHeight());
            dataToAdd.setText(data.getText());
        }
    }
    public TextNoteData getSelectedTextNoteData  () {
        return _selectedTextNoteData;
    }

    public TextNoteData getChangedTextNoteData () {
        return _changedTextNoteData;
    }
    public int getChangedIndex () {
        return _changedIndex;
    }
    // Override
    public TextNoteData getTextNoteData () {
        return _selectedTextNoteData != null? _selectedTextNoteData : super.getTextNoteData();
    }
    @Command
    @NotifyChange("textNoteData")
    public void updateAttrs () {
        // for trigger update
    }
    @Command
    @NotifyChange("model")
    public void updateModel () {
        // for trigger update
    }
    @Command
    @NotifyChange({"changedTextNoteData", "changedIndex"})
    public void updateChangedTextNoteData (@ContextParam(ContextType.TRIGGER_EVENT) TextNoteBlockUpdateEvent event) {
        // grab data from event then update
        _changedTextNoteData = event.getTextNoteData();
        _changedIndex = event.getIndex();
    }
}


RecordableTextNote.java

Java class of RecordableTextNote component, extends SelectableTextNote and handle note block update.

package custom.zk.components.quicknote;

import java.util.List;

import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.event.Events;

import custom.zk.components.quicknote.Data.TextNoteData;
import custom.zk.components.quicknote.event.TextNoteBlockUpdateEvent;
import custom.zk.components.quicknote.model.TextNoteModel;

/** RecordableTextNote, will receive and store which note block is changed from client side action
 * 
 * Two new things:
 * 
 * Convert request to an event by static method defined in TextNoteBlockUpdateEvent
 *         two benefits:
 *         1. Do not need to write the code to retrieve data in service method
 *         2. Can grab data from event easily in event listener since we can wrap data properly at first
 * 
 * UiException: throw this exception to notify user something wrong,
 *         will alert at client side
 * 
 * @author benbai123
 *
 */
public class RecordableTextNote extends SelectableTextNote {

    private static final long serialVersionUID = -4769807131469685854L;
    static {
        addClientEvent(SelectableTextNote.class, "onTextNoteBlockChanged", CE_IMPORTANT | CE_DUPLICATE_IGNORE | CE_NON_DEFERRABLE);
        addClientEvent(SelectableTextNote.class, "onSelectedTextNoteBlockChanged", CE_IMPORTANT | CE_DUPLICATE_IGNORE | CE_NON_DEFERRABLE);
    }
    // getter, so can save data to vm
    public TextNoteData getSelectedTextNoteData () {
        int index = getSelectedTextNoteIndex();
        if (index >= 0) {
            return getTextNoteData(index);
        }
        return null;
    }
    public TextNoteData getTextNoteData (int index) {
        List datas = getModel().getTextNoteData();
        return (datas != null && datas.size() > index)?
                (TextNoteData)datas.get(index) : null;
    }
    // process client event
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        // any text note block changed
        if (cmd.equals("onTextNoteBlockChanged")) {
            TextNoteModel model = getModel();
            if (model != null) {
                TextNoteBlockUpdateEvent event = TextNoteBlockUpdateEvent.getTextNoteBlockUpdateEvent(cmd, this, request);
                // created at client side
                if (model.getTextNoteData().size() <= event.getIndex())
                    model.add(event.getTextNoteData());
                else // already in model
                    model.update(event.getIndex(), event.getTextNoteData());
                // post event to trigger listeners if any
                Events.postEvent(event);
            } else {
                throw new UiException("Model is required !!");
            }
            // selected text note block changed
        } else if (cmd.equals("onSelectedTextNoteBlockChanged")) {
            TextNoteModel model = getModel();
            if (model != null) {
                TextNoteBlockUpdateEvent event = TextNoteBlockUpdateEvent.getTextNoteBlockUpdateEvent(cmd, this, request);
                // simply post event to trigger listeners if any
                Events.postEvent(event);
            } else {
                throw new UiException("Model is required !!");
            }
        } else 
            super.service(request, everError);
    }
}


RecordableTextNote.js

Widget class of RecordableTextNote component, extends SelectableTextNote and handle onfocus/onblur events of textarea in text note block.

/**
 * Widget class of RecordableTextNote component,
 * extends custom.zk.components.quicknote.SelectableTextNote
 * 
 * actually no new thing, just handle more events and do more
 * works to make it recordable
 * 
 */
custom.zk.components.quicknote.RecordableTextNote = zk.$extends(custom.zk.components.quicknote.SelectableTextNote, {
    _createNoteBlock: function (x, y) {
        // call super
        var noteBlock = this.$supers('_createNoteBlock', arguments),
            textarea = noteBlock.firstChild,
            wgt = this;
        // add event listener for onfocus and onblur of textarea
        textarea.onfocus = function () {
            wgt.doTextNoteBlockFocus(textarea);
        };
        textarea.onblur = function () {
            wgt.doTextNoteBlockBlur(textarea);
        };
        // define an object to hold attributes of text note block
        // and store it at textarea
        textarea.zkAttributes = {};
        this.storeTextBlockAttributes(textarea);
        return noteBlock;
    },
    // rewrite _renderNoteBlock to add _afterRenderNoteBlock
    // at the tail
    _renderNoteBlock: function (x, y, w, h, txt) {
        var noteBlock = this._createNoteBlock(x, y),
            textarea = noteBlock.firstChild;
        jq(textarea).css({'width': w+'px',
                        'height': h+'px'});
        textarea.innerHTML = txt;
        this.$n().appendChild(noteBlock);
        // add _afterRenderNoteBlock
        this._afterRenderNoteBlock(noteBlock);
    },
    // called after _renderNoteBlock
    _afterRenderNoteBlock: function (noteBlock) {
        var idx = this._selectedTextNoteIndex;
        if (idx >= 0
            && idx == this.getTextBlockIndex(noteBlock.firstChild))
            jq(noteBlock).addClass(this.getZclass() + '-noteblock-selected');
        // simply update textarea.zkAttributes
        this.storeTextBlockAttributes(noteBlock.firstChild);
    },
    // store/update attributes of text note block
    // to textarea.zkAttributes
    storeTextBlockAttributes: function (textarea) {
        var $textarea = jq(textarea),
            $div = jq(textarea.parentNode),
            zattr = textarea.zkAttributes;
        // store current value
        zattr.left = $div.css('left');
        zattr.top = $div.css('top');
        zattr.width = $textarea.css('width');
        zattr.height = $textarea.css('height');
        zattr.text = $textarea.val();
    },
    // called while onfocus of textarea in text note block
    doTextNoteBlockFocus: function (textarea) {
        // store changed attributes
        // and fire event as needed
        this.recordTextBlock(textarea);
    },
    // called while onblur of textarea in text note block
    doTextNoteBlockBlur: function (textarea) {
        this.recordTextBlock(textarea);
    },
    // update changed attributes and fire event as needed
    recordTextBlock: function (textarea) {
        // update attributes and fire event
        // if attributes are changed
        if (this.isTextNoteBlockChanged(textarea)) {
            this.storeTextBlockAttributes(textarea);
            this.fireOnTextNoteBlockChanged(textarea);
        }
    },
    // check whether attributes of a text note block
    // are changed
    isTextNoteBlockChanged: function (textarea) {
        var $textarea = jq(textarea),
            $div = jq(textarea.parentNode),
            zattr = textarea.zkAttributes;
        // store current value
        return zattr.left != $div.css('left')
            ||    zattr.top != $div.css('top')
            || zattr.width != $textarea.css('width')
            || zattr.height != $textarea.css('height')
            || zattr.text != $textarea.val();
    },
    // fire text note block changed event
    fireOnTextNoteBlockChanged: function (textarea) {
        var zattr = textarea.zkAttributes,
            idx = this.getTextBlockIndex(textarea),
            selected = jq('.' + this.getZclass() + '-noteblock-selected')[0];
        // has attributes object and index exists
        if (zattr
            && idx >= 0) {
            // create data
            var data = {index: idx,
                    left: parseInt(zattr.left),
                    top: parseInt(zattr.top),
                    width: parseInt(zattr.width),
                    height: parseInt(zattr.height),
                    text: zattr.text
            };
            // always fire onTextNoteBlockChanged
            this.fire('onTextNoteBlockChanged', data);
            // also fire onSelectedTextNoteBlockChanged if
            // changed text note block is selected one
            if (selected 
                && this.getTextBlockIndex(selected.firstChild) == idx) {
                this.fire('onSelectedTextNoteBlockChanged', data);
            }
        }
    }
});


TextNoteBlockUpdateEvent.java

Event class that wrap data of text note block update event.

package custom.zk.components.quicknote.event;

import java.util.Map;

import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;

import custom.zk.components.quicknote.Data.TextNoteData;

/** TextNoteBlockUpdateEvent
 * 
 * Wrap the data of au request from text note block changed
 * 
 * One new thing: Extends org.zkoss.zk.ui.event.Event and call
 *                 'super(name, target);' constructor to make it
 *                 working with ZK Event processing flow
 * 
 * @author benbai123
 *
 */
public class TextNoteBlockUpdateEvent extends Event {

    private static final long serialVersionUID = 8137381810564543333L;

    private int _index;
    private TextNoteData _textNoteData;

    public static TextNoteBlockUpdateEvent getTextNoteBlockUpdateEvent (String name, Component target, AuRequest request) {
        // create and return event
        return new TextNoteBlockUpdateEvent(name, target, request);
    }
    // construct event with given name, target and au request
    @SuppressWarnings("rawtypes")
    public TextNoteBlockUpdateEvent (String name, Component target, AuRequest request) {
        super(name, target);
        Map data = request.getData(); // get data map
        _index = (Integer)data.get("index");
        _textNoteData = new TextNoteData((Integer)data.get("left"),
                (Integer)data.get("top"),
                (Integer)data.get("width"),
                (Integer)data.get("height"),
                (String)data.get("text"));
    }
    public int getIndex () {
        return _index;
    }
    public TextNoteData getTextNoteData () {
        return _textNoteData;
    }
}


zk.wpd

Define components under "custom.zk.components.quicknote"

* only the added part, not the full code

NOTE: more components will be added with other articles later

    ...
    <widget name="RecordableTextNote" />
    ...


lang-addon.xml

Define all components in the project

* only the added part, not the full code

NOTE: more components will be added with other articles later

    ...

    <!-- 6th, recordabletextnote component
        extends selectabletextnote and fire event to server
        to update server side data

     -->
     <component>
        <component-name>recordabletextnote</component-name>
        <extends>selectabletextnote</extends>
        <component-class>custom.zk.components.quicknote.RecordableTextNote</component-class>
        <widget-class>custom.zk.components.quicknote.RecordableTextNote</widget-class>
        <annotation>
            <annotation-name>ZKBIND</annotation-name>
            <property-name>selectedTextNoteData</property-name>
            <attribute>
                <attribute-name>ACCESS</attribute-name>
                <attribute-value>save</attribute-value>
            </attribute>
            <attribute>
                <attribute-name>SAVE_EVENT</attribute-name>
                <attribute-value>onSelectedTextNoteBlockChanged</attribute-value>
            </attribute>
            <attribute>
                <attribute-name>LOAD_TYPE</attribute-name>
                <attribute-value>custom.zk.components.quicknote.Data.TextNoteData</attribute-value>
            </attribute>
        </annotation>
    </component>

    ...


References

MVVM Data Binding @save
http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVVM/Syntax/Data_Binding/@save

Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Components_Development__Series/001_walkthrough/ZKQuickNote

recordabletextnote_component.swf
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/component_development_series/001_walkthrough/recordabletextnote_component.swf

Saturday, June 1, 2013

Index Page of ZK CDT (Component Development Tutorial) Walkthrough


Introduction

This is the index page for walkthrough of ZK component development.

The goal of this walkthrough is to create a quicknote component that help you do some quick note on web page (similar to you crop some screenshot, highlight something and add some text in photo editor), and describe each part of a component through each article in this walkthrough.

What we will do:

* Go through each part smoothly
* Cover each part of component development as possible
* Simple code, simple sample, clear description, complete runnable project

What we will ignore/skip

* Any optimization/task that will hurt the goal listed in the list above, e.g., CSS optimization, to use something new (like CSS3/HTML5, Canvas), complex structure or flow control, etc, we will just try to do everything strait forward to keep them as simple/easy as possible.

So these components probably not works well in some browser or in some combination.

* To use modern tools that may let us missing some details, e.g., create component with ZK CDT (https://code.google.com/a/eclipselabs.org/p/zk-cdt/) or Maven Archetype (http://books.zkoss.org/wiki/ZK%20Component%20Development%20Essentials/ZK%20Component%20Overview/Creating%20ZK%20Component%20with%20Maven%20Archetype)

You can use those tools to boost your development task as needed once you are familiar with ZK Component Development.

Audience

We assume you know the things below as well:

1. ZK MVVM Pattern

2. Basic JAVA

3. Basic Javascript

4. Basic CSS

And you want to know how to implement a ZK Component


ZK CDT Pages

001: Mask, the basic part of a component.
http://ben-bai.blogspot.tw/2013/06/zk-cdt-create-custom-mask-component.html

The start point of this walkthrough.

002: EnhancedMask, handle attributes of a component.
http://ben-bai.blogspot.tw/2013/06/zk-cdt-add-attributes-to-create.html

Contains server side setter/getter, client side setter/getter, server side render/update property.

003: SimpleTextNote, handle client side event.
http://ben-bai.blogspot.tw/2013/06/zk-cdt-handling-client-side-event-to.html

Contains basic client side event listener.

004: RenderableTextNote, render content with the data in custom model.
http://ben-bai.blogspot.tw/2013/06/zk-cdt-renderabletextnote-render-note.html

Similar to 002, but handle json data to render dom element at client side.

005: SelectableTextNote, fire event to server side and update selected note block.
http://ben-bai.blogspot.tw/2013/06/zk-cdt-fire-event-to-server-to-create.html

Contains 'fire' client side API, 'service' server side method and annotation written in XML for ZKBIND.

006: RecordableTextNote, more event, update all attributes of selected note block to server.
http://ben-bai.blogspot.tw/2013/06/zk-cdt-create-custom-event-for.html

Create a custom event TextNoteBlockUpdateEvent

007: UpdatableTextNote, update component by model.
http://ben-bai.blogspot.tw/2013/06/zk-cdt-add-eventlistener-into-model-to.html

Contains the listener pattern of model.


above: has been published
--
below: probably will be changed

008: SimpleQuickNote, nothing new, adding highlight part.

009: QuickNote, will finish everything on this, such like note block control (open/close/delete, etc)

reply this article if you think something are missing, not clear or wrong, or reply under other ZK CDT Pages if you have any questions.

ZK CDT: Fire Event to Server to Create a SelectableTextNote


Introduction

This is the 5th article of ZK CDT (ZK Component Development Tutorial) walkthrough, this article describe how to fire event to bring client data to server side.

Fire event to server.

The goal of this walkthrough is to create a quicknote component that help you do some quick note on web page (similar to you crop some screenshot, highlight something and add some text in photo editor), and describe each part of a component through each article in this walkthrough.

This is the 5th part: Fire event to server at client side.

Result

View demo online:
http://screencast.com/t/cDqjn5k3hU

As you can see, now you can update the selected note block by clicking on it instead of type index in intbox.
Also the selected note block will become the top most one.

Pre-request

ZK CDT: RenderableTextNote: Render Note Blocks with Server Side Data
http://ben-bai.blogspot.tw/2013/06/zk-cdt-renderabletextnote-render-note.html

Program

selectabletextnote.zul

Contains a selectabletextnote component, a control block that used to add/update/clear note blocks, a slider/colorbox that control the opacity/background-color of the selectabletextnote.

<zk>
    <!-- two new things
    
        selectedTextNoteIndex="@save(vm.indexToUpdate)":
        that will save selectedTextNoteIndex of selectabletextnote to vm
        chek the component definition in lang-addon.xml to see
        how it can work
        
        control of opacity and maskColor are moved into VM
     -->
    <div apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('custom.zk.samples.quicknote.SelectableTextNoteVM')">
        <hlayout>
            <!-- The selectabletextnote component that will cover
                its children by a mask
                you can click on the mask to add a textarea
                and type text in it
            -->
            <selectabletextnote width="700px" id="stn"
                opacity="@load(vm.opacity)" maskColor="@load(vm.maskColor)"
                model="@load(vm.model)"
                selectedTextNoteIndex="@save(vm.indexToUpdate)">
                <button label="ZK Website" />
                <iframe width="100%"
                    height="1000px"
                    src="http://www.zkoss.org/"></iframe>
            </selectabletextnote>
            <vlayout>
                <!-- controll block for add/update/clear note blocks -->
                <vlayout>
                    <hlayout>
                        x: <intbox value="@bind(vm.textNoteData.posX)" />
                    </hlayout>
                    <hlayout>
                        y: <intbox value="@bind(vm.textNoteData.posY)" />
                    </hlayout>
                    <hlayout>
                        width: <intbox value="@bind(vm.textNoteData.width)" />
                    </hlayout>
                    <hlayout>
                        height: <intbox value="@bind(vm.textNoteData.height)" />
                    </hlayout>
                    <hlayout>
                        text: <textbox value="@bind(vm.textNoteData.text)" />
                    </hlayout> 
                    <hlayout>
                        <button label="add" onClick="@command('addNoteBlock')" />
                    </hlayout>
                    <hlayout>
                        index to update: <label value="@load(vm.indexToUpdate)" />
                        <button label="update" onClick="@command('updateNoteBlock')" />
                    </hlayout>
                    <hlayout>
                        <button label="clear" onClick="@command('clearAllBlocks')" />
                    </hlayout>
                </vlayout>
                <hlayout>
                    <!-- slider used to control opacity of selectabletextnote -->
                    <slider curpos="@bind(vm.opacity)" maxpos="100"
                        onScroll="@command('updateOpacity')" />
                    <!-- colorbox used to control mask color of selectabletextnote -->
                    <colorbox color="@bind(vm.maskColor)"
                        onChange="@command('updateMaskColor')" />
                </hlayout>
            </vlayout>
        </hlayout>
    </div>
</zk>


SelectableTextNoteVM.java

VM used in selectabletextnote.zul, provide data, do command and update data.

package custom.zk.samples.quicknote;

import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;

/** VM used in selectabletextnote.zul,
 * extends RenderableTextNoteVM then simply
 * define a member field and its getter/setter
 * 
 * @author benbai123
 *
 */
public class SelectableTextNoteVM extends RenderableTextNoteVM {

    private int _opacity = 20;
    private String _maskColor = "#00FF00";
    // getters/setters

    public int getOpacity () {
        return _opacity;
    }
    public void setOpacity (int opacity) {
        _opacity = opacity;
    }

    public String getMaskColor () {
        return _maskColor;
    }
    public void setMaskColor (String maskColor) {
        _maskColor = maskColor;
    }
    // commands
    @Command
    @NotifyChange("opacity")
    public void updateOpacity () {
        // do nothing, just for trigger NotifyChange
    }
    @Command
    @NotifyChange("maskCOlor")
    public void updateMaskColor () {
        // do nothing, just for trigger NotifyChange
    }
}


SelectableTextNote.java

Java class of SelectableTextNote component, extends RenderableTextNote and handle note block selection.

package custom.zk.components.quicknote;

import java.util.Map;

import org.zkoss.zk.ui.event.Events;

/**
 * SelectableTextNote, will receive and store which note block is selected from client side action
 * 
 * Three new things:
 * 
 * addClientEvent: add an event that the client might send to server with specific settings,
 *         refer to http://www.zkoss.org/javadoc/latest/zk/org/zkoss/zk/ui/AbstractComponent.html#addClientEvent(java.lang.Class, java.lang.String, int)
 * 
 * service: Handles an AU request. It is invoked internally.
 *         refer to http://www.zkoss.org/javadoc/latest/zk/org/zkoss/zk/ui/AbstractComponent.html#service(org.zkoss.zk.au.AuRequest, boolean)
 *
 * postEvent: post an event to self instance so the composer can be notified
 *             this is also required for save data with MVVM
 *             refer to http://books.zkoss.org/wiki/ZK_Developer's_Reference/Event_Handling/Event_Firing
 * 
 * @author benbai123
 *
 */
public class SelectableTextNote extends RenderableTextNote {

    private static final long serialVersionUID = -6589891861074953359L;

    private int _selectedTextNoteIndex = -1;

    static {
        /* CE_IMPORTANT: always fire it to server,
         *             without this flag, the au engine will only fire event to server
         *             if and only if there is an EventListener listen to this event
         * 
         * CE_DUPLICATE_IGNORE: ignore multiple event in an au request
         * 
         * CE_NON_DEFERRABLE: always fire it immediately,
         *             without this flag, au engine will queue this event at client side
         *             and fire it to server with other non-defferrable event
         */
        addClientEvent(SelectableTextNote.class, "onTextNoteBlockSelect", CE_IMPORTANT | CE_DUPLICATE_IGNORE | CE_NON_DEFERRABLE);
    }

    // setter/getter
    public void setSelectedTextNoteIndex (int selectedTextNoteIndex) {
        _selectedTextNoteIndex = selectedTextNoteIndex;
    }
    public int getSelectedTextNoteIndex () {
        return _selectedTextNoteIndex;
    }

    // process client event
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        if (cmd.equals("onTextNoteBlockSelect")) {
            Map data = request.getData(); // get data map
            // get index by the key "index" since
            // we define the data as {index: idx}
            // while firing onTextNoteBlockSelect event
            // in SelectableTextNote.js
            Integer index = (Integer)data.get("index");
            // store value
            _selectedTextNoteIndex = index;
            // post event to trigger listeners if any
            Events.postEvent("onTextNoteBlockSelect", this, data);
        } else 
            super.service(request, everError);
    }
}


SelectableTextNote.js

Widget class of SelectableTextNote component, extends RenderableTextNote and handle click event of textarea in text note block.

/**
 * Widget class of SelectableTextNote component,
 * extends custom.zk.components.quicknote.RenderableTextNote
 * 
 * One new thing:
 * fire: fire event to server,
 *         widget.fire('EVENT_NAME', {DATA}, {OPTIONS}, TIMEOUT);
 *         refer to http://www.zkoss.org/javadoc/latest/jsdoc/zk/Widget.html#fire(_global_.String, zk.Object, _global_.Map, int)
 * 
 */
custom.zk.components.quicknote.SelectableTextNote = zk.$extends(custom.zk.components.quicknote.RenderableTextNote, {
    _selectedTextNoteIndex: -1,
    // called while onclick of any dom elements
    // under root element is triggered
    doClick_: function (evt) {
        // call super at first
        this.$supers('doClick_', arguments);
        var target = evt.domTarget;
        // clicked in textarea in text note block
        if (jq(target).hasClass(this.getZclass() + '-noteblock-textarea')) {
            this._doTextNoteBlockClick(evt);
        }
    },
    /** processing onclick of textarea in note block
     * pass event into this function (instead of just pass target)
     * since we probably need some information (e.g., pageX/Y, etc) in
     * the future
     * 
     * @param evt
     */
    _doTextNoteBlockClick: function (evt) {
        // cls: css class of textarea within text note block
        // target: the clicked dom element
        var scls = this.getZclass() + '-noteblock-selected',
            target = evt.domTarget,
            idx;

        // clear selected class of old selected block
        jq('.' + scls).each(function () {
            jq(this).removeClass(scls);
        });
        // add class to make it become top most note block
        jq(target.parentNode).addClass(scls);
        // fire event to update index to server side
        if ((idx = this.getTextBlockIndex(target)) >= 0) {
            this._selectedTextNoteIndex = idx;
            this.fire('onTextNoteBlockSelect', {index: idx});
        }
    },
    getTextBlockIndex: function (textarea) {
        var cls = this.getZclass() + '-noteblock-textarea';
        // current: a copy of text note block for while loop
        // idx: index of current text note block
        var current = this.$n('mask').nextSibling,
            idx = 0;

        // for each text note block
        while (current) {
            // found clicked block
            if (jq(current).find('.'+cls)[0] == textarea) {
                // return index
                return idx;
            }
            current = current.nextSibling;
            idx++;
        }
        return -1;
    },
    // override with new css class name
    getZclass: function () {
        var zcls = this._zclass;
        return zcls? zcls : 'z-selectabletextnote';
    }
});


selectableTextNote.css.dsp

CSS classes for SelectableTextNote component

<%--// ------------------------------------------- --%>
<%--//                                             --%>
<%--//            SelectableTextNote component           --%>
<%--//                                             --%>
<%--// ------------------------------------------- --%>
<%--// root element --%>
.z-simpletextnote {
    <%--// be the anchor of absolute positioned children --%>
    position: relative;
    overflow: hidden;
}
<%--// the mask that cover whole element --%>
<%--// no background-color and opacity specified --%>
<%--// since we specified them in widget class --%>
.z-selectabletextnote-cover {
    <%--// absoluted positioned --%>
    position: absolute;
    <%--// align the left-top corner of parent (root) element --%>
    left: 0px;
    top: 0px;
    <%--// cover whole root element --%>
    height: 100%;
    width: 100%;
    <%--// make it the top most element under root element --%>
    z-index: 99999;
}

.z-selectabletextnote .z-selectabletextnote-noteblock {
    <%--// absoluted positioned --%>
    position: absolute;
    <%--// in front of mask --%>
    z-index: 999999;
}

.z-selectabletextnote .z-selectabletextnote-noteblock-textarea {
    <%--// h/v resizable --%>
    resize: both;
    <%--// default width and height --%>
    <%--// NOTE: the specified value of width/height will  --%>
    <%--// be the minimum value, you cannot shrink textarea --%>
    <%--// smaller than these values (at least on chrome) --%>
    width: 50px;
    height: 30px;
}
.z-selectabletextnote .z-selectabletextnote-noteblock-selected {
    <%--// in front other text note blocks --%>
    z-index: 1000000;
}


zk.wpd

Define components under "custom.zk.components.quicknote"

* only the added part, not the full code

NOTE: more components will be added with other articles later

    ...

    <widget name="SelectableTextNote" />

    ...


lang-addon.xml

Define all components in the project

* only the added part, not the full code

NOTE: more components will be added with other articles later

    ...

    <!-- 5th, selectabletextnote component
        extends renderabletextnote,
        update zclass, handle click event of
        textareas and fire event to server
        to update the selected note block index

        one new thing, the 'annotation' block that define
        how an attribute (selectedTextNoteIndex here) works with ZKBIND
        
        as you can see opacity and maskColor are not defined here
        since they only require the 'load' direction
        the 'load' direction is supported by default so
        you just need to provide setter (in component) and getter (in VM) properly
        
        refer to:
        Document: http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVVM/Advanced/Binding_Annotation_for_a_Custom_Component
        Code: https://github.com/zkoss/zk/blob/master/zkbind/src/archive/metainfo/zk/lang-addon.xml
     -->
     <component>
        <component-name>selectabletextnote</component-name>
        <extends>renderabletextnote</extends>
        <component-class>custom.zk.components.quicknote.SelectableTextNote</component-class>
        <widget-class>custom.zk.components.quicknote.SelectableTextNote</widget-class>
        <mold>
            <mold-name>default</mold-name>
            <css-uri>css/selectableTextNote.css.dsp</css-uri>
        </mold>
        <annotation>
            <!-- ZKBIND is the zkbind system annotation -->
            <annotation-name>ZKBIND</annotation-name>
            <!-- property name -->
            <property-name>selectedTextNoteIndex</property-name>
            <attribute>
                <!-- ACCESS is the access direction:
                    can be "both", "save", "load";
                    default to "load" if not found -->
                <attribute-name>ACCESS</attribute-name>
                <attribute-value>both</attribute-value>
            </attribute>
            <attribute>
                <!-- SAVE_EVENT is the save trigger event;
                    meaningful only when ACCESS is "both" or "save" -->
                <attribute-name>SAVE_EVENT</attribute-name>
                <attribute-value>onTextNoteBlockSelect</attribute-value>
            </attribute>
            <attribute>
                <!-- LOAD_TYPE is the type of attribute for loading -->
                <attribute-name>LOAD_TYPE</attribute-name>
                <attribute-value>java.lang.Integer</attribute-value>
            </attribute>
        </annotation>
    </component>

    ...


References

addClientEvent java method
http://www.zkoss.org/javadoc/latest/zk/org/zkoss/zk/ui/AbstractComponent.html#addClientEvent(java.lang.Class, java.lang.String, int)

service java method
http://www.zkoss.org/javadoc/latest/zk/org/zkoss/zk/ui/AbstractComponent.html#service(org.zkoss.zk.au.AuRequest, boolean)

postEvent java method
http://books.zkoss.org/wiki/ZK_Developer's_Reference/Event_Handling/Event_Firing

fire client widget method
http://www.zkoss.org/javadoc/latest/jsdoc/zk/Widget.html#fire(_global_.String, zk.Object, _global_.Map, int)

ZKBIND annotation
Document: 
http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVVM/Advanced/Binding_Annotation_for_a_Custom_Component
Code:
https://github.com/zkoss/zk/blob/master/zkbind/src/archive/metainfo/zk/lang-addon.xml


Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Components_Development__Series/001_walkthrough/ZKQuickNote

selectabletextnote_component.swf
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/component_development_series/001_walkthrough/selectabletextnote_component.swf