Knockout-Validation icon indicating copy to clipboard operation
Knockout-Validation copied to clipboard

feature request: custom method to display error message

Open andreykl opened this issue 10 years ago • 4 comments

Hello

I like ko and ko.validation plugin. Good work, guys. Thank you. I'm trying to use ko and validation in my project now.

Problem which I faced is next: I would like to have custom show-error function to display errors in my own way. Particularly, I would like to show tooltip (like jquery-ui-tooltip or tipsy) to display errors.

I think kv could use something like displayErrors: function(element, observable, errors) {...} in configuration to allow custom message rendering. And also hideErrors: function(element, observable) {...} to allow hide error messages. And kv could trigger this functions when it need to display errors. And current implementation would became the default implementation, so, everyone can easy replace default implementation. I think it will give great flexibility in message displaying.

I'm thinking now about implementing this.

Please, let me know what do you think? Will this be good feature for kv and will you accept pull request (what is your politics in this part)?

andreykl avatar Mar 05 '14 14:03 andreykl

It could work, but I would have thought you could do what you want by using the validationMessage binding. I'm not sure there is necessarily the need to integrate this into the core library.

Maybe I'm missing the point though...

stevegreatrex avatar Mar 05 '14 14:03 stevegreatrex

I missed this. Thank you for pointing me, checking it.

andreykl avatar Mar 05 '14 14:03 andreykl

The using of validationMessage binding is not too easy for my purposes. The problem is that tooltip I would like to use (this one http://onehackoranother.com/projects/jquery/tipsy/ ) has rather sofisticated render method. And to use it I need a way to call something like $(element).tipsy('show'). Otherwise I'm not sure how to have it work except repeating render method's code somewhere.

To gain my goal I created my own binding. Just very similar to validationMessage:

ko.bindingHandlers['showTooltip'] = {
    update: function (element, valueAccessor) {
        var cfg = koUtils.unwrapObservable(valueAccessor())
        var obsv = cfg.value,
            config = kv.utils.getConfigOptions(element),
            val = unwrap(obsv),
            msg = null,
            isModified = false,
            isValid = false,
            tooltipShow = cfg.tooltipShow,
            tooltipHide = cfg.tooltipHide,
            tooltipSetMessage = cfg.tooltipSetMessage;

        if (!obsv.isValid || !obsv.isModified) {
            throw new Error("Observable is not validatable");
        }

        isModified = obsv.isModified();
        isValid = obsv.isValid();

        var error = null;
        if (!config.messagesOnModified || isModified) {
            error = isValid ? null : obsv.error;
        }

        var isVisible = !config.messagesOnModified || isModified ? !isValid : false;
        var errTxt = koUtils.unwrapObservable(error);

        tooltipHide(element)

        if(isVisible) {
            tooltipSetMessage(element, errTxt)
            tooltipShow(element)
        }
    }
};

html will be

<input placeholder="Company Name*" type="text" name="companyName" data-bind="value: model.companyName, showTooltip: {value: model.companyName, tooltipShow: tooltipShow, tooltipHide: tooltipHide, tooltipSetMessage: tooltipSetMessage}" />

It works as expected. But I think this is little more complicated then I would like to have. May be I dont do it in right way. Now, when you have a code, could you look at this and say me may be you see a simpler way?

UPD1. Also, right now I faced one more interesting feature for me: I would like to hide error message when element receives focus (so, on focus event). For exactly my particular case I can just do $(element).focus(function() { hideTooltip(this) }), but I think may be this is not so bad idea to have default way to do it for any type of messages. I think it may be good for user experience because it looks like system responds on user actions quickly.

UPD2. I also have created demonstration here http://embed.plnkr.co/rYmczX2NRMMBoNE62BWd/preview (Enter 1 symbol in input field and press the button.) You can then press "Edit" in top right corner to view code.

UPD3. May be it can be just a binding, but one needs only rename it to something like "customMessage" (and options like showMessageFunction, hideMessageFunction, setMessageTextFunction), what do you think? Can it be part of core?

UPD4. also, I think may be issue https://github.com/Knockout-Contrib/Knockout-Validation/issues/238 is very close to current one.

andreykl avatar Mar 05 '14 17:03 andreykl

I've solved this by customizing the code with an extra configuration "messageBindingHandler" (which is the name of the KO binding that handles the error presentation).

Only a smaller change in the Knockout.Validation code. Row 1016:

// if requested insert message element and apply bindings
if (config.insertMessages && kv.utils.isValidatable(observable)) {

    //HACK: Custom KO binding for custom presentation.
    if (config.messageBindingHandler) {
        var binding = {};
        binding[config.messageBindingHandler] = valueAccessor();

        ko.applyBindingsToNode(element, binding);
    } else {
        // insert the <span></span>
        var validationMessageElement = kv.insertValidationMessage(element);

KO Validation is initialized with;

        ko.validation.init({
            messageBindingHandler: 'validationPopOverMessage'
        }, true);

The messageBinding uses jQuery PopOver to show the error... hooks into isModified, onFocus etc.

//Some code removed for easier overview

var ValidationPopOverMessage = (function () {
    function ValidationPopOverMessage() {
    }
    ValidationPopOverMessage.init = function (element, valueAccessor, allBindingsAccessor) {
        var obsv = valueAccessor();
        var input = $(element);

        obsv.extend({ validatable: true });

        var errorMsgAccessor = function () {
            if (obsv.isModified()) {
                return obsv.isValid() ? null : ko.unwrap(obsv.error);
            } else {
                return null;
            }
        };

        var onModifyValidChange = function (newValue) {
            var show = obsv.isModified() ? !obsv.isValid() : false;
            input.popover(show ? 'show' : 'hide');

            if (show) {
                var popOver = btn.parents('.popover');
                if (!popOver.hasClass('validationPopup')) {
                    popOver.addClass('alert-error');
                    popOver.addClass('validationPopup');
                }
            } 
        };

        var modifySubscription = obsv.isModified.subscribe(onModifyValidChange);
        var validateSubscription = obsv.isValid.subscribe(onModifyValidChange);

        var options = {
            placement: 'bottom',
            container: null,
            trigger: 'manual',
            html: true,
            title: '',
            content: errorMsgAccessor
        };

        var bindingOptions = allBindingsAccessor() && allBindingsAccessor().validationPopoverOptions ? allBindingsAccessor().validationPopoverOptions : {};
        $.extend(options, bindingOptions);

        input.popover(options);

        var focusHandler = function () {
            input.popover('hide');
        };

        input.bind('blur', onModifyValidChange);

        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            modifySubscription.dispose();
            validateSubscription.dispose();
            input.unbind('focus', focusHandler);
            input.unbind('blur', onModifyValidChange);
            input.popover('destroy');
        });
    };

    ValidationPopOverMessage.update = function (element, valueAccessor) {
    };
    return ValidationPopOverMessage;
})();
;
ko.bindingHandlers['validationPopOverMessage'] = ValidationPopOverMessage;

jstrandelid avatar May 18 '15 13:05 jstrandelid