5

I've been trying to create a custom bindingHandler that i can use to give a watermark behaviour to text input fields.

By watermark i mean: to add default values to text fields that are removed on focus, and replaced on blur if the text field is still empty

I have managed to get this to work as demonstrated in this jsfiddle: http://jsfiddle.net/rpallas/nvxuw/

I have 3 questions about this solution:

  1. Is there any way to change it so that I only have to declare the watermark value once? Currently I have to put it on the place where I declare the binding and I also have to initialise the observable with the same value in the viewModel - as it will otherwise have no initial value.
  2. Is there a better way of getting to the underlying observable that the elements value is bound to. I'm currently grabbing it using the allBindingsAccessor, but this feels wrong to me. Originally I was just setting the value using jquery $(element).val('') but this also felt wrong. Which is best, or is there a better way?
  3. Does any one have or know of an existing solution to this this problem? Am I re-inventing the wheel?
Robbie
  • 18,750
  • 4
  • 41
  • 45

2 Answers2

14

I think you're use of allbindings is unecessary. In fact I don't think the watermark needs to be aware of the observable at all since that's what a watermark generally does i.e the placeholder attribute.

Would this work for you?

ko.bindingHandlers.watermark = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        var value = valueAccessor(), allBindings = allBindingsAccessor();
        var defaultWatermark = ko.utils.unwrapObservable(value);
        var $element = $(element);

        setTimeout(function() {
            $element.val(defaultWatermark);}, 0);

        $element.focus(
            function () {
                if ($element.val() === defaultWatermark) {
                    $element.val("");
                }
            }).blur(function () {
                if ($element.val() === '') {
                    $element.val(defaultWatermark)
                }
            });
    }
};

http://jsfiddle.net/madcapnmckay/Q5yME/1/

Hope this helps.

madcapnmckay
  • 15,782
  • 6
  • 61
  • 78
  • 1
    yeah, that's almost exactly what i had before i changed it to use the allBindingsAccessor. I was missing the `setTimeout` when attempting to set the initial value. Could you explain briefly why that is required? Also, do you know if there is a better way? or do you think this is a good (enough) way (in terms of the whole solution)? For example i notice that there is a hasfocus binding (built-in). Could that be a better approach? – Robbie Jun 07 '12 at 18:02
  • 2
    I think this approach is fine if you want to support older browsers. For new ones just use the placeholder attribute. The setTimeout is needed because internally KO uses a setTimeout before setting the value of the input. This means that your code was running before the KO code set the value hence you need the setTimeout to again make sure your code comes last in the execution. – madcapnmckay Jun 07 '12 at 18:21
  • Woah. I was not expecting to find a complete solution to this. Thanks! – Pure Function Jan 24 '13 at 01:13
  • Very interesting regarding setTimeout, I didn't realise that was required until now. Thanks. – Rob Curtis Nov 13 '13 at 09:30
1

The previous approach is fine as long as your app logic is really simple, be aware that solution is messing with the values of your View Model, those values can be observables and they can have subscriptions or computeds associate to it, so by changing the value you change your View Model. Here is a different solution without updating your View Model

ko.bindingHandlers.fakePlaceHolderWhenNeedIt = {
    init: function (element, valueAccessor, allBindings, vm) {
     if (!Modernizr.input.placeholder) {
        var placeHolderVal = $(element).attr("placeholder");

        if (placeHolderVal != null || placeHolderVal != '') {

            var $element = $(element);
            var value = valueAccessor()
            var valueUnwrapped = ko.utils.unwrapObservable(value);


            $element.keyup(function () {
                var inputValue = $(this).val();
                var $watermark = $(this).prev('.ie-placeholder');
                if (inputValue == null || inputValue == '') {
                    $watermark.show();
                }
                else {
                    $watermark.hide();
                }
            });

            var display = valueUnwrapped != null || valueUnwrapped != '' ? "block" : "none";
            var left = $element.position().left;
            var top = $element.position().top;
            var paddingLeft = $element.css('padding-left');
            var paddingRight = $element.css('padding-right');
            var paddingTop = $element.css('padding-top');
            var paddingBottom = $element.css('padding-bottom');

            var height = $element.css('height');
            var placeHolder = '<div class="ie-placeholder" style="position:absolute;left:' + left + ';top:' + top + ';padding-top: ' + paddingTop + ';padding-bottom: ' + paddingBottom + ';padding-left: ' + paddingLeft + ';padding-right: ' + paddingRight + ';height: ' + height + ';line-height:' + height + ';display:' + display + ';">' + placeHolderVal + '</div>';

            $(placeHolder).click(function () { $element.focus(); }).insertBefore(element);
        }
    }
},
update: function (element, valueAccessor, allBindings, vm) {
    if (!Modernizr.input.placeholder) {
        var placeHolderVal = $(element).attr("placeholder");

        if (placeHolderVal != null || placeHolderVal != '') {
            var $element = $(element);
            var value = valueAccessor()
            var valueUnwrapped = ko.utils.unwrapObservable(value);

            var $watermark = $element.prev('.ie-placeholder');
            if (valueUnwrapped == null || valueUnwrapped == '') {
                $watermark.show();
            }
            else {
                $watermark.hide();
            }
        }
    }
}