zul/src/main/java/org/zkoss/zul/impl/InputElement.java

Summary

Maintainability
F
5 days
Test Coverage
/* InputElement.java

    Purpose:
        
    Description:
        
    History:
        Tue Jul  5 08:49:30     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
    This program is distributed under LGPL Version 2.1 in the hope that
    it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul.impl;

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.json.JavaScriptValue;
import org.zkoss.lang.Exceptions;
import org.zkoss.lang.Objects;
import org.zkoss.lang.Strings;
import org.zkoss.zk.au.AuRequests;
import org.zkoss.zk.au.out.AuInvoke;
import org.zkoss.zk.au.out.AuSelect;
import org.zkoss.zk.au.out.AuWrongValue;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.ErrorEvent;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.InputEvent;
import org.zkoss.zk.ui.event.SelectionEvent;
import org.zkoss.zk.ui.ext.Disable;
import org.zkoss.zk.ui.ext.Readonly;
import org.zkoss.zk.ui.ext.Scopes;
import org.zkoss.zk.ui.sys.BooleanPropertyAccess;
import org.zkoss.zk.ui.sys.IntPropertyAccess;
import org.zkoss.zk.ui.sys.ObjectPropertyAccess;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zk.ui.sys.StringPropertyAccess;
import org.zkoss.zk.ui.util.Clients;
import org.zkoss.zul.ClientConstraint;
import org.zkoss.zul.Constraint;
import org.zkoss.zul.CustomConstraint;
import org.zkoss.zul.SimpleConstraint;
import org.zkoss.zul.ext.Constrainted;
import org.zkoss.zul.mesg.MZul;

/**
 * A skeletal implementation of an input box.
 * 
 * <p>Events: onChange, onChanging, onFocus, onBlur, onSelection.
 *
 * @author tomyeh
 */
public abstract class InputElement extends XulElement implements Constrainted, Readonly, Disable {
    private static final Logger log = LoggerFactory.getLogger(InputElement.class);

    static {
        addClientEvent(InputElement.class, Events.ON_CHANGE, CE_IMPORTANT | CE_REPEAT_IGNORE);
        addClientEvent(InputElement.class, Events.ON_CHANGING, CE_DUPLICATE_IGNORE);
        addClientEvent(InputElement.class, Events.ON_FOCUS, CE_DUPLICATE_IGNORE);
        addClientEvent(InputElement.class, Events.ON_BLUR, CE_DUPLICATE_IGNORE);
        addClientEvent(InputElement.class, Events.ON_SELECTION, CE_REPEAT_IGNORE);
        addClientEvent(InputElement.class, Events.ON_ERROR, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
    }

    /** The value. */
    protected Object _value;
    protected int _cols;
    private AuxInfo _auxinf;
    private boolean _disabled, _readonly;
    /** Whether this input is validated (Feature 1461209). */
    private boolean _valided;
    private boolean _inplace;
    private String _placeholder;
    private Map<String, String> _inputAttributes;

    /**
     * Returns the placeholder text
     * @since 6.5.0
     */
    public String getPlaceholder() {
        return _placeholder;
    }

    /**
     * Sets the placeholder text that is displayed when input is empty.
     * Only works for browsers supporting HTML5.
     * @since 6.5.0
     */
    public void setPlaceholder(String placeholder) {
        if (!Objects.equals(_placeholder, placeholder)) {
            _placeholder = placeholder;
            smartUpdate("placeholder", _placeholder);
        }
    }

    /**
     * Sets to enable the inplace-editing function that the look and feel is
     * like a label.
     * @since 5.0.0 
     */
    public void setInplace(boolean inplace) {
        if (_inplace != inplace) {
            _inplace = inplace;
            smartUpdate("inplace", _inplace);
        }
    }

    /**
     * Returns whether enable the inplace-editing.
     * <p>default: false.
     * @since 5.0.0
     */
    public boolean isInplace() {
        return _inplace;
    }

    /** Returns whether it is disabled.
     * <p>Default: false.
     */
    public boolean isDisabled() {
        return _disabled;
    }

    /** Sets whether it is disabled.
     */
    public void setDisabled(boolean disabled) {
        if (_disabled != disabled) {
            _disabled = disabled;
            smartUpdate("disabled", _disabled);
        }
    }

    /** Returns whether it is readonly.
     * <p>Default: false.
     */
    public boolean isReadonly() {
        return _readonly;
    }

    /** Sets whether it is readonly.
     */
    public void setReadonly(boolean readonly) {
        if (_readonly != readonly) {
            _readonly = readonly;
            smartUpdate("readonly", _readonly);
        }
    }

    /** Returns the name of this component.
     * <p>Default: null.
     * <p>Don't use this method if your application is purely based
     * on ZK's event-driven model.
     * <p>The name is used only to work with "legacy" Web application that
     * handles user's request by servlets.
     * It works only with HTTP/HTML-based browsers. It doesn't work
     * with other kind of clients.
     */
    public String getName() {
        return _auxinf != null ? _auxinf.name : null;
    }

    /** Sets the name of this component.
     * <p>Don't use this method if your application is purely based
     * on ZK's event-driven model.
     * <p>The name is used only to work with "legacy" Web application that
     * handles user's request by servlets.
     * It works only with HTTP/HTML-based browsers. It doesn't work
     * with other kind of clients.
     *
     * @param name the name of this component.
     */
    public void setName(String name) {
        if (name != null && name.length() == 0)
            name = null;
        if (!Objects.equals(_auxinf != null ? _auxinf.name : null, name)) {
            initAuxInfoForInputElement().name = name;
            smartUpdate("name", getName());
        }
    }

    /** Returns the error message that is caused when user entered
     * invalid value, or null if no error at all.
     *
     * <p>The error message is set when user has entered a wrong value,
     * or setValue is called with a wrong value.
     * It is cleared once a correct value is assigned.
     *
     * <p>If the error message is set, we say this input is in the error mode.
     * Any following invocation to {@link #getText} or getValue will throw
     * any exception.
     * Example, {@link org.zkoss.zul.Textbox#getValue} and
     * {@link org.zkoss.zul.Intbox#getValue}.
     */
    public String getErrorMessage() {
        return _auxinf != null ? _auxinf.errmsg : null;
    }

    /** Associates an error message to this input.
     * It will cause the given error message to be shown at the client.
     * <p>Notice that the application rarely invokes this method. Rather,
     * throw {@link WrongValueException} instead.
     * <p>Notice it does not invoke {@link CustomConstraint#showCustomError}
     * even if {@link #getConstraint} implements  {@link CustomConstraint}.
     * @since 5.0.4
     */
    public void setErrorMessage(String errmsg) {
        if (errmsg != null && errmsg.length() > 0) {
            initAuxInfoForInputElement().errmsg = errmsg;
            response(new AuWrongValue(this, errmsg));
        } else {
            clearErrorMessage();
        }
    }

    /** Clears the error message.
     *
     * <p>The error message is cleared automatically, so you rarely need
     * to call this method.
     * However, if a constraint depends on multiple input fields and
     * the error can be corrected by changing one of these fields,
     * then you may have to clear the error message manually by invoking
     * this method.
     *
     * <p>For example, assume you have two {@link org.zkoss.zul.Intbox}
     * and want the value of the first one to be smaller than that of the
     * second one. Then, you have to call this method for the second intbox
     * once the validation of the first intbox succeeds, and vice versa.
     * Otherwise, the error message for the second intbox remains if
     * the user fixed the error by lowering down the value of the first one
     * Why? The second intbox got no idea to clear the error message
     * (since its content doesn't change).
     *
     * @param revalidateRequired whether to re-validate the current value
     * when {@link #getText} or others (such as {@link org.zkoss.zul.Intbox#getValue})
     * is called.
     * If false, the current value is assumed to be correct and
     * the following invocation to {@link #getText} or others (such as {@link org.zkoss.zul.Intbox#getValue})
     * won't check the value again.
     * Note: when an input element is constructed, the initial value
     * is assumed to be "not-validated-yet".
     * @since 3.0.1
     */
    public void clearErrorMessage(boolean revalidateRequired) {
        if (_auxinf != null && _auxinf.errmsg != null) {
            _auxinf.errmsg = null;
            Clients.clearWrongValue(this);
        }
        _valided = !revalidateRequired;
    }

    /** Clears the error message.
     * It is the same as clearErrorMessage(false). That is, the current
     * value is assumed to be correct. {@link #getText} or others (such as {@link org.zkoss.zul.Intbox#getValue})
     * won't re-validate it again.
     *
     * <p>The error message is cleared automatically, so you rarely need
     * to call this method.
     *
     * @see #clearErrorMessage(boolean)
     */
    public void clearErrorMessage() {
        clearErrorMessage(false);
    }

    /** Returns the value in the String format.
     * In most case, you shall use the setValue method instead, e.g.,
     * {@link org.zkoss.zul.Textbox#getValue} and
     * {@link org.zkoss.zul.Intbox#getValue}.
     *
     * <p>It invokes {@link #checkUserError} to ensure no user error.
     *
     * <p>It invokes {@link #coerceToString} to convert the stored value
     * into a string.
     *
     * @exception WrongValueException if user entered a wrong value
     */
    public String getText() throws WrongValueException {
        checkUserError();
        return coerceToString(_value);
    }

    /** Sets the value in the String format.
     * In most case, you shall use the setValue method instead, e.g.,
     * {@link org.zkoss.zul.Textbox#setValue} and
     * {@link org.zkoss.zul.Intbox#setValue}.
     *
     * <p>It invokes {@link #coerceFromString} first and then {@link #validate}.
     * Derives might override them for type conversion and special
     * validation.
     *
     * @param value the value; If null, it is considered as empty.
     */
    public void setText(String value) throws WrongValueException {
        if (_auxinf != null && _auxinf.maxlength > 0 && value != null && value.length() > _auxinf.maxlength)
            throw new WrongValueException(this, MZul.STRING_TOO_LONG, new Integer(_auxinf.maxlength));

        final Object val = coerceFromString(value);

        // cannot use java.util.Objects.equals() because it will cause BigDecimal with String comparison to be wrong. See B95_ZK_4658Test.
        final boolean same = Objects.equals(_value, val);
        boolean errFound = false;
        if (!same || !_valided || (_auxinf != null && _auxinf.errmsg != null)) { //note: the first time (!_valided) must always validate
            validate(val); //Bug 2946917: don't validate if not changed

            errFound = _auxinf != null && _auxinf.errmsg != null;
            clearErrorMessage(); //no error at all
        }

        if (!same) {
            _value = val;
            smartUpdate("_value", marshall(val));
        } else if (errFound) {
            smartUpdate("_value", marshall(_value)); //send back original value
            //Bug 1876292: make sure client see the updated value
        }
    }

    /** Coerces the value passed to {@link #setText}.
     *
     * <p>Deriving note:<br>
     * If you want to store the value in other type, say BigDecimal,
     * you have to override {@link #coerceToString} and {@link #coerceFromString}
     * to convert between a string and your targeting type.
     *
     * <p>Moreover, when {@link org.zkoss.zul.Textbox} is called, it calls this method
     * with value = null. Derives shall handle this case properly.
     */
    protected abstract Object coerceFromString(String value) throws WrongValueException;

    /** Coerces the value passed to {@link #setText}.
     *
     * <p>Default: convert null to an empty string.
     *
     * <p>Deriving note:<br>
     * If you want to store the value in other type, say BigDecimal,
     * you have to override {@link #coerceToString} and {@link #coerceFromString}
     * to convert between a string and your targeting type.
     */
    protected abstract String coerceToString(Object value);

    /** Validates the value returned by {@link #coerceFromString}.
     * <p>Default: use  {@link #getConstraint}'s {@link Constraint#validate},
     * if not null.
     * <p>You rarely need to override this method.
     */
    protected void validate(Object value) throws WrongValueException {
        final Constraint constr = getConstraint();
        if (constr != null) { //then _auxinf must be non-null
            //Bug 1698190: constructor might be zscript
            Scopes.beforeInterpret(this);
            try {
                constr.validate(this, value);
                if (!_auxinf.checkOnly && (constr instanceof CustomConstraint)) {
                    try {
                        ((CustomConstraint) constr).showCustomError(this, null);
                        //not call thru showCustomError(Wrong...) for better performance
                    } catch (Throwable ex) {
                        log.error("", ex);
                    }
                }
            } finally {
                Scopes.afterInterpret();
            }
        }
    }

    /** Shows the error message in the custom way by calling
     * ({@link CustomConstraint#showCustomError}, if the constraint
     * implements {@link CustomConstraint}.
     *
     * <p>Derived class shall call this method before throwing
     * {@link WrongValueException}, such that the constraint,
     * if any, has a chance to show the error message in a custom way.
     *
     * @param ex the exception, or null to clean up the error.
     * @return the exception (ex)
     */
    protected WrongValueException showCustomError(WrongValueException ex) {
        if (_auxinf != null && _auxinf.constr instanceof CustomConstraint) {
            Scopes.beforeInterpret(this);
            try {
                ((CustomConstraint) _auxinf.constr).showCustomError(this, ex);
            } catch (Throwable t) {
                log.error("", t); //and ignore it
            } finally {
                Scopes.afterInterpret();
            }
        }
        return ex;
    }

    /** Returns the maxlength.
     * <p>Default: 0 (non-positive means unlimited).
     */
    public int getMaxlength() {
        return _auxinf != null ? _auxinf.maxlength : 0;
    }

    /** Sets the maxlength.
     * <p> The length includes the format, if specified.
     */
    public void setMaxlength(int maxlength) {
        if ((_auxinf != null ? _auxinf.maxlength : 0) != maxlength) {
            initAuxInfoForInputElement().maxlength = maxlength;
            smartUpdate("maxlength", getMaxlength());
        }
    }

    /** Returns the cols.
     * <p>Default: 0 (non-positive means the same as browser's default).
     */
    public int getCols() {
        return _cols;
    }

    /** Sets the cols which determines the visible width, in characters.
     */
    public void setCols(int cols) throws WrongValueException {
        if (cols < 0)
            throw new WrongValueException("Illegal cols: " + cols);

        if (_cols != cols) {
            _cols = cols;
            smartUpdate("cols", _cols);
        }
    }

    /** Returns true if onChange event is sent as soon as user types in the input
     * component.
     * <p>Default: false
     * @since 6.0.0
     */
    public boolean getInstant() {
        return isInstant();
    }

    /** Returns true if onChange event is sent as soon as user types in the input
     * component.
     * <p>Default: false
     * @since 8.0.0
     */
    public boolean isInstant() {
        return _auxinf != null && _auxinf.instant;
    }

    /** Sets the instant attribute. When the attribute is true, onChange event 
     * will be fired as soon as user type in the input component.
     * @since 6.0.0
     */
    public void setInstant(boolean instant) {
        if (getInstant() != instant) {
            initAuxInfoForInputElement().instant = instant;
            smartUpdate("instant", getInstant());
        }
    }

    /** Returns whether it is multiline.
     * <p>Default: false.
     */
    public boolean isMultiline() {
        return false;
    }

    /** Returns the type.
     * <p>Default: text.
     */
    public String getType() {
        return "text";
    }

    /** Selects the whole text in this input.
     */
    public void select() {
        response(new AuSelect(this));
    }

    //-- Constrainted --//
    public void setConstraint(String constr) {
        setConstraint(constr != null ? SimpleConstraint.getInstance(constr) : null); //Bug 2564298
    }

    public void setConstraint(Constraint constr) {
        if (!Objects.equals(_auxinf != null ? _auxinf.constr : null, constr)) {
            initAuxInfoForInputElement().constr = constr;
            _valided = false;

            if (_auxinf.constr instanceof CustomConstraint) { //client ignored if custom
                smartUpdate("constraint", "[c"); //implies validated at server
                return;
            } else if (_auxinf.constr instanceof ClientConstraint) {
                final ClientConstraint cc = (ClientConstraint) _auxinf.constr;
                final JavaScriptValue cpkgs = getClientPackages(cc);
                if (cpkgs != null)
                    smartUpdate("_0", cpkgs); //name doesn't matter

                final Object code = getClientConstraintCode(cc);
                if (code != null) {
                    if (code instanceof JavaScriptValue)
                        smartUpdate("z$al", code);
                    else //must be string
                        smartUpdate("constraint", new JavaScriptValue((String) code));
                    return;
                }
            }
            smartUpdate("constraint", _auxinf.constr != null ? "[s" : null);
        }
    }

    private static JavaScriptValue getClientPackages(ClientConstraint cc) {
        final String cpkg = cc.getClientPackages();
        return cpkg != null ? new JavaScriptValue("zk.load('" + cpkg + "')") : null;
    }

    private static Object getClientConstraintCode(ClientConstraint cc) {
        final String js = cc.getClientConstraint();
        if (js != null && js.length() > 0) {
            final char c = js.charAt(0);
            if (c != '\'' && c != '"')
                return new JavaScriptValue("{constraint:function(){\nreturn " + js + ";}}");
            //some JavaScript code => z$al
            return js;
        }
        return null;
    }

    public Constraint getConstraint() {
        return _auxinf != null ? _auxinf.constr : null;
    }

    /**
     * Returns the raw constraint string value if any.
     * @since 10.0.0
     */
    public String getConstraintString() {
        Constraint constraint = getConstraint();
        if (constraint instanceof SimpleConstraint) {
            return ((SimpleConstraint) constraint).getRawValue();
        }
        return null;
    }

    /** Returns the value in the targeting type.
     * It is used by the deriving class to implement the getValue method.
     * For example, {@link org.zkoss.zul.Intbox#getValue} is the same
     * as this method except with a different signature.
     *
     * <p>It invokes {@link #checkUserError} to ensure no user error.
     * @exception WrongValueException if the user entered a wrong value
     * @see #getText
     */
    protected Object getTargetValue() throws WrongValueException {
        checkUserError();
        return _value;
    }

    /** Returns the raw value directly with checking whether any
     * error message not yet fixed. In other words, it does NOT invoke
     * {@link #checkUserError}.
     *
     * <p>Note: if the user entered an incorrect value (i.e., caused
     * {@link WrongValueException}), the incorrect value doesn't
     * be stored so this method returned the last correct value.
     *
     * @see #getRawText
     * @see #getText
     * @see #setRawValue
     */
    public Object getRawValue() {
        return _value;
    }

    /** Returns the text directly without checking whether any error
     * message not yet fixed. In other words, it does NOT invoke
     * {@link #checkUserError}.
     *
     * <p>Note: if the user entered an incorrect value (i.e., caused
     * {@link WrongValueException}), the incorrect value doesn't
     * be stored so this method returned the last correct value.
     *
     * @see #getRawValue
     * @see #getText
     */
    public String getRawText() {
        return coerceToString(_value);
    }

    /** Sets the raw value directly. The caller must make sure the value
     * is correct (or intend to be incorrect), because this method
     * doesn't do any validation.
     *
     * <p>If you feel confusing with setValue, such as {@link org.zkoss.zul.Textbox#setValue},
     * it is usually better to use setValue instead. This method
     * is reserved for developer that really want to set an 'illegal'
     * value (such as an empty string to a textbox with no-empty constraint).
     *
     * <p>Note: since 3.0.1, the value will be re-validate again if
     * {@link #getText} or others (such as {@link org.zkoss.zul.Intbox#getValue})
     * is called. In other words, it is assumed that the specified value
     * is not validated yet -- the same state when this component is
     * created. If you want to avoid the re-validation, you have to invoke
     * {@link #clearErrorMessage()}.
     *
     * <p>Like setValue, the result is returned back to the server
     * by calling {@link #getText}.
     *
     * @see #getRawValue
     */
    public void setRawValue(Object value) {
        if ((_auxinf != null && _auxinf.errmsg != null) || !Objects.equals(_value, value)) {
            clearErrorMessage(true);
            _value = value;
            smartUpdate("_value", marshall(_value));
        }
    }

    /** Sets the value directly.
     * Note: Unlike {@link #setRawValue} (nor setValue), this method
     * assigns the value directly without clearing error message or
     * synchronizing with the client.
     *
     * <p>It is usually used only the constructor.
     * Though it is also OK to use {@link #setRawValue} in the constructor,
     * this method has better performance.
     * @since 3.0.3
     */
    protected void setValueDirectly(Object value) {
        _value = value;
    }

    /** Returns the current content of this input is correct.
     * If the content is not correct, next call to the getValue method will
     * throws WrongValueException.
     */
    public boolean isValid() {
        if (_auxinf != null && _auxinf.errmsg != null)
            return false;

        if (!_valided && _auxinf != null && _auxinf.constr != null) {
            _auxinf.checkOnly = true;
            try {
                validate(_value);
            } catch (Throwable ex) {
                return false;
            } finally {
                _auxinf.checkOnly = false;
            }
        }
        return true;
    }

    /**
     * Sets the text of this InputElement to the specified text which is
     * beginning with the new start point and ending with the new end point.
     * 
     * @param start the start position of the text (included)
     * @param end the end position of the text (excluded)
     * @param newtxt the new text to be set.
     * @param isHighLight
     *            Sets whether it will represent highlight style or cursor
     *            style.If the start point same with the end point always
     *            represent cursor style.
     */
    public void setSelectedText(int start, int end, String newtxt, boolean isHighLight) {
        if (start <= end) {
            final String txt = getText();
            final int len = txt.length();
            if (start < 0)
                start = 0;
            if (start > len)
                start = len;
            if (end < 0)
                end = 0;
            if (end > len)
                end = len;

            if (newtxt == null)
                newtxt = "";

            setText(txt.substring(0, start) + newtxt + txt.substring(end));
            setSelectionRange(start, isHighLight ? start + newtxt.length() : start);
        }
    }

    /**
     * Sets the selection end to the specified position and the selection start
     * to the specified position. The new end point is constrained to be at or
     * after the current selection start. If the new start point is different
     * with the new end point, then will represent the result of highlight in
     * this text.
     *
     * <p>Set both arguments to the same value to move the cursor to
     * the corresponding position without selecting text.
     * 
     * @param start the start position of the text (included)
     * @param end the end position of the text (excluded)
     */
    public void setSelectionRange(int start, int end) {
        response(new AuSelect(this, start, end));
    }

    /**
     * Inserts the text at the current cursor position.
     * It would trigger focus and change event.
     *
     * @param text the text to be inserted
     * @since 8.5.1
     */
    public void setInsertedText(String text) {
        if (!Strings.isEmpty(text))
            response(new AuInvoke(this, "setInsertedText", text));
    }

    /** Checks whether user entered a wrong value (and not correct it yet).
     * Since user might enter a wrong value and moves on to other components,
     * this method is called when {@link #getText} or {@link #getTargetValue} is
     * called.
     *
     * <p>Derives rarely need to access this method if they use only
     * {@link #getText} and {@link #getTargetValue}.
     */
    protected void checkUserError() throws WrongValueException {
        if (_auxinf != null && _auxinf.errmsg != null)
            throw new WrongValueException(this, _auxinf.errmsg);
        //Note: we still throw exception to abort the exec flow
        //It's client's job NOT to show the error box!
        //(client checks z.srvald to decide whether to open error box)

        if (!_valided && _auxinf != null && _auxinf.constr != null)
            setText(coerceToString(_value));
    }

    /**
     * Returns the class name of the custom style applied to the errorbox of this component.
     * @return Sclass
     * @since 8.0.1
     */
    public String getErrorboxSclass() {
        return _auxinf != null ? _auxinf.errorboxSclass : null;
    }

    /**
     * Sets the class name of the custom style to be applied to the errorbox of this component.
     * @param sclass
     * @since 8.0.1
     */
    public void setErrorboxSclass(String sclass) {
        if (sclass != null && sclass.length() == 0)
            sclass = null;
        if (!Objects.equals(_auxinf != null ? _auxinf.errorboxSclass : null, sclass)) {
            initAuxInfoForInputElement().errorboxSclass = sclass;
            smartUpdate("errorboxSclass", getErrorboxSclass());
        }
    }

    /**
     * Returns the class name of the custom style applied to the errorbox icon of this component.
     * @return Sclass
     * @since 8.0.1
     */
    public String getErrorboxIconSclass() {
        return _auxinf != null ? _auxinf.errorboxIconSclass : null;
    }

    /**
     * Sets the class name of the custom style to be applied to the errorbox icon of this component.
     * @param iconSclass
     * @since 8.0.1
     */
    public void setErrorboxIconSclass(String iconSclass) {
        if (iconSclass != null && iconSclass.length() == 0)
            iconSclass = null;
        if (!Objects.equals(_auxinf != null ? _auxinf.errorboxSclass : null, iconSclass)) {
            initAuxInfoForInputElement().errorboxIconSclass = iconSclass;
            smartUpdate("errorboxIconSclass", getErrorboxIconSclass());
        }
    }
    
    /**
     * Returns the additional attributes which is set by {@code setInputAttributes(Map<String, String> inputAttributes)}.
     * @return inputAttributes a Map with attribute names as the keys.
     * @since 8.6.1
     */
    public Map<String, String> getInputAttributes() {
        return _inputAttributes;
    }

    /**
     * Sets some additional attributes to the input html tag in the component.
     * This will only reset the additional attributes that are set by this method.
     * @param inputAttributes a Map with attribute names as the keys.
     * @since 8.6.1
     */
    public void setInputAttributes(Map<String, String> inputAttributes) {
        if (!Objects.equals(_inputAttributes, inputAttributes)) {
            _inputAttributes = inputAttributes;
            smartUpdate("inputAttributes", _inputAttributes);
        }
    }
    
    /**
     * Sets some additional attributes to the input html tag in the component.
     * This will only reset the additional attributes that are set by this method.
     * @param inputAttributes a String of attribute separate by ";" and follow name=value rule.
     * for example: "spellcheck=true;autocorrect=on"
     * @since 8.6.1
     */
    public void setInputAttributes(String inputAttributes) {
        if (!Strings.isEmpty(inputAttributes)) {
            Map<String, String> map = new HashMap<String, String>();
            String[] attributes = inputAttributes.split(";");
            for (String attr : attributes) {
                String[] keyAndValue = attr.split("=");
                map.put(keyAndValue[0], keyAndValue[1]);
            }
            setInputAttributes(map);
        }
    }

    //-- Component --//
    /** Not childable. */
    protected boolean isChildable() {
        return false;
    }

    //-- ComponentCtrl --//
    public WrongValueException onWrongValue(WrongValueException ex) {
        initAuxInfoForInputElement().errmsg = Exceptions.getMessage(ex);
        return showCustomError(ex);
    }

    /** Marshall value to be sent to the client if needed.
     *
     * <p>Overrides it if the value to be sent to the client is not JSON Compatible.
     * @param value the value to be sent to the client
     * @return the marshalled value
     * @since 5.0.5
     */
    protected Object marshall(Object value) {
        return value;
    }

    /** Unmarshall value returned from client if needed.
     *
     * <p>Overrides it if the value returned is not JSON Compatible.
     * @param value the value returned from client
     * @return the unmarshalled value
     * @since 5.0.5
     */
    protected Object unmarshall(Object value) {
        return value;
    }

    private void setValueByClient(Object value, String valstr) {
        if (_auxinf != null && _auxinf.maxlength > 0 && valstr != null && valstr.length() > _auxinf.maxlength)
            throw new WrongValueException(this, MZul.STRING_TOO_LONG, new Integer(_auxinf.maxlength));

        final boolean same = Objects.equals(_value, value);
        boolean errFound = false;
        if (!same || !_valided || (_auxinf != null && _auxinf.errmsg != null)) { //note: the first time (!_valided) must always validate
            validate(value); //Bug 2946917: don't validate if not changed

            errFound = _auxinf != null && _auxinf.errmsg != null;
            clearErrorMessage(); //no error at all
        }

        if (!same) {
            _value = value;
        } else if (errFound) {
            smartUpdate("_value", marshall(_value)); //send back original value
            //Bug 1876292: make sure client see the updated value
        }
    }

    /** Processes an AU request.
     *
     * <p>Default: in addition to what are handled by {@link XulElement#service},
     * it also handles onChange, onChanging and onError.
     * @since 5.0.0
     */
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        if (cmd.equals(Events.ON_CHANGE)) {
            try {
                final Object oldval = _value;
                Object value = null;
                final Map<String, Object> data = request.getData();
                final String rawValue = (String) data.get("rawValue");
                if (rawValue != null) {
                    value = coerceFromString(rawValue);
                } else {
                    final Object clientv = data.get("value");
                    try {
                        value = unmarshall(clientv);
                    } catch (NumberFormatException ex) {
                        throw new WrongValueException(this, MZul.NUMBER_REQUIRED, clientv);
                    }
                }

                final String valstr = coerceToString(value);
                setValueByClient(value, valstr); //always since it might have func even not change
                if (rawValue != null && !rawValue.equals(valstr))
                    smartUpdate("_value", marshall(_value));

                if (Objects.equals(oldval, _value))
                    return; //Bug 1881557: don't post event if not modified

                final InputEvent evt = new InputEvent(cmd, this, valstr, oldval, //20101022, henrichen: for backward compatible, must coerceToString
                        AuRequests.getBoolean(data, "bySelectBack"), AuRequests.getInt(data, "start", 0));
                Events.postEvent(evt);
            } catch (WrongValueException ex) {
                initAuxInfoForInputElement().errmsg = ex.getMessage();
                throw ex; //No need to go through onWrongValue since UiEngine will do it
            }
        } else if (cmd.equals(Events.ON_CHANGING)) {
            final Map<String, Object> data = request.getData();
            final Object clientv = data.get("value");
            final Object oldval = _value;
            final InputEvent evt = new InputEvent(cmd, this, clientv == null ? "" : clientv.toString(), oldval, //clientv is what user input (not marshal)
                    AuRequests.getBoolean(data, "bySelectBack"), AuRequests.getInt(data, "start", 0));
            Events.postEvent(evt);
        } else if (cmd.equals(Events.ON_ERROR)) {
            ErrorEvent evt = ErrorEvent.getErrorEvent(request, _value);
            final String msg = evt.getMessage();
            initAuxInfoForInputElement().errmsg = msg != null && msg.length() > 0 ? msg : null;
            Events.postEvent(evt);
        } else if (cmd.equals(Events.ON_SELECTION)) {
            Events.postEvent(SelectionEvent.getSelectionEvent(request));
        } else
            super.service(request, everError);
    }

    //super//
    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
        super.renderProperties(renderer);

        render(renderer, "_value", marshall(_value));
        //ZK-658: we have to render the value before constraint
        render(renderer, "readonly", _readonly);
        render(renderer, "disabled", _disabled);
        render(renderer, "name", getName());
        render(renderer, "inplace", _inplace);

        if (_placeholder != null)
            render(renderer, "placeholder", _placeholder);
        if (_inputAttributes != null)
            render(renderer, "inputAttributes", _inputAttributes);

        int v;
        if ((v = getMaxlength()) > 0)
            renderer.render("maxlength", v);
        if (_cols > 0)
            renderer.render("cols", _cols);
        if (getInstant())
            renderer.render("instant", true);

        boolean constrDone = false;
        final Constraint constr = _auxinf != null ? _auxinf.constr : null;
        if (constr instanceof CustomConstraint) { //client ignored if custom
            renderer.render("constraint", "[c"); //implies validated at server
            constrDone = true;
        } else if (constr instanceof ClientConstraint) {
            final ClientConstraint cc = (ClientConstraint) constr;
            render(renderer, "_0", getClientPackages(cc)); //name doesn't matter

            final Object code = getClientConstraintCode(cc);
            if (code != null) {
                if (code instanceof JavaScriptValue)
                    renderer.render("z$al", code);
                else //must be string
                    renderer.renderDirectly("constraint", code);
                constrDone = true;
            }
        }
        if (!constrDone && constr != null)
            renderer.render("constraint", "[s");

        Utils.renderCrawlableText(coerceToString(_value));

        //ZK-2677
        render(renderer, "errorboxSclass", getErrorboxSclass());
        render(renderer, "errorboxIconSclass", getErrorboxIconSclass());
    }

    //--ComponentCtrl--//
    private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(12);

    static {
        _properties.put("name", new StringPropertyAccess() {
            public void setValue(Component cmp, String name) {
                ((InputElement) cmp).setName(name);
            }

            public String getValue(Component cmp) {
                return ((InputElement) cmp).getName();
            }
        });
        _properties.put("rawValue", new ObjectPropertyAccess() {
            public void setValue(Component cmp, Object rawValue) {
                ((InputElement) cmp).setRawValue(rawValue);
            }

            public Object getValue(Component cmp) {
                return ((InputElement) cmp).getRawValue();
            }
        });
        _properties.put("disabled", new BooleanPropertyAccess() {
            public void setValue(Component cmp, Boolean disabled) {
                ((InputElement) cmp).setDisabled(disabled);
            }

            public Boolean getValue(Component cmp) {
                return ((InputElement) cmp).isDisabled();
            }
        });

        _properties.put("readonly", new BooleanPropertyAccess() {
            public void setValue(Component cmp, Boolean readonly) {
                ((InputElement) cmp).setReadonly(readonly);
            }

            public Boolean getValue(Component cmp) {
                return ((InputElement) cmp).isReadonly();
            }
        });

        _properties.put("placeholder", new StringPropertyAccess() {
            public void setValue(Component cmp, String placeholder) {
                ((InputElement) cmp).setPlaceholder(placeholder);
            }

            public String getValue(Component cmp) {
                return ((InputElement) cmp).getPlaceholder();
            }
        });

        _properties.put("inplace", new BooleanPropertyAccess() {
            public void setValue(Component cmp, Boolean inplace) {
                ((InputElement) cmp).setInplace(inplace);
            }

            public Boolean getValue(Component cmp) {
                return ((InputElement) cmp).isInplace();
            }
        });

        _properties.put("instant", new BooleanPropertyAccess() {
            public void setValue(Component cmp, Boolean instant) {
                ((InputElement) cmp).setInstant(instant);
            }

            public Boolean getValue(Component cmp) {
                return ((InputElement) cmp).isInstant();
            }
        });

        _properties.put("maxlength", new IntPropertyAccess() {
            public void setValue(Component cmp, Integer maxlength) {
                ((InputElement) cmp).setMaxlength(maxlength);
            }

            public Integer getValue(Component cmp) {
                return ((InputElement) cmp).getMaxlength();
            }
        });

        _properties.put("cols", new IntPropertyAccess() {
            public void setValue(Component cmp, Integer cols) {
                ((InputElement) cmp).setCols(cols);
            }

            public Integer getValue(Component cmp) {
                return ((InputElement) cmp).getCols();
            }
        });

        _properties.put("errorboxSclass", new StringPropertyAccess() {
            public void setValue(Component cmp, String errorboxSclass) {
                ((InputElement) cmp).setErrorboxSclass(errorboxSclass);
            }

            public String getValue(Component cmp) {
                return ((InputElement) cmp).getErrorboxSclass();
            }
        });

        _properties.put("errorboxIconSclass", new StringPropertyAccess() {
            public void setValue(Component cmp, String errorboxIconSclass) {
                ((InputElement) cmp).setErrorboxIconSclass(errorboxIconSclass);
            }

            public String getValue(Component cmp) {
                return ((InputElement) cmp).getErrorboxIconSclass();
            }
        });
    }

    public PropertyAccess getPropertyAccess(String prop) {
        PropertyAccess pa = _properties.get(prop);
        if (pa != null)
            return pa;
        return super.getPropertyAccess(prop);
    }

    //Cloneable//
    public Object clone() {
        final InputElement clone = (InputElement) super.clone();
        if (_auxinf != null)
            clone._auxinf = (AuxInfo) _auxinf.clone();
        return clone;
    }

    private AuxInfo initAuxInfoForInputElement() {
        if (_auxinf == null)
            _auxinf = new AuxInfo();
        return _auxinf;
    }

    private static class AuxInfo implements java.io.Serializable, Cloneable {
        /** The error message. Not null if users entered a wrong data (and
         * not correct it yet).
         */
        private String errmsg;
        /** The name. */
        private String name;
        private int maxlength;
        /** Whether to send onChange as soon as possible */
        private boolean instant;
        private Constraint constr;
        /** Whether the validation is caused by {@link #isValid}. */
        private transient boolean checkOnly;
        private String errorboxSclass;
        private String errorboxIconSclass;

        public Object clone() {
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {
                throw new InternalError();
            }
        }
    }
}