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

2 comments:

  1. Hi Benbai,

    I need your help to create custom event onScroll to combo box in
    http://forum.zkoss.org/question/105030/zk-create-custom-event-for-combo-box/

    ReplyDelete
  2. Hi, please refer to this sample
    http://zkfiddle.org/sample/29oah2u/3-Another-new-ZK-fiddle

    ReplyDelete